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/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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
+
6
21
  ## [0.11.3] — 2026-05-05
7
22
 
8
23
  Hotfix.
@@ -1,6 +1,6 @@
1
1
  {
2
- "cleargate_version": "0.11.3",
3
- "generated_at": "2026-05-04T23:10:17.763Z",
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.3",
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 \u2014 restart Claude Code if already open.
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.
3416
+ `);
3417
+ } else {
3418
+ stdout(`[cleargate init] .claude/settings.json unchanged (hooks block already current)
3357
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;
@@ -9159,7 +9231,15 @@ async function upgradeHandler(flags, cli) {
9159
9231
  let count = 0;
9160
9232
  for (const item of workItems) {
9161
9233
  const state2 = classify(item.entry.sha256, item.installSha, item.currentSha, item.entry.tier);
9162
- 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}`);
9163
9243
  count++;
9164
9244
  }
9165
9245
  stdout(`[dry-run] ${count} files planned. No changes made.`);
@@ -9171,6 +9251,15 @@ async function upgradeHandler(flags, cli) {
9171
9251
  const sessionRestartFiles = [];
9172
9252
  for (const item of workItems) {
9173
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
+ }
9174
9263
  switch (action) {
9175
9264
  case "skip": {
9176
9265
  stdout(`[skip] ${entry.path} policy=${entry.overwrite_policy}`);
@@ -9201,8 +9290,17 @@ async function upgradeHandler(flags, cli) {
9201
9290
  current_sha: postSha,
9202
9291
  package_sha: entry.sha256
9203
9292
  };
9204
- if (SESSION_LOAD_PATHS.has(entry.path) && postSha !== currentSha) {
9205
- sessionRestartFiles.push(entry.path);
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
+ }
9206
9304
  }
9207
9305
  }
9208
9306
  await writeDriftState(cwd, driftMap, { lastRefreshed: now.toISOString() });