cleargate 0.11.3 → 0.11.4

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/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ import { Command } from "commander";
21
21
  // package.json
22
22
  var package_default = {
23
23
  name: "cleargate",
24
- version: "0.11.3",
24
+ version: "0.11.4",
25
25
  private: false,
26
26
  type: "module",
27
27
  description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, five-role agent team (architect/developer/qa/devops/reporter), Karpathy-style awareness wiki.",
@@ -2430,6 +2430,61 @@ function resolveScaffoldRoot(opts) {
2430
2430
  };
2431
2431
  }
2432
2432
 
2433
+ // src/lib/session-load-delta.ts
2434
+ function canonicalize(value) {
2435
+ if (value === null || typeof value !== "object") {
2436
+ return JSON.stringify(value);
2437
+ }
2438
+ if (Array.isArray(value)) {
2439
+ return "[" + value.map(canonicalize).join(",") + "]";
2440
+ }
2441
+ const obj = value;
2442
+ const sortedKeys = Object.keys(obj).sort();
2443
+ const pairs = sortedKeys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
2444
+ return "{" + pairs.join(",") + "}";
2445
+ }
2446
+ var HOOK_EVENTS = ["PreToolUse", "PostToolUse", "SessionStart", "SubagentStop"];
2447
+ function extractSettingsHooksBlock(settings) {
2448
+ const hooks = settings["hooks"] ?? {};
2449
+ const extracted = {};
2450
+ for (const event of HOOK_EVENTS) {
2451
+ if (Object.prototype.hasOwnProperty.call(hooks, event)) {
2452
+ extracted[event] = hooks[event];
2453
+ }
2454
+ }
2455
+ return extracted;
2456
+ }
2457
+ function extractMcpCleargateEntry(mcp2) {
2458
+ const servers = mcp2["mcpServers"] ?? {};
2459
+ return servers["cleargate"] ?? null;
2460
+ }
2461
+ function extractSessionLoadDelta(filePath, oldContent, newContent) {
2462
+ const normalized = filePath.replace(/\\/g, "/");
2463
+ if (normalized === ".claude/settings.json") {
2464
+ try {
2465
+ const oldSettings = JSON.parse(oldContent);
2466
+ const newSettings = JSON.parse(newContent);
2467
+ const oldHooks = extractSettingsHooksBlock(oldSettings);
2468
+ const newHooks = extractSettingsHooksBlock(newSettings);
2469
+ return canonicalize(oldHooks) !== canonicalize(newHooks);
2470
+ } catch {
2471
+ return true;
2472
+ }
2473
+ }
2474
+ if (normalized === ".mcp.json") {
2475
+ try {
2476
+ const oldMcp = JSON.parse(oldContent);
2477
+ const newMcp = JSON.parse(newContent);
2478
+ const oldEntry = extractMcpCleargateEntry(oldMcp);
2479
+ const newEntry = extractMcpCleargateEntry(newMcp);
2480
+ return canonicalize(oldEntry) !== canonicalize(newEntry);
2481
+ } catch {
2482
+ return true;
2483
+ }
2484
+ }
2485
+ return true;
2486
+ }
2487
+
2433
2488
  // src/commands/init.ts
2434
2489
  var HOOK_ADDITION = {
2435
2490
  hooks: {
@@ -2605,10 +2660,17 @@ async function initHandler(opts = {}) {
2605
2660
  }
2606
2661
  }
2607
2662
  const mergedSettings = mergeSettings(existingSettings, HOOK_ADDITION);
2663
+ const mergedSettingsContent = JSON.stringify(mergedSettings, null, 2) + "\n";
2664
+ const existingSettingsContent = existingSettings !== null ? JSON.stringify(existingSettings, null, 2) + "\n" : "{}";
2608
2665
  fs15.mkdirSync(path15.dirname(settingsPath), { recursive: true });
2609
- writeAtomic(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n");
2610
- stdout(`[cleargate init] Updated .claude/settings.json: merged PostToolUse hook \u2014 restart Claude Code if already open.
2666
+ writeAtomic(settingsPath, mergedSettingsContent);
2667
+ if (extractSessionLoadDelta(".claude/settings.json", existingSettingsContent, mergedSettingsContent)) {
2668
+ stdout(`[cleargate init] Updated .claude/settings.json: merged PostToolUse hook \u2014 restart Claude Code if already open.
2669
+ `);
2670
+ } else {
2671
+ stdout(`[cleargate init] .claude/settings.json unchanged (hooks block already current)
2611
2672
  `);
2673
+ }
2612
2674
  const claudeMdPath = path15.join(cwd, "CLAUDE.md");
2613
2675
  const claudeMdSrcPath = path15.join(payloadDir, "CLAUDE.md");
2614
2676
  let claudeMdBlock;
@@ -7775,7 +7837,16 @@ function removeClearGateHooks(settings) {
7775
7837
  // src/lib/merge-ui.ts
7776
7838
  import { createPatch } from "diff";
7777
7839
  function renderInlineDiff(ours, theirs, filePath) {
7778
- return createPatch(filePath, ours, theirs, "installed", "upstream");
7840
+ const patch = createPatch(filePath, ours, theirs, "installed", "upstream");
7841
+ const hasHunkLines = patch.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-")).filter((l) => !l.startsWith("+++") && !l.startsWith("---")).length > 0;
7842
+ if (!hasHunkLines) {
7843
+ const ourBytes = Buffer.byteLength(ours, "utf-8");
7844
+ const theirBytes = Buffer.byteLength(theirs, "utf-8");
7845
+ const byteNote = ourBytes !== theirBytes ? `${Math.abs(theirBytes - ourBytes)} bytes changed` : "same byte count";
7846
+ return patch + `(whitespace/EOL-only differences \u2014 ${byteNote})
7847
+ `;
7848
+ }
7849
+ return patch;
7779
7850
  }
7780
7851
  async function promptMergeChoice(opts) {
7781
7852
  const { path: filePath, state: state2, ours, theirs } = opts;
@@ -8070,7 +8141,15 @@ async function upgradeHandler(flags, cli) {
8070
8141
  let count = 0;
8071
8142
  for (const item of workItems) {
8072
8143
  const state2 = classify(item.entry.sha256, item.installSha, item.currentSha, item.entry.tier);
8073
- stdout(`[dry-run] ${item.entry.path} action=${item.action} state=${state2}`);
8144
+ const projectedPostSha = item.entry.sha256;
8145
+ const projectedPostState = classify(
8146
+ item.entry.sha256,
8147
+ item.entry.sha256,
8148
+ projectedPostSha,
8149
+ item.entry.tier
8150
+ );
8151
+ const stateLabel = state2 !== projectedPostState ? `state=${state2} \u2192 ${projectedPostState}` : `state=${state2}`;
8152
+ stdout(`[dry-run] ${item.entry.path} action=${item.action} ${stateLabel}`);
8074
8153
  count++;
8075
8154
  }
8076
8155
  stdout(`[dry-run] ${count} files planned. No changes made.`);
@@ -8082,6 +8161,15 @@ async function upgradeHandler(flags, cli) {
8082
8161
  const sessionRestartFiles = [];
8083
8162
  for (const item of workItems) {
8084
8163
  const { entry, currentSha, installSha, action } = item;
8164
+ let preMutationContent = null;
8165
+ if (SESSION_LOAD_PATHS.has(entry.path)) {
8166
+ const targetPath = path36.join(cwd, entry.path);
8167
+ try {
8168
+ preMutationContent = await fsp.readFile(targetPath, "utf-8");
8169
+ } catch {
8170
+ preMutationContent = "";
8171
+ }
8172
+ }
8085
8173
  switch (action) {
8086
8174
  case "skip": {
8087
8175
  stdout(`[skip] ${entry.path} policy=${entry.overwrite_policy}`);
@@ -8112,8 +8200,17 @@ async function upgradeHandler(flags, cli) {
8112
8200
  current_sha: postSha,
8113
8201
  package_sha: entry.sha256
8114
8202
  };
8115
- if (SESSION_LOAD_PATHS.has(entry.path) && postSha !== currentSha) {
8116
- sessionRestartFiles.push(entry.path);
8203
+ if (SESSION_LOAD_PATHS.has(entry.path) && preMutationContent !== null) {
8204
+ const targetPath = path36.join(cwd, entry.path);
8205
+ let postMutationContent;
8206
+ try {
8207
+ postMutationContent = await fsp.readFile(targetPath, "utf-8");
8208
+ } catch {
8209
+ postMutationContent = "";
8210
+ }
8211
+ if (extractSessionLoadDelta(entry.path, preMutationContent, postMutationContent)) {
8212
+ sessionRestartFiles.push(entry.path);
8213
+ }
8117
8214
  }
8118
8215
  }
8119
8216
  await writeDriftState(cwd, driftMap, { lastRefreshed: now.toISOString() });