halfcycle 0.5.1 → 0.5.2
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 +70 -30
- package/dist/cli.js +149 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +149 -4
- package/dist/index.js.map +1 -1
- 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/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
|
|
5
5
|
"commands": [
|
|
6
6
|
{
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -14665,6 +14665,9 @@ var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
|
14665
14665
|
var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
|
|
14666
14666
|
var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
|
|
14667
14667
|
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
14668
|
+
var CONTEXT_BUDGET_CHECK_REL = ".claude/hooks/context-budget-check.sh";
|
|
14669
|
+
var CONTEXT_DRIFT_CHECK_REL = ".claude/hooks/context-drift-check.sh";
|
|
14670
|
+
var CONTEXT_DRIFT_MATCHER = "Edit|Write";
|
|
14668
14671
|
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
14669
14672
|
var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
|
|
14670
14673
|
function hookCommand(script, verb) {
|
|
@@ -14741,6 +14744,20 @@ var HALFCYCLE_HOOK_WIRING = [
|
|
|
14741
14744
|
timeout: 5,
|
|
14742
14745
|
command: hookCommand(SESSION_END_MARKER_REL),
|
|
14743
14746
|
script: SESSION_END_MARKER_REL
|
|
14747
|
+
},
|
|
14748
|
+
{
|
|
14749
|
+
event: "SessionStart",
|
|
14750
|
+
matcher: null,
|
|
14751
|
+
timeout: 10,
|
|
14752
|
+
command: hookCommand(CONTEXT_BUDGET_CHECK_REL),
|
|
14753
|
+
script: CONTEXT_BUDGET_CHECK_REL
|
|
14754
|
+
},
|
|
14755
|
+
{
|
|
14756
|
+
event: "PostToolUse",
|
|
14757
|
+
matcher: CONTEXT_DRIFT_MATCHER,
|
|
14758
|
+
timeout: 5,
|
|
14759
|
+
command: hookCommand(CONTEXT_DRIFT_CHECK_REL),
|
|
14760
|
+
script: CONTEXT_DRIFT_CHECK_REL
|
|
14744
14761
|
}
|
|
14745
14762
|
];
|
|
14746
14763
|
var RETIRED_HOOK_COMMANDS = [
|
|
@@ -14750,7 +14767,9 @@ var RETIRED_HOOK_COMMANDS = [
|
|
|
14750
14767
|
"bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
|
|
14751
14768
|
"bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
14752
14769
|
"bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
|
|
14753
|
-
"bash ./.claude/hooks/session-end-marker.sh"
|
|
14770
|
+
"bash ./.claude/hooks/session-end-marker.sh",
|
|
14771
|
+
"bash ./.claude/hooks/context-budget-check.sh",
|
|
14772
|
+
"bash ./.claude/hooks/context-drift-check.sh"
|
|
14754
14773
|
];
|
|
14755
14774
|
var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
|
|
14756
14775
|
...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
|
|
@@ -15154,7 +15173,9 @@ var CODEX_HOOK_WIRING = [
|
|
|
15154
15173
|
codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 60, GUARD_RUNNER_REL, "hook post-tool-use --agent codex"),
|
|
15155
15174
|
codexHookRow("Stop", null, 60, GUARD_RUNNER_REL, "hook stop --agent codex"),
|
|
15156
15175
|
codexHookRow("SubagentStop", null, 60, GUARD_RUNNER_REL, "hook subagent-stop --agent codex"),
|
|
15157
|
-
codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL)
|
|
15176
|
+
codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL),
|
|
15177
|
+
codexHookRow("SessionStart", null, 10, CONTEXT_BUDGET_CHECK_REL),
|
|
15178
|
+
codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 5, CONTEXT_DRIFT_CHECK_REL)
|
|
15158
15179
|
];
|
|
15159
15180
|
var RETIRED_CODEX_HOOK_COMMANDS = [];
|
|
15160
15181
|
function codexHookCommandSet(retired) {
|
|
@@ -29234,7 +29255,8 @@ function refusalFor(facts, description) {
|
|
|
29234
29255
|
function heldStep(steps, description, why) {
|
|
29235
29256
|
return {
|
|
29236
29257
|
person: PERSON_HOLD_LINE,
|
|
29237
|
-
agent: `${HELD_STEP_OPENER}${steps}. ${reasonSentence(description)} ${PRECAUTION_OPENER}${why}. ${HOLD_AGENT_INSTRUCTION}
|
|
29258
|
+
agent: `${HELD_STEP_OPENER}${steps}. ${reasonSentence(description)} ${PRECAUTION_OPENER}${why}. ${HOLD_AGENT_INSTRUCTION}`,
|
|
29259
|
+
step: steps
|
|
29238
29260
|
};
|
|
29239
29261
|
}
|
|
29240
29262
|
var STEP_VERBS = {
|
|
@@ -29651,6 +29673,7 @@ import { createHash as createHash5 } from "node:crypto";
|
|
|
29651
29673
|
import { appendFileSync as appendFileSync4, readFileSync as readFileSync20, renameSync as renameSync10, unlinkSync as unlinkSync9, writeFileSync as writeFileSync12 } from "node:fs";
|
|
29652
29674
|
var ALLOW_ONCE_MS = 5 * 60 * 1e3;
|
|
29653
29675
|
var HELD_STEP_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
29676
|
+
var STEP_MAX_LENGTH = 1e3;
|
|
29654
29677
|
function isAllowReply(prompt) {
|
|
29655
29678
|
return typeof prompt === "string" && prompt.trim().toLowerCase() === ALLOW_REPLY;
|
|
29656
29679
|
}
|
|
@@ -29676,11 +29699,11 @@ function canonical(value) {
|
|
|
29676
29699
|
}
|
|
29677
29700
|
return JSON.stringify(value) ?? "null";
|
|
29678
29701
|
}
|
|
29679
|
-
function recordHeld(sessionId, key, deps = {}) {
|
|
29702
|
+
function recordHeld(sessionId, key, deps = {}, step = null) {
|
|
29680
29703
|
const paths = allowOncePaths(sessionId, deps.dir);
|
|
29681
29704
|
if (paths === null)
|
|
29682
29705
|
return;
|
|
29683
|
-
const record2 = { version: 1, session: sessionId, key, at: nowOf(deps) };
|
|
29706
|
+
const record2 = { version: 1, session: sessionId, key, at: nowOf(deps), ...isStep(step) ? { step } : {} };
|
|
29684
29707
|
try {
|
|
29685
29708
|
appendFileSync4(paths.held, `${JSON.stringify(record2)}
|
|
29686
29709
|
`, { mode: 384 });
|
|
@@ -29688,23 +29711,26 @@ function recordHeld(sessionId, key, deps = {}) {
|
|
|
29688
29711
|
return;
|
|
29689
29712
|
}
|
|
29690
29713
|
}
|
|
29691
|
-
function
|
|
29714
|
+
function armAllowStep(sessionId, deps = {}) {
|
|
29692
29715
|
try {
|
|
29693
29716
|
const paths = allowOncePaths(sessionId, deps.dir);
|
|
29694
29717
|
if (paths === null)
|
|
29695
|
-
return "nothing-held";
|
|
29718
|
+
return { outcome: "nothing-held", step: null };
|
|
29696
29719
|
const now = nowOf(deps);
|
|
29697
|
-
const
|
|
29720
|
+
const held = claimHeld(paths.held, sessionId, now);
|
|
29721
|
+
const keys = [...new Set(held.map((r) => r.key))];
|
|
29698
29722
|
if (keys.length === 0)
|
|
29699
|
-
return "nothing-held";
|
|
29723
|
+
return { outcome: "nothing-held", step: null };
|
|
29700
29724
|
if (keys.length > 1)
|
|
29701
|
-
return "several-held";
|
|
29702
|
-
|
|
29725
|
+
return { outcome: "several-held", step: null };
|
|
29726
|
+
const step = held[held.length - 1].step ?? null;
|
|
29727
|
+
const saved = writeRecord(paths.allow, { version: 1, session: sessionId, key: keys[0], at: now });
|
|
29728
|
+
return saved ? { outcome: "armed", step } : { outcome: "not-saved", step: null };
|
|
29703
29729
|
} catch {
|
|
29704
|
-
return "not-saved";
|
|
29730
|
+
return { outcome: "not-saved", step: null };
|
|
29705
29731
|
}
|
|
29706
29732
|
}
|
|
29707
|
-
function
|
|
29733
|
+
function claimHeld(path, session, now) {
|
|
29708
29734
|
const claimed = `${path}.${process.pid}.${now}.claim`;
|
|
29709
29735
|
try {
|
|
29710
29736
|
renameSync10(path, claimed);
|
|
@@ -29718,13 +29744,16 @@ function claimHeldKeys(path, session, now) {
|
|
|
29718
29744
|
text = "";
|
|
29719
29745
|
}
|
|
29720
29746
|
removeQuietly(claimed);
|
|
29721
|
-
const
|
|
29747
|
+
const records = [];
|
|
29722
29748
|
for (const line2 of text.split("\n")) {
|
|
29723
29749
|
const r = parseRecord(line2, session);
|
|
29724
29750
|
if (r !== null && within(r.at, now, HELD_STEP_MAX_AGE_MS))
|
|
29725
|
-
|
|
29751
|
+
records.push(r);
|
|
29726
29752
|
}
|
|
29727
|
-
return
|
|
29753
|
+
return records;
|
|
29754
|
+
}
|
|
29755
|
+
function isStep(v) {
|
|
29756
|
+
return typeof v === "string" && v !== "" && v.length <= STEP_MAX_LENGTH;
|
|
29728
29757
|
}
|
|
29729
29758
|
function spendAllow(sessionId, key, deps = {}) {
|
|
29730
29759
|
try {
|
|
@@ -29776,7 +29805,8 @@ function parseRecord(text, session) {
|
|
|
29776
29805
|
return null;
|
|
29777
29806
|
if (typeof r["at"] !== "number")
|
|
29778
29807
|
return null;
|
|
29779
|
-
|
|
29808
|
+
const step = r["step"];
|
|
29809
|
+
return { version: 1, session, key: r["key"], at: r["at"], ...isStep(step) ? { step } : {} };
|
|
29780
29810
|
}
|
|
29781
29811
|
function writeRecord(path, record2) {
|
|
29782
29812
|
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -30696,7 +30726,7 @@ function runPreToolUse(deps) {
|
|
|
30696
30726
|
const allowOnce = { ...deps.liveness?.dir === void 0 ? {} : { dir: deps.liveness.dir }, now: () => at.getTime() };
|
|
30697
30727
|
if (spendAllow(input["session_id"], key, allowOnce))
|
|
30698
30728
|
return "";
|
|
30699
|
-
recordHeld(input["session_id"], key, allowOnce);
|
|
30729
|
+
recordHeld(input["session_id"], key, allowOnce, answer.refusal.step);
|
|
30700
30730
|
}
|
|
30701
30731
|
}
|
|
30702
30732
|
return `${preToolUseReplyText("refuse", answer.refusal.person, answer.refusal.agent)}
|
|
@@ -31684,14 +31714,15 @@ function autonomyLineForAnswer(answer) {
|
|
|
31684
31714
|
}
|
|
31685
31715
|
var NOTHING_ALLOWED = "Nothing was allowed";
|
|
31686
31716
|
var ALLOW_MINUTES = Math.round(ALLOW_ONCE_MS / 6e4);
|
|
31687
|
-
function allowArmedLine() {
|
|
31688
|
-
|
|
31717
|
+
function allowArmedLine(step = null) {
|
|
31718
|
+
const named = step === null ? "The person allowed the paused step once." : `The person allowed this paused step once: ${step}.`;
|
|
31719
|
+
return line(`${named} Run it again now, exactly as before; it will go ahead once if it runs within ${ALLOW_MINUTES} minutes, and any other risky step is still paused.`);
|
|
31689
31720
|
}
|
|
31690
31721
|
function allowNothingHeldLine() {
|
|
31691
31722
|
return line(`${NOTHING_ALLOWED}: no step is paused in this session.`);
|
|
31692
31723
|
}
|
|
31693
31724
|
function allowSeveralHeldLine() {
|
|
31694
|
-
return
|
|
31725
|
+
return `${NOTHING_ALLOWED}: more than one step was paused, so Halfcycle could not tell which one you meant. Nothing ran. When your agent tries the step again, type ${ALLOW_REPLY} once more. ${SHOW_AS_WRITTEN}`;
|
|
31695
31726
|
}
|
|
31696
31727
|
function allowNotAttendedLine() {
|
|
31697
31728
|
return line(`${NOTHING_ALLOWED}: a paused step can be allowed by typing only when a person types it into an open session.`);
|
|
@@ -31700,14 +31731,18 @@ function allowNotSavedLine() {
|
|
|
31700
31731
|
return line(`${NOTHING_ALLOWED}: Halfcycle could not save the answer on this computer. Type ${ALLOW_REPLY} again.`);
|
|
31701
31732
|
}
|
|
31702
31733
|
var CODEX_TYPING_NEVER_COUNTS = "in Codex, Halfcycle cannot tell a line a person typed from one a program sent, so a typed decision never counts there";
|
|
31734
|
+
var SHOW_AS_WRITTEN = "Show the person this line as written.";
|
|
31735
|
+
function codexLine(text) {
|
|
31736
|
+
return `${text} ${SHOW_AS_WRITTEN}`;
|
|
31737
|
+
}
|
|
31703
31738
|
function codexScopeLine(act) {
|
|
31704
|
-
return act === "approve" ?
|
|
31739
|
+
return act === "approve" ? codexLine(`${nothingDone(act)}: ${CODEX_TYPING_NEVER_COUNTS}. To approve, choose Accept in the console.`) : codexLine(`${nothingDone(act)}: ${CODEX_TYPING_NEVER_COUNTS}. The console offers only Accept, so work cannot be sent back from Codex.`);
|
|
31705
31740
|
}
|
|
31706
31741
|
function codexReviewLine(act) {
|
|
31707
|
-
return act === "accept" ?
|
|
31742
|
+
return act === "accept" ? codexLine(`${NOTHING_RECORDED}: ${CODEX_TYPING_NEVER_COUNTS}. To accept, choose Accept in the console.`) : codexLine(`${NOTHING_RECORDED}: ${CODEX_TYPING_NEVER_COUNTS}. The console offers only Accept, so this cannot be done from Codex.`);
|
|
31708
31743
|
}
|
|
31709
31744
|
function codexAutonomyLine() {
|
|
31710
|
-
return
|
|
31745
|
+
return codexLine(`${AUTONOMY_NOTHING_CHANGED}: ${CODEX_TYPING_NEVER_COUNTS}. Turn it on or off with "${AUTONOMY_A1_TITLE}" in the console's settings.`);
|
|
31711
31746
|
}
|
|
31712
31747
|
|
|
31713
31748
|
// dist/hooks/user-prompt-submit.js
|
|
@@ -31772,13 +31807,13 @@ function decideAllow(opts) {
|
|
|
31772
31807
|
const env = opts.env ?? process.env;
|
|
31773
31808
|
if ((opts.agent ?? "claude-code") === "claude-code" && !readSessionAttended(env))
|
|
31774
31809
|
return allowNotAttendedLine();
|
|
31775
|
-
const outcome =
|
|
31810
|
+
const { outcome, step } = armAllowStep(sessionIdOf(opts.input), {
|
|
31776
31811
|
...opts.tempDir === void 0 ? {} : { dir: opts.tempDir },
|
|
31777
31812
|
...opts.now === void 0 ? {} : { now: opts.now }
|
|
31778
31813
|
});
|
|
31779
31814
|
switch (outcome) {
|
|
31780
31815
|
case "armed":
|
|
31781
|
-
return allowArmedLine();
|
|
31816
|
+
return allowArmedLine(step);
|
|
31782
31817
|
case "nothing-held":
|
|
31783
31818
|
return allowNothingHeldLine();
|
|
31784
31819
|
case "several-held":
|
|
@@ -32415,6 +32450,10 @@ function isCheckedSessionStart(input, profile) {
|
|
|
32415
32450
|
return CHECKED_SOURCES.has(source);
|
|
32416
32451
|
}
|
|
32417
32452
|
var CONNECTED_SENTENCE = `Halfcycle's sign-in works and its service answered. If its tools are missing, turn on ${MCP_SERVER_KEY} in /mcp; if it is not listed, run "claude mcp reset-project-choices" here and restart Claude Code.`;
|
|
32453
|
+
var CODEX_CONNECTED_SENTENCE = "Halfcycle's sign-in works and its service answered. If its tools are missing, start a new Codex session to reconnect them.";
|
|
32454
|
+
function connectedSentenceFor(profile) {
|
|
32455
|
+
return profile.id === "codex" ? CODEX_CONNECTED_SENTENCE : CONNECTED_SENTENCE;
|
|
32456
|
+
}
|
|
32418
32457
|
var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
|
|
32419
32458
|
var SESSION_START_PROBE_ATTEMPTS = 2;
|
|
32420
32459
|
function registeredMcpUrl(projectRoot) {
|
|
@@ -32501,6 +32540,7 @@ async function probeDelivery(url2, bearer, fetchFn, deadlineMs, maxAttempts) {
|
|
|
32501
32540
|
}
|
|
32502
32541
|
}
|
|
32503
32542
|
async function checkConnection(renewedToken, opts) {
|
|
32543
|
+
const profile = opts.profile ?? AGENT_PROFILES["claude-code"];
|
|
32504
32544
|
const url2 = registeredMcpUrl(projectRootFromBinary(opts.binaryPath));
|
|
32505
32545
|
if (url2 === void 0)
|
|
32506
32546
|
return void 0;
|
|
@@ -32514,10 +32554,10 @@ async function checkConnection(renewedToken, opts) {
|
|
|
32514
32554
|
environmentTokenFirst: false
|
|
32515
32555
|
});
|
|
32516
32556
|
if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
|
|
32517
|
-
return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer };
|
|
32557
|
+
return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer, connected: false };
|
|
32518
32558
|
}
|
|
32519
32559
|
const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS, opts.maxAttempts ?? SESSION_START_PROBE_ATTEMPTS);
|
|
32520
|
-
return { sentence: outcome ??
|
|
32560
|
+
return { sentence: outcome ?? connectedSentenceFor(profile), url: url2, bearer, connected: outcome === void 0 };
|
|
32521
32561
|
}
|
|
32522
32562
|
function positionRequestBody(engagementId) {
|
|
32523
32563
|
return JSON.stringify({
|
|
@@ -32607,10 +32647,10 @@ async function runRefreshCredential(opts) {
|
|
|
32607
32647
|
renewalOpen = false;
|
|
32608
32648
|
const parts = [];
|
|
32609
32649
|
if (isCheckedSessionStart(input, profile)) {
|
|
32610
|
-
const connection = await checkConnection(renewedToken, { ...opts, writeErr });
|
|
32650
|
+
const connection = await checkConnection(renewedToken, { ...opts, profile, writeErr });
|
|
32611
32651
|
if (connection !== void 0) {
|
|
32612
32652
|
parts.push(connection.sentence);
|
|
32613
|
-
if (connection.
|
|
32653
|
+
if (connection.connected) {
|
|
32614
32654
|
const engagementId = readConfig().config?.guardEngagementId;
|
|
32615
32655
|
if (engagementId !== void 0) {
|
|
32616
32656
|
const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
|
package/dist/cli.js
CHANGED
|
@@ -148,6 +148,9 @@ var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
|
148
148
|
var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
|
|
149
149
|
var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
|
|
150
150
|
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
151
|
+
var CONTEXT_BUDGET_CHECK_REL = ".claude/hooks/context-budget-check.sh";
|
|
152
|
+
var CONTEXT_DRIFT_CHECK_REL = ".claude/hooks/context-drift-check.sh";
|
|
153
|
+
var CONTEXT_DRIFT_MATCHER = "Edit|Write";
|
|
151
154
|
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
152
155
|
var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
|
|
153
156
|
function hookCommand(script, verb) {
|
|
@@ -224,6 +227,20 @@ var HALFCYCLE_HOOK_WIRING = [
|
|
|
224
227
|
timeout: 5,
|
|
225
228
|
command: hookCommand(SESSION_END_MARKER_REL),
|
|
226
229
|
script: SESSION_END_MARKER_REL
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
event: "SessionStart",
|
|
233
|
+
matcher: null,
|
|
234
|
+
timeout: 10,
|
|
235
|
+
command: hookCommand(CONTEXT_BUDGET_CHECK_REL),
|
|
236
|
+
script: CONTEXT_BUDGET_CHECK_REL
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
event: "PostToolUse",
|
|
240
|
+
matcher: CONTEXT_DRIFT_MATCHER,
|
|
241
|
+
timeout: 5,
|
|
242
|
+
command: hookCommand(CONTEXT_DRIFT_CHECK_REL),
|
|
243
|
+
script: CONTEXT_DRIFT_CHECK_REL
|
|
227
244
|
}
|
|
228
245
|
];
|
|
229
246
|
var RETIRED_HOOK_COMMANDS = [
|
|
@@ -233,7 +250,9 @@ var RETIRED_HOOK_COMMANDS = [
|
|
|
233
250
|
"bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
|
|
234
251
|
"bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
235
252
|
"bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
|
|
236
|
-
"bash ./.claude/hooks/session-end-marker.sh"
|
|
253
|
+
"bash ./.claude/hooks/session-end-marker.sh",
|
|
254
|
+
"bash ./.claude/hooks/context-budget-check.sh",
|
|
255
|
+
"bash ./.claude/hooks/context-drift-check.sh"
|
|
237
256
|
];
|
|
238
257
|
var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
|
|
239
258
|
...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
|
|
@@ -1099,7 +1118,9 @@ var CODEX_HOOK_WIRING = [
|
|
|
1099
1118
|
codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 60, GUARD_RUNNER_REL, "hook post-tool-use --agent codex"),
|
|
1100
1119
|
codexHookRow("Stop", null, 60, GUARD_RUNNER_REL, "hook stop --agent codex"),
|
|
1101
1120
|
codexHookRow("SubagentStop", null, 60, GUARD_RUNNER_REL, "hook subagent-stop --agent codex"),
|
|
1102
|
-
codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL)
|
|
1121
|
+
codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL),
|
|
1122
|
+
codexHookRow("SessionStart", null, 10, CONTEXT_BUDGET_CHECK_REL),
|
|
1123
|
+
codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 5, CONTEXT_DRIFT_CHECK_REL)
|
|
1103
1124
|
];
|
|
1104
1125
|
var RETIRED_CODEX_HOOK_COMMANDS = [];
|
|
1105
1126
|
function codexHookCommandSet(retired) {
|
|
@@ -6431,6 +6452,8 @@ function kinds() {
|
|
|
6431
6452
|
[".claude/hooks/user-prompt-findings.sh", "check-script"],
|
|
6432
6453
|
[".claude/hooks/session-end-marker.sh", "check-script"],
|
|
6433
6454
|
[".claude/hooks/session-start-marker.sh", "check-script"],
|
|
6455
|
+
[".claude/hooks/context-budget-check.sh", "check-script"],
|
|
6456
|
+
[".claude/hooks/context-drift-check.sh", "check-script"],
|
|
6434
6457
|
[SETTINGS_REL, "settings"],
|
|
6435
6458
|
[MCP_REGISTRATION_REL, "server-entry"],
|
|
6436
6459
|
[GITIGNORE_REL, "ignore-list"],
|
|
@@ -8425,13 +8448,133 @@ var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
|
8425
8448
|
function planMcpHeadersHelperRemoval(targetRepo) {
|
|
8426
8449
|
return decideRetiredRemoval(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
|
|
8427
8450
|
}
|
|
8451
|
+
var GENERATED_CONTEXT_BUDGET_HEADER = "# Context budget check \u2014 generated by";
|
|
8452
|
+
function generateContextBudgetCheck() {
|
|
8453
|
+
return `#!/usr/bin/env bash
|
|
8454
|
+
${GENERATED_CONTEXT_BUDGET_HEADER} the Halfcycle installer.
|
|
8455
|
+
# Runs when a session starts. It warns, and never blocks, when the instructions
|
|
8456
|
+
# every session loads have grown past their budget, so they can be trimmed
|
|
8457
|
+
# before every later session pays for them:
|
|
8458
|
+
# - AGENTS.md at the project root, over 16384 bytes;
|
|
8459
|
+
# - this project's memory index in Claude Code, over 20480 bytes or 150 lines
|
|
8460
|
+
# (Claude Code cuts the index off at about 25 KB or 200 lines when it loads it).
|
|
8461
|
+
|
|
8462
|
+
cat >/dev/null 2>&1 || true
|
|
8463
|
+
|
|
8464
|
+
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd -P )" || exit 0
|
|
8465
|
+
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd -P )" || exit 0
|
|
8466
|
+
|
|
8467
|
+
NOTES=""
|
|
8468
|
+
|
|
8469
|
+
AGENTS_FILE="$PROJECT_ROOT/AGENTS.md"
|
|
8470
|
+
if [ -f "$AGENTS_FILE" ]; then
|
|
8471
|
+
BYTES=$(wc -c < "$AGENTS_FILE" | tr -d ' ')
|
|
8472
|
+
if [ "\${BYTES:-0}" -gt 16384 ]; then
|
|
8473
|
+
NOTES="\${NOTES}AGENTS.md is \${BYTES} bytes, over its budget of 16384. "
|
|
8474
|
+
fi
|
|
8475
|
+
fi
|
|
8476
|
+
|
|
8477
|
+
CONFIG_DIR="\${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
8478
|
+
SLUG=$(printf '%s' "\${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}" | sed 's/[^A-Za-z0-9]/-/g')
|
|
8479
|
+
INDEX="$CONFIG_DIR/projects/$SLUG/memory/MEMORY.md"
|
|
8480
|
+
if [ -f "$INDEX" ]; then
|
|
8481
|
+
BYTES=$(wc -c < "$INDEX" | tr -d ' ')
|
|
8482
|
+
LINES=$(wc -l < "$INDEX" | tr -d ' ')
|
|
8483
|
+
if [ "\${BYTES:-0}" -gt 20480 ] || [ "\${LINES:-0}" -gt 150 ]; then
|
|
8484
|
+
NOTES="\${NOTES}The memory index is \${BYTES} bytes and \${LINES} lines, over its budget of 20480 bytes and 150 lines. "
|
|
8485
|
+
fi
|
|
8486
|
+
fi
|
|
8487
|
+
|
|
8488
|
+
if [ -z "$NOTES" ]; then exit 0; fi
|
|
8489
|
+
|
|
8490
|
+
MESSAGE="Context budget: \${NOTES}Run the context-update command to fold and trim it."
|
|
8491
|
+
printf '{"systemMessage":"%s","hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$MESSAGE" "$MESSAGE"
|
|
8492
|
+
exit 0
|
|
8493
|
+
`;
|
|
8494
|
+
}
|
|
8495
|
+
var GENERATED_CONTEXT_DRIFT_HEADER = "# Context drift check \u2014 generated by";
|
|
8496
|
+
function generateContextDriftCheck() {
|
|
8497
|
+
return `#!/usr/bin/env bash
|
|
8498
|
+
${GENERATED_CONTEXT_DRIFT_HEADER} the Halfcycle installer.
|
|
8499
|
+
# Context drift detector \u2014 runs after each file edit. If a trigger file was modified,
|
|
8500
|
+
# it asks the session to run the context-update command before the session ends.
|
|
8501
|
+
|
|
8502
|
+
set -euo pipefail
|
|
8503
|
+
|
|
8504
|
+
INPUT=$(cat)
|
|
8505
|
+
|
|
8506
|
+
# First line: the event's name, for the reply. Then one line per edited file, made
|
|
8507
|
+
# absolute against the event's cwd: the edit's file_path, or every path a patch names.
|
|
8508
|
+
# With no python3 on the machine it says nothing, rather than fail after every edit.
|
|
8509
|
+
PARSED=$(HOOK_INPUT="$INPUT" python3 - <<'PYEOF'
|
|
8510
|
+
import json, os, re
|
|
8511
|
+
try:
|
|
8512
|
+
data = json.loads(os.environ["HOOK_INPUT"])
|
|
8513
|
+
except Exception:
|
|
8514
|
+
raise SystemExit(0)
|
|
8515
|
+
tool_input = data.get("tool_input") or {}
|
|
8516
|
+
cwd = data.get("cwd") or os.getcwd()
|
|
8517
|
+
if tool_input.get("file_path"):
|
|
8518
|
+
paths = [tool_input["file_path"]]
|
|
8519
|
+
else:
|
|
8520
|
+
headers = r"^\\*\\*\\* (?:Add File|Update File|Delete File|Move to): (.+)$"
|
|
8521
|
+
paths = re.findall(headers, str(tool_input.get("command", "")), re.M)
|
|
8522
|
+
print(data.get("hook_event_name", ""))
|
|
8523
|
+
for p in paths:
|
|
8524
|
+
print(os.path.realpath(os.path.join(cwd, p.strip())))
|
|
8525
|
+
PYEOF
|
|
8526
|
+
) || exit 0
|
|
8527
|
+
|
|
8528
|
+
if [ -z "$PARSED" ]; then exit 0; fi
|
|
8529
|
+
HOOK_EVENT=$(printf '%s\\n' "$PARSED" | head -n 1)
|
|
8530
|
+
|
|
8531
|
+
# Portable PROJECT_ROOT derivation: the script lives two folders below the root
|
|
8532
|
+
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd -P )"
|
|
8533
|
+
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd -P )"
|
|
8534
|
+
|
|
8535
|
+
REASONS=""
|
|
8536
|
+
while IFS= read -r FILE_PATH; do
|
|
8537
|
+
[ -n "$FILE_PATH" ] || continue
|
|
8538
|
+
case "$FILE_PATH" in
|
|
8539
|
+
"$PROJECT_ROOT"/*) ;;
|
|
8540
|
+
*) continue ;;
|
|
8541
|
+
esac
|
|
8542
|
+
|
|
8543
|
+
REL_PATH="\${FILE_PATH#$PROJECT_ROOT/}"
|
|
8544
|
+
|
|
8545
|
+
# Skip the instructions files and each agent's own folders (editing them needs no reminder)
|
|
8546
|
+
case "$REL_PATH" in
|
|
8547
|
+
CLAUDE.md|AGENTS.md|README.md|.claude/*|.codex/*|.agents/*|dist/*|node_modules/*|.git/*)
|
|
8548
|
+
continue ;;
|
|
8549
|
+
esac
|
|
8550
|
+
|
|
8551
|
+
REASON=""
|
|
8552
|
+
case "$REL_PATH" in
|
|
8553
|
+
package.json|package-lock.json) REASON="package.json changed \u2014 check Tech Stack / Commands sections" ;;
|
|
8554
|
+
wrangler.jsonc|wrangler.toml) REASON="wrangler config changed \u2014 check Deployment section" ;;
|
|
8555
|
+
astro.config.*|next.config.*|vite.config.*) REASON="build config changed \u2014 check Tech Stack / Architecture" ;;
|
|
8556
|
+
src/pages/api/*|src/routes/api/*|pages/api/*) REASON="API route changed \u2014 check API Endpoints" ;;
|
|
8557
|
+
scripts/*) REASON="scripts/ changed \u2014 check Commands section" ;;
|
|
8558
|
+
tsconfig.json|.eslintrc*|.prettierrc*) REASON="tool config changed \u2014 check Code Conventions" ;;
|
|
8559
|
+
esac
|
|
8560
|
+
if [ -n "$REASON" ]; then REASONS="\${REASONS}\${REL_PATH} was modified: \${REASON}. "; fi
|
|
8561
|
+
done < <(printf '%s\\n' "$PARSED" | tail -n +2)
|
|
8562
|
+
|
|
8563
|
+
if [ -z "$REASONS" ]; then exit 0; fi
|
|
8564
|
+
|
|
8565
|
+
REMINDER="Context drift detected. \${REASONS}Before ending this session, run the context-update command to audit and update AGENTS.md and the memory folder."
|
|
8566
|
+
python3 -c 'import json, sys; print(json.dumps({"hookSpecificOutput": {"hookEventName": sys.argv[1], "additionalContext": sys.argv[2]}}))' "$HOOK_EVENT" "$REMINDER"
|
|
8567
|
+
`;
|
|
8568
|
+
}
|
|
8428
8569
|
var OWNED_GENERATED_HEADERS = {
|
|
8429
8570
|
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
8430
8571
|
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
8431
8572
|
".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
|
|
8432
8573
|
".claude/hooks/user-prompt-findings.sh": GENERATED_USER_PROMPT_FINDINGS_HEADER,
|
|
8433
8574
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
8434
|
-
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
8575
|
+
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER,
|
|
8576
|
+
".claude/hooks/context-budget-check.sh": GENERATED_CONTEXT_BUDGET_HEADER,
|
|
8577
|
+
".claude/hooks/context-drift-check.sh": GENERATED_CONTEXT_DRIFT_HEADER
|
|
8435
8578
|
};
|
|
8436
8579
|
function hookScriptsThisVersionWrites() {
|
|
8437
8580
|
return [
|
|
@@ -8440,7 +8583,9 @@ function hookScriptsThisVersionWrites() {
|
|
|
8440
8583
|
[".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
8441
8584
|
[".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
|
|
8442
8585
|
[".claude/hooks/user-prompt-findings.sh", generateUserPromptFindingsHook()],
|
|
8443
|
-
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
|
|
8586
|
+
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()],
|
|
8587
|
+
[".claude/hooks/context-budget-check.sh", generateContextBudgetCheck()],
|
|
8588
|
+
[".claude/hooks/context-drift-check.sh", generateContextDriftCheck()]
|
|
8444
8589
|
];
|
|
8445
8590
|
}
|
|
8446
8591
|
function generateMcpRegistration(existing, mcpOrigin) {
|