cleargate 0.11.1 → 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/CHANGELOG.md CHANGED
@@ -3,6 +3,35 @@
3
3
  All notable changes to this project are documented in this file.
4
4
  Format: [Common Changelog](https://common-changelog.org/) — most-recent version first.
5
5
 
6
+ ## [0.11.4] — 2026-05-05
7
+
8
+ SPRINT-26 Dogfood Hardening — Issues Surfaced by Live Use. Five items shipped (3 bugs + 2 CRs); the SDLC Hardening arc closed in SPRINT-25, this is the first sprint addressing dogfood-surfaced framework gaps from real-use testing on `markdown_file_renderer`.
9
+
10
+ ### Fixed
11
+ - **Token-ledger `work_item` fallback grep no longer misattributes to first lexical EPIC-NNN** (BUG-027) — `cleargate-planning/.claude/hooks/token-ledger.sh` resolver chain now tries (1) prior ledger row's `work_item_id` then (2) most-recent `dispatch-marker:` log line BEFORE falling back to transcript grep. Regression of [[BUG-024]] (closed in SPRINT-19); 12 misattribution instances observed during the SPRINT-02 dogfood test on `markdown_file_renderer` (~25% of session output tokens tagged to wrong epic). New authoritative snapshot `cleargate-cli/test/snapshots/hooks/token-ledger.bug-027.sh` supersedes cr-044's byte-equality assertion (cr-044 demoted to existence-only).
12
+ - **`cleargate upgrade --dry-run` and live run now report identical `state=` for every file** (BUG-028) — `cleargate-cli/src/commands/upgrade.ts` dry-run path now computes a hypothetical `postSha` by reading the upstream payload and emits a two-state line `state=<pre> → <post>` for upstream-changed files. `cleargate-cli/src/lib/merge-ui.ts` `renderInlineDiff()` appends a `(whitespace/EOL-only differences — N bytes changed)` annotation when `createPatch()` produces an empty body, so the user always sees a signal in the merge prompt. Discovered when the same `.claude/hooks/session-start.sh` file reported `state=clean` in dry-run and `state=upstream-changed` in the immediately-following real run.
13
+ - **Parallel-eligible Task dispatches no longer silently serialize** (BUG-029) — `cleargate-planning/.claude/hooks/write_dispatch.sh` and `cleargate-planning/.claude/hooks/pending-task-sentinel.sh` now uniquify marker filenames with `${ts}-${PID}-${RANDOM}` suffixes (was: single `.dispatch-${SESSION_ID}.json` and `.pending-task-${TURN_INDEX}.json` per orchestrator session, which collided when two Agent calls fired in one turn). `cleargate-planning/.claude/hooks/token-ledger.sh` SubagentStop handler now matches completion to dispatch by `(work_item_id, agent_type)` tuple from the transcript instead of the prior `ls -t | head -1` newest-file lookup. Discovered during the SPRINT-02 dogfood test where two parallel-eligible stories dispatched at the same timestamp but only one produced a ledger row; the second was silently dropped and re-dispatched 18 minutes later. New authoritative snapshot `cleargate-cli/test/snapshots/hooks/token-ledger.bug-029.sh`; `bug-027.sh` rolls forward to the new canonical state and is demoted to existence-only.
14
+
15
+ ### Added
16
+ - **Smarter session-load restart warning — suppresses no-op rewrites** (CR-059) — new helper `cleargate-cli/src/lib/session-load-delta.ts` exporting `extractSessionLoadDelta(filePath, oldContent, newContent)` returns `true` only when schema-meaningful keys actually changed: `hooks.{PreToolUse,PostToolUse,SessionStart,SubagentStop}.*` for `.claude/settings.json`, `mcpServers.cleargate` for `.mcp.json`. Both `upgrade.ts` and `init.ts` now consult the helper before emitting the v0.11.2 "Restart Claude Code" warning, so cosmetic re-formats (key order, whitespace, trailing newline) don't trigger warning fatigue. Conservative on parse failure: when in doubt, warn.
17
+
18
+ ### Changed
19
+ - **`CLAUDE.md` "Dogfood split" section clarified** (CR-060) — explicit one-paragraph note that target repos do **not** receive a `cleargate-planning/` directory; only the *contents* of `.claude/` and `.cleargate/` are copied. Top-level `CLAUDE.md` is bounded-block-injected; top-level `MANIFEST.json` is skipped per `cleargate-cli/src/init/copy-payload.ts:54` SKIP_FILES; install snapshot lands at `.cleargate/.install-manifest.json`. Pure doc, no behavior change.
20
+
21
+ ## [0.11.3] — 2026-05-05
22
+
23
+ Hotfix.
24
+
25
+ ### Fixed
26
+ - **`cleargate upgrade` no longer strips the executable bit from `.sh` hook scripts** — `cleargate-cli/src/commands/upgrade.ts` `writeAtomic()` now `chmod 0o755` after writing any `.sh` target. `fs.writeFile` defaults to 0o644; init solved this in `copy-payload.ts` (BUG-018) but upgrade's write path bypassed that fix. Result: every Claude Code hook in target repos failed with `Permission denied` after upgrade until manually re-chmod'd. Observed today on `markdown_file_renderer` after the SPRINT-02 PostToolUse:Edit hook fired.
27
+
28
+ ## [0.11.2] — 2026-05-05
29
+
30
+ Hotfix.
31
+
32
+ ### Fixed
33
+ - **`cleargate upgrade` and `cleargate init` now warn when session-loaded configs change** — `cleargate-cli/src/commands/upgrade.ts` tracks modifications to `.claude/settings.json` and `.mcp.json` during the run and prints a `⚠ Restart Claude Code in this repo` block at the end if either changed. `init.ts` adds the same "restart Claude Code if already open" suffix to its `Updated .claude/settings.json` log line (parallels the existing `.mcp.json` message). Without this, hook wiring + MCP-server changes silently fail to load until the user happens to restart their session — observed today when SPRINT-02 drafts in `markdown_file_renderer` did not trigger `stamp-and-gate.sh` after upgrade because the running session held a pre-upgrade settings snapshot.
34
+
6
35
  ## [0.11.1] — 2026-05-05
7
36
 
8
37
  Hotfix.
@@ -1,6 +1,6 @@
1
1
  {
2
- "cleargate_version": "0.11.1",
3
- "generated_at": "2026-05-04T22:23:50.738Z",
2
+ "cleargate_version": "0.11.4",
3
+ "generated_at": "2026-05-05T17:48:13.929Z",
4
4
  "files": [
5
5
  {
6
6
  "path": ".claude/agents/architect.md",
@@ -67,7 +67,7 @@
67
67
  },
68
68
  {
69
69
  "path": ".claude/hooks/pending-task-sentinel.sh",
70
- "sha256": "8204286b19287c490cc09014b0c87e2b2686e6bd120393b7165895d215d6b49a",
70
+ "sha256": "a3c2dc71a803c37527afd059b81e2adad2104d4887ae125846a2416f2f719b71",
71
71
  "tier": "hook",
72
72
  "overwrite_policy": "always",
73
73
  "preserve_on_uninstall": false
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "path": ".claude/hooks/token-ledger.sh",
126
- "sha256": "6678f814520c379b3ab055f3a1b98c92f631fd415ae24752d45aa8bee058c29c",
126
+ "sha256": "37a5bc311ca36f016ed72f1ded92d70d2b0acdd8993d634667b05d3407de0f22",
127
127
  "tier": "hook",
128
128
  "overwrite_policy": "always",
129
129
  "preserve_on_uninstall": false
@@ -389,7 +389,7 @@
389
389
  },
390
390
  {
391
391
  "path": ".cleargate/scripts/write_dispatch.sh",
392
- "sha256": "abdcaf09b09251f3ab42cb7ec8bdedc5806fd0eb337578f55043bffb158d8128",
392
+ "sha256": "2d4ebbd8a6f0e833c86b534c3106377018d54f6e24b3ff1a171b18d807103748",
393
393
  "tier": "script",
394
394
  "overwrite_policy": "always",
395
395
  "preserve_on_uninstall": false
package/dist/cli.cjs CHANGED
@@ -696,7 +696,7 @@ var import_commander = require("commander");
696
696
  // package.json
697
697
  var package_default = {
698
698
  name: "cleargate",
699
- version: "0.11.1",
699
+ version: "0.11.4",
700
700
  private: false,
701
701
  type: "module",
702
702
  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.",
@@ -3176,6 +3176,62 @@ function resolveScaffoldRoot(opts) {
3176
3176
  };
3177
3177
  }
3178
3178
 
3179
+ // src/lib/session-load-delta.ts
3180
+ init_cjs_shims();
3181
+ function canonicalize(value) {
3182
+ if (value === null || typeof value !== "object") {
3183
+ return JSON.stringify(value);
3184
+ }
3185
+ if (Array.isArray(value)) {
3186
+ return "[" + value.map(canonicalize).join(",") + "]";
3187
+ }
3188
+ const obj = value;
3189
+ const sortedKeys = Object.keys(obj).sort();
3190
+ const pairs = sortedKeys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
3191
+ return "{" + pairs.join(",") + "}";
3192
+ }
3193
+ var HOOK_EVENTS = ["PreToolUse", "PostToolUse", "SessionStart", "SubagentStop"];
3194
+ function extractSettingsHooksBlock(settings) {
3195
+ const hooks = settings["hooks"] ?? {};
3196
+ const extracted = {};
3197
+ for (const event of HOOK_EVENTS) {
3198
+ if (Object.prototype.hasOwnProperty.call(hooks, event)) {
3199
+ extracted[event] = hooks[event];
3200
+ }
3201
+ }
3202
+ return extracted;
3203
+ }
3204
+ function extractMcpCleargateEntry(mcp2) {
3205
+ const servers = mcp2["mcpServers"] ?? {};
3206
+ return servers["cleargate"] ?? null;
3207
+ }
3208
+ function extractSessionLoadDelta(filePath, oldContent, newContent) {
3209
+ const normalized = filePath.replace(/\\/g, "/");
3210
+ if (normalized === ".claude/settings.json") {
3211
+ try {
3212
+ const oldSettings = JSON.parse(oldContent);
3213
+ const newSettings = JSON.parse(newContent);
3214
+ const oldHooks = extractSettingsHooksBlock(oldSettings);
3215
+ const newHooks = extractSettingsHooksBlock(newSettings);
3216
+ return canonicalize(oldHooks) !== canonicalize(newHooks);
3217
+ } catch {
3218
+ return true;
3219
+ }
3220
+ }
3221
+ if (normalized === ".mcp.json") {
3222
+ try {
3223
+ const oldMcp = JSON.parse(oldContent);
3224
+ const newMcp = JSON.parse(newContent);
3225
+ const oldEntry = extractMcpCleargateEntry(oldMcp);
3226
+ const newEntry = extractMcpCleargateEntry(newMcp);
3227
+ return canonicalize(oldEntry) !== canonicalize(newEntry);
3228
+ } catch {
3229
+ return true;
3230
+ }
3231
+ }
3232
+ return true;
3233
+ }
3234
+
3179
3235
  // src/commands/init.ts
3180
3236
  var HOOK_ADDITION = {
3181
3237
  hooks: {
@@ -3351,10 +3407,17 @@ async function initHandler(opts = {}) {
3351
3407
  }
3352
3408
  }
3353
3409
  const mergedSettings = mergeSettings(existingSettings, HOOK_ADDITION);
3410
+ const mergedSettingsContent = JSON.stringify(mergedSettings, null, 2) + "\n";
3411
+ const existingSettingsContent = existingSettings !== null ? JSON.stringify(existingSettings, null, 2) + "\n" : "{}";
3354
3412
  fs17.mkdirSync(path18.dirname(settingsPath), { recursive: true });
3355
- writeAtomic(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n");
3356
- stdout(`[cleargate init] Updated .claude/settings.json: merged PostToolUse hook
3413
+ writeAtomic(settingsPath, mergedSettingsContent);
3414
+ if (extractSessionLoadDelta(".claude/settings.json", existingSettingsContent, mergedSettingsContent)) {
3415
+ stdout(`[cleargate init] Updated .claude/settings.json: merged PostToolUse hook \u2014 restart Claude Code if already open.
3357
3416
  `);
3417
+ } else {
3418
+ stdout(`[cleargate init] .claude/settings.json unchanged (hooks block already current)
3419
+ `);
3420
+ }
3358
3421
  const claudeMdPath = path18.join(cwd, "CLAUDE.md");
3359
3422
  const claudeMdSrcPath = path18.join(payloadDir, "CLAUDE.md");
3360
3423
  let claudeMdBlock;
@@ -8863,7 +8926,16 @@ function removeClearGateHooks(settings) {
8863
8926
  init_cjs_shims();
8864
8927
  var import_diff = require("diff");
8865
8928
  function renderInlineDiff(ours, theirs, filePath) {
8866
- return (0, import_diff.createPatch)(filePath, ours, theirs, "installed", "upstream");
8929
+ const patch = (0, import_diff.createPatch)(filePath, ours, theirs, "installed", "upstream");
8930
+ const hasHunkLines = patch.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-")).filter((l) => !l.startsWith("+++") && !l.startsWith("---")).length > 0;
8931
+ if (!hasHunkLines) {
8932
+ const ourBytes = Buffer.byteLength(ours, "utf-8");
8933
+ const theirBytes = Buffer.byteLength(theirs, "utf-8");
8934
+ const byteNote = ourBytes !== theirBytes ? `${Math.abs(theirBytes - ourBytes)} bytes changed` : "same byte count";
8935
+ return patch + `(whitespace/EOL-only differences \u2014 ${byteNote})
8936
+ `;
8937
+ }
8938
+ return patch;
8867
8939
  }
8868
8940
  async function promptMergeChoice(opts) {
8869
8941
  const { path: filePath, state: state2, ours, theirs } = opts;
@@ -8934,6 +9006,9 @@ async function writeAtomic2(filePath, content) {
8934
9006
  const tmpPath = filePath + ".tmp." + Date.now();
8935
9007
  await fsp.writeFile(tmpPath, content, "utf-8");
8936
9008
  await fsp.rename(tmpPath, filePath);
9009
+ if (filePath.endsWith(".sh")) {
9010
+ await fsp.chmod(filePath, 493);
9011
+ }
8937
9012
  }
8938
9013
  async function updateSnapshotEntry(projectRoot, filePath, newSha) {
8939
9014
  const snapshotPath = path41.join(projectRoot, ".cleargate", ".install-manifest.json");
@@ -9156,7 +9231,15 @@ async function upgradeHandler(flags, cli) {
9156
9231
  let count = 0;
9157
9232
  for (const item of workItems) {
9158
9233
  const state2 = classify(item.entry.sha256, item.installSha, item.currentSha, item.entry.tier);
9159
- stdout(`[dry-run] ${item.entry.path} action=${item.action} state=${state2}`);
9234
+ const projectedPostSha = item.entry.sha256;
9235
+ const projectedPostState = classify(
9236
+ item.entry.sha256,
9237
+ item.entry.sha256,
9238
+ projectedPostSha,
9239
+ item.entry.tier
9240
+ );
9241
+ const stateLabel = state2 !== projectedPostState ? `state=${state2} \u2192 ${projectedPostState}` : `state=${state2}`;
9242
+ stdout(`[dry-run] ${item.entry.path} action=${item.action} ${stateLabel}`);
9160
9243
  count++;
9161
9244
  }
9162
9245
  stdout(`[dry-run] ${count} files planned. No changes made.`);
@@ -9164,8 +9247,19 @@ async function upgradeHandler(flags, cli) {
9164
9247
  }
9165
9248
  const packageRoot = cli?.packageRoot ?? cwd;
9166
9249
  const driftMap = {};
9250
+ const SESSION_LOAD_PATHS = /* @__PURE__ */ new Set([".claude/settings.json", ".mcp.json"]);
9251
+ const sessionRestartFiles = [];
9167
9252
  for (const item of workItems) {
9168
9253
  const { entry, currentSha, installSha, action } = item;
9254
+ let preMutationContent = null;
9255
+ if (SESSION_LOAD_PATHS.has(entry.path)) {
9256
+ const targetPath = path41.join(cwd, entry.path);
9257
+ try {
9258
+ preMutationContent = await fsp.readFile(targetPath, "utf-8");
9259
+ } catch {
9260
+ preMutationContent = "";
9261
+ }
9262
+ }
9169
9263
  switch (action) {
9170
9264
  case "skip": {
9171
9265
  stdout(`[skip] ${entry.path} policy=${entry.overwrite_policy}`);
@@ -9196,9 +9290,28 @@ async function upgradeHandler(flags, cli) {
9196
9290
  current_sha: postSha,
9197
9291
  package_sha: entry.sha256
9198
9292
  };
9293
+ if (SESSION_LOAD_PATHS.has(entry.path) && preMutationContent !== null) {
9294
+ const targetPath = path41.join(cwd, entry.path);
9295
+ let postMutationContent;
9296
+ try {
9297
+ postMutationContent = await fsp.readFile(targetPath, "utf-8");
9298
+ } catch {
9299
+ postMutationContent = "";
9300
+ }
9301
+ if (extractSessionLoadDelta(entry.path, preMutationContent, postMutationContent)) {
9302
+ sessionRestartFiles.push(entry.path);
9303
+ }
9304
+ }
9199
9305
  }
9200
9306
  await writeDriftState(cwd, driftMap, { lastRefreshed: now.toISOString() });
9201
9307
  stdout("[upgrade] complete.");
9308
+ if (sessionRestartFiles.length > 0) {
9309
+ stdout("");
9310
+ stdout(`\u26A0 Restart Claude Code in this repo to load the new ${sessionRestartFiles.length === 1 ? "config" : "configs"}:`);
9311
+ for (const f of sessionRestartFiles) {
9312
+ stdout(` ${f} (loaded once at session start)`);
9313
+ }
9314
+ }
9202
9315
  }
9203
9316
 
9204
9317
  // src/commands/uninstall.ts