cleargate 0.5.0 → 0.6.0

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.
Files changed (54) hide show
  1. package/dist/MANIFEST.json +30 -16
  2. package/dist/cli.cjs +486 -51
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.js +481 -47
  5. package/dist/cli.js.map +1 -1
  6. package/dist/templates/cleargate-planning/.claude/agents/architect.md +24 -0
  7. package/dist/templates/cleargate-planning/.claude/agents/developer.md +24 -0
  8. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +74 -0
  9. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +162 -0
  10. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +10 -7
  11. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +9 -8
  12. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +36 -13
  13. package/dist/templates/cleargate-planning/.claude/settings.json +9 -0
  14. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +55 -0
  15. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +7 -7
  16. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +137 -40
  17. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +93 -0
  18. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +8 -4
  19. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +9 -1
  20. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +74 -0
  21. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +65 -1
  22. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +31 -8
  23. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +93 -8
  24. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +19 -4
  25. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +58 -0
  26. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +32 -2
  27. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +3 -1
  28. package/dist/templates/cleargate-planning/CLAUDE.md +1 -1
  29. package/dist/templates/cleargate-planning/MANIFEST.json +30 -16
  30. package/package.json +1 -1
  31. package/templates/cleargate-planning/.claude/agents/architect.md +24 -0
  32. package/templates/cleargate-planning/.claude/agents/developer.md +24 -0
  33. package/templates/cleargate-planning/.claude/agents/reporter.md +74 -0
  34. package/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +162 -0
  35. package/templates/cleargate-planning/.claude/hooks/session-start.sh +10 -7
  36. package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +9 -8
  37. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +36 -13
  38. package/templates/cleargate-planning/.claude/settings.json +9 -0
  39. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +55 -0
  40. package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +7 -7
  41. package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +137 -40
  42. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +93 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/constants.mjs +8 -4
  44. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +9 -1
  45. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +74 -0
  46. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +65 -1
  47. package/templates/cleargate-planning/.cleargate/scripts/state.schema.json +31 -8
  48. package/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +93 -8
  49. package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +19 -4
  50. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +58 -0
  51. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +32 -2
  52. package/templates/cleargate-planning/.cleargate/templates/story.md +3 -1
  53. package/templates/cleargate-planning/CLAUDE.md +1 -1
  54. package/templates/cleargate-planning/MANIFEST.json +30 -16
package/dist/cli.cjs CHANGED
@@ -627,7 +627,7 @@ var import_commander = require("commander");
627
627
  // package.json
628
628
  var package_default = {
629
629
  name: "cleargate",
630
- version: "0.5.0",
630
+ version: "0.6.0",
631
631
  private: false,
632
632
  type: "module",
633
633
  description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, four-agent loop (architect/developer/qa/reporter), Karpathy-style awareness wiki.",
@@ -1900,11 +1900,17 @@ init_cjs_shims();
1900
1900
  var fs15 = __toESM(require("fs"), 1);
1901
1901
  var path16 = __toESM(require("path"), 1);
1902
1902
  var import_node_url5 = require("url");
1903
+ var import_node_child_process3 = require("child_process");
1903
1904
 
1904
1905
  // src/init/copy-payload.ts
1905
1906
  init_cjs_shims();
1906
1907
  var fs7 = __toESM(require("fs"), 1);
1907
1908
  var path7 = __toESM(require("path"), 1);
1909
+ var PIN_PLACEHOLDER = "__CLEARGATE_VERSION__";
1910
+ var HOOK_FILES_WITH_PIN = /* @__PURE__ */ new Set([
1911
+ ".claude/hooks/stamp-and-gate.sh",
1912
+ ".claude/hooks/session-start.sh"
1913
+ ]);
1908
1914
  function listFilesRecursive(dir) {
1909
1915
  const results = [];
1910
1916
  function walk(current, rel) {
@@ -1932,10 +1938,15 @@ function copyPayload(payloadDir, targetCwd, opts) {
1932
1938
  const srcPath = path7.join(payloadDir, relPath);
1933
1939
  const dstPath = path7.join(targetCwd, relPath);
1934
1940
  fs7.mkdirSync(path7.dirname(dstPath), { recursive: true });
1935
- const srcContent = fs7.readFileSync(srcPath);
1941
+ let srcContent = fs7.readFileSync(srcPath);
1942
+ if (opts.pinVersion && HOOK_FILES_WITH_PIN.has(relPath)) {
1943
+ const text = srcContent.toString("utf8").replaceAll(PIN_PLACEHOLDER, opts.pinVersion);
1944
+ srcContent = text;
1945
+ }
1946
+ const srcBuffer = typeof srcContent === "string" ? Buffer.from(srcContent, "utf8") : srcContent;
1936
1947
  if (fs7.existsSync(dstPath)) {
1937
1948
  const dstContent = fs7.readFileSync(dstPath);
1938
- if (srcContent.equals(dstContent)) {
1949
+ if (srcBuffer.equals(dstContent)) {
1939
1950
  report.skipped++;
1940
1951
  report.actions.push({ action: "skipped", relPath });
1941
1952
  continue;
@@ -1945,11 +1956,11 @@ function copyPayload(payloadDir, targetCwd, opts) {
1945
1956
  report.actions.push({ action: "skipped", relPath });
1946
1957
  continue;
1947
1958
  }
1948
- fs7.writeFileSync(dstPath, srcContent);
1959
+ fs7.writeFileSync(dstPath, srcBuffer);
1949
1960
  report.overwritten++;
1950
1961
  report.actions.push({ action: "overwritten", relPath });
1951
1962
  } else {
1952
- fs7.writeFileSync(dstPath, srcContent);
1963
+ fs7.writeFileSync(dstPath, srcBuffer);
1953
1964
  report.created++;
1954
1965
  report.actions.push({ action: "created", relPath });
1955
1966
  }
@@ -2798,7 +2809,7 @@ init_cjs_shims();
2798
2809
  var readline3 = __toESM(require("readline"), 1);
2799
2810
  async function promptYesNo(question, defaultYes, opts) {
2800
2811
  const stdoutFn = opts?.stdout ?? ((s) => process.stdout.write(s));
2801
- stdoutFn(question + "\n");
2812
+ stdoutFn(question + " ");
2802
2813
  const inputStream = opts?.stdin ?? process.stdin;
2803
2814
  return new Promise((resolve14) => {
2804
2815
  const rl = readline3.createInterface({
@@ -2829,7 +2840,7 @@ async function promptYesNo(question, defaultYes, opts) {
2829
2840
  }
2830
2841
  async function promptEmail(question, defaultValue, opts) {
2831
2842
  const stdoutFn = opts?.stdout ?? ((s) => process.stdout.write(s));
2832
- stdoutFn(question + "\n");
2843
+ stdoutFn(question + " ");
2833
2844
  const inputStream = opts?.stdin ?? process.stdin;
2834
2845
  return new Promise((resolve14) => {
2835
2846
  const rl = readline3.createInterface({
@@ -2922,6 +2933,17 @@ function resolveIdentity(projectRoot, opts = {}) {
2922
2933
  // src/commands/init.ts
2923
2934
  var HOOK_ADDITION = {
2924
2935
  hooks: {
2936
+ PreToolUse: [
2937
+ {
2938
+ matcher: "Edit|Write",
2939
+ hooks: [
2940
+ {
2941
+ type: "command",
2942
+ command: "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-edit-gate.sh"
2943
+ }
2944
+ ]
2945
+ }
2946
+ ],
2925
2947
  PostToolUse: [
2926
2948
  {
2927
2949
  matcher: "Edit|Write",
@@ -2958,6 +2980,17 @@ function writeAtomic(filePath, content) {
2958
2980
  fs15.writeFileSync(tmpPath, content, "utf8");
2959
2981
  fs15.renameSync(tmpPath, filePath);
2960
2982
  }
2983
+ function readPackageVersion(packageJsonPath) {
2984
+ try {
2985
+ const raw = fs15.readFileSync(packageJsonPath, "utf8");
2986
+ const pkg = JSON.parse(raw);
2987
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
2988
+ return pkg.version;
2989
+ }
2990
+ } catch {
2991
+ }
2992
+ return null;
2993
+ }
2961
2994
  async function initHandler(opts = {}) {
2962
2995
  const cwd = opts.cwd ?? process.cwd();
2963
2996
  const force = opts.force ?? false;
@@ -2968,6 +3001,7 @@ async function initHandler(opts = {}) {
2968
3001
  const runWikiBuild = opts.runWikiBuild ?? wikiBuildHandler;
2969
3002
  const promptYesNoFn = opts.promptYesNo ?? promptYesNo;
2970
3003
  const promptEmailFn = opts.promptEmail ?? promptEmail;
3004
+ const spawnSyncFn = opts.spawnSyncFn ?? import_node_child_process3.spawnSync;
2971
3005
  if (!fs15.existsSync(cwd)) {
2972
3006
  stderr(`[cleargate init] ERROR: target directory does not exist: ${cwd}
2973
3007
  `);
@@ -3029,7 +3063,14 @@ async function initHandler(opts = {}) {
3029
3063
  }
3030
3064
  }
3031
3065
  }
3032
- const copyReport = copyPayload(payloadDir, cwd, { force });
3066
+ let pinVersion;
3067
+ if (opts.pin) {
3068
+ pinVersion = opts.pin;
3069
+ } else {
3070
+ const payloadParent = path16.resolve(payloadDir, "..", "..");
3071
+ pinVersion = readPackageVersion(path16.join(payloadParent, "package.json")) ?? readPackageVersion(path16.join(path16.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl)), "..", "package.json")) ?? "latest";
3072
+ }
3073
+ const copyReport = copyPayload(payloadDir, cwd, { force, pinVersion });
3033
3074
  for (const action of copyReport.actions) {
3034
3075
  const verb = action.action === "created" ? "Created" : action.action === "overwritten" ? "Overwritten" : "Skipped (exists)";
3035
3076
  stdout(`[cleargate init] ${verb} ${action.relPath}
@@ -3110,6 +3151,47 @@ async function initHandler(opts = {}) {
3110
3151
  `);
3111
3152
  }
3112
3153
  }
3154
+ {
3155
+ const distCliPath = path16.join(cwd, "cleargate-cli", "dist", "cli.js");
3156
+ let branch = null;
3157
+ let branchLabel = "";
3158
+ if (fs15.existsSync(distCliPath)) {
3159
+ branch = { cmd: "node", args: [distCliPath, "--version"] };
3160
+ branchLabel = `local dist (${distCliPath})`;
3161
+ } else {
3162
+ const whichResult = spawnSyncFn("command", ["-v", "cleargate"], {
3163
+ shell: true,
3164
+ encoding: "utf8",
3165
+ timeout: 3e3
3166
+ });
3167
+ if (whichResult.status === 0) {
3168
+ branch = { cmd: "cleargate", args: ["--version"] };
3169
+ branchLabel = "PATH (global install)";
3170
+ } else {
3171
+ branch = { cmd: "npx", args: ["-y", `cleargate@${pinVersion}`, "--version"] };
3172
+ branchLabel = `npx cleargate@${pinVersion} (cold-start ~600ms first call)`;
3173
+ }
3174
+ }
3175
+ if (branch !== null) {
3176
+ const probeResult = spawnSyncFn(branch.cmd, branch.args, {
3177
+ encoding: "utf8",
3178
+ timeout: 15e3
3179
+ });
3180
+ if (probeResult.status === 0) {
3181
+ stdout(`[cleargate init] \u{1F7E2} cleargate CLI resolved via ${branchLabel}
3182
+ `);
3183
+ } else {
3184
+ stdout(
3185
+ `[cleargate init] \u{1F534} cleargate CLI: not resolvable \u2014 hooks will no-op.
3186
+ [cleargate init] Attempted: ${branchLabel}
3187
+ [cleargate init] Fix: npm i -g cleargate@${pinVersion} or npx cleargate@${pinVersion} doctor
3188
+ `
3189
+ );
3190
+ exit(1);
3191
+ return;
3192
+ }
3193
+ }
3194
+ }
3113
3195
  const existingParticipant = readParticipant(cwd);
3114
3196
  if (existingParticipant === null) {
3115
3197
  const identityOpts = opts.identityOpts ?? {};
@@ -3127,8 +3209,10 @@ async function initHandler(opts = {}) {
3127
3209
  stdout(`[cleargate init] Participant identity: ${finalEmail} (inferred)
3128
3210
  `);
3129
3211
  } else {
3130
- const defaultEmail = gitEmail ?? "user@localhost";
3131
- const question = `[cleargate init] Participant email [${defaultEmail}]:`;
3212
+ const isNoreply = gitEmail !== null && /@users\.noreply\.github\.com$/i.test(gitEmail);
3213
+ const defaultEmail = gitEmail !== null && !isNoreply ? gitEmail : "user@localhost";
3214
+ stdout("\n");
3215
+ const question = `Participant email (press Enter for default) [${defaultEmail}]:`;
3132
3216
  const answer = await promptEmailFn(question, defaultEmail);
3133
3217
  await writeParticipant(cwd, answer, "prompted", now);
3134
3218
  stdout(`[cleargate init] Participant identity: ${answer} (prompted)
@@ -3146,7 +3230,7 @@ async function initHandler(opts = {}) {
3146
3230
  init_cjs_shims();
3147
3231
  var fs16 = __toESM(require("fs"), 1);
3148
3232
  var path17 = __toESM(require("path"), 1);
3149
- var import_node_child_process3 = require("child_process");
3233
+ var import_node_child_process4 = require("child_process");
3150
3234
  var EXCLUDED_SUFFIXES2 = [
3151
3235
  ".cleargate/knowledge/",
3152
3236
  ".cleargate/templates/",
@@ -3298,7 +3382,7 @@ function checkContentUnchanged(absRawPath, sha, relRawPath, gitRunner) {
3298
3382
  }
3299
3383
  }
3300
3384
  function defaultGitRunner(cmd, args) {
3301
- const result = (0, import_node_child_process3.spawnSync)(cmd, args, { encoding: "utf8" });
3385
+ const result = (0, import_node_child_process4.spawnSync)(cmd, args, { encoding: "utf8" });
3302
3386
  if (result.status !== 0) return "\0__NONZERO__";
3303
3387
  return result.stdout ?? "";
3304
3388
  }
@@ -3550,7 +3634,7 @@ function loadWikiPages(wikiRoot) {
3550
3634
  init_cjs_shims();
3551
3635
  var fs18 = __toESM(require("fs"), 1);
3552
3636
  var path19 = __toESM(require("path"), 1);
3553
- var import_node_child_process4 = require("child_process");
3637
+ var import_node_child_process5 = require("child_process");
3554
3638
  var import_js_yaml3 = __toESM(require("js-yaml"), 1);
3555
3639
 
3556
3640
  // src/lib/work-item-type.ts
@@ -3646,7 +3730,7 @@ function checkStaleCommit(page, repoRoot, gitRunner) {
3646
3730
  if (gitRunner) {
3647
3731
  currentSha = gitRunner("git", ["log", "-1", "--format=%H", "--", rawPath]).trim();
3648
3732
  } else {
3649
- const result = (0, import_node_child_process4.spawnSync)("git", ["log", "-1", "--format=%H", "--", rawPath], {
3733
+ const result = (0, import_node_child_process5.spawnSync)("git", ["log", "-1", "--format=%H", "--", rawPath], {
3650
3734
  encoding: "utf8",
3651
3735
  cwd: repoRoot
3652
3736
  });
@@ -4391,6 +4475,7 @@ function applyStatusFix(rawText, newStatus) {
4391
4475
  init_cjs_shims();
4392
4476
  var fs22 = __toESM(require("fs"), 1);
4393
4477
  var path24 = __toESM(require("path"), 1);
4478
+ var import_node_child_process6 = require("child_process");
4394
4479
 
4395
4480
  // src/lib/pricing.ts
4396
4481
  init_cjs_shims();
@@ -4440,6 +4525,7 @@ function selectMode(flags) {
4440
4525
  if (flags.checkScaffold) modes.push("check-scaffold");
4441
4526
  if (flags.sessionStart) modes.push("session-start");
4442
4527
  if (flags.pricing) modes.push("pricing");
4528
+ if (flags.canEdit) modes.push("can-edit");
4443
4529
  if (modes.length > 1) {
4444
4530
  throw new Error(
4445
4531
  `cleargate doctor: mutually exclusive flags set: ${modes.join(", ")}. Use only one mode flag at a time.`
@@ -4464,7 +4550,18 @@ function parseHookLogLine(line) {
4464
4550
  file: m[5].trim()
4465
4551
  };
4466
4552
  }
4467
- function runHookHealth(stdout, cwd, now) {
4553
+ function runHookHealth(stdout, cwd, now, outcome) {
4554
+ const cleargateDir = path24.join(cwd, ".cleargate");
4555
+ if (!fs22.existsSync(cleargateDir)) {
4556
+ stdout("cleargate misconfigured: no .cleargate/ found. Run: cleargate init");
4557
+ if (outcome) outcome.configError = true;
4558
+ return;
4559
+ }
4560
+ const manifestPath = path24.join(cwd, "cleargate-planning", "MANIFEST.json");
4561
+ if (!fs22.existsSync(manifestPath)) {
4562
+ stdout(`cleargate misconfigured: cleargate-planning/MANIFEST.json not found. Run: cleargate init`);
4563
+ if (outcome) outcome.configError = true;
4564
+ }
4468
4565
  const settingsPath = path24.join(cwd, ".claude", "settings.json");
4469
4566
  if (!fs22.existsSync(settingsPath)) {
4470
4567
  stdout("[doctor] No .claude/settings.json found \u2014 hook config unavailable.");
@@ -4601,7 +4698,57 @@ function parseCachedGateResult2(raw) {
4601
4698
  failing_criteria: parsed.failing_criteria ?? []
4602
4699
  };
4603
4700
  }
4604
- async function runSessionStart(cwd, stdout) {
4701
+ function emitResolverStatusLine(cwd, stdout) {
4702
+ const distCliPath = path24.join(cwd, "cleargate-cli", "dist", "cli.js");
4703
+ if (fs22.existsSync(distCliPath)) {
4704
+ stdout(`cleargate CLI: local dist \u2014 ${distCliPath}`);
4705
+ return;
4706
+ }
4707
+ const whichResult = (0, import_node_child_process6.spawnSync)("command", ["-v", "cleargate"], {
4708
+ shell: true,
4709
+ encoding: "utf8",
4710
+ timeout: 3e3
4711
+ });
4712
+ if (whichResult.status === 0) {
4713
+ stdout("cleargate CLI: PATH (global install) \u2014 cleargate");
4714
+ return;
4715
+ }
4716
+ let pinVersion = "unknown";
4717
+ const hookPath = path24.join(cwd, ".claude", "hooks", "stamp-and-gate.sh");
4718
+ if (fs22.existsSync(hookPath)) {
4719
+ try {
4720
+ const hookContent = fs22.readFileSync(hookPath, "utf-8");
4721
+ const pinMatch = hookContent.match(/^#\s*cleargate-pin:\s*(\S+)\s*$/m);
4722
+ if (pinMatch?.[1]) {
4723
+ pinVersion = pinMatch[1];
4724
+ } else {
4725
+ const npxMatch = hookContent.match(/@cleargate\/cli@([^\s"']+)/);
4726
+ if (npxMatch?.[1]) pinVersion = npxMatch[1];
4727
+ }
4728
+ } catch {
4729
+ }
4730
+ }
4731
+ if (pinVersion === "unknown") {
4732
+ stdout("cleargate CLI: \u{1F534} not resolvable \u2014 hooks will no-op. Fix: npm i -g cleargate or npx cleargate doctor");
4733
+ } else {
4734
+ stdout(`cleargate CLI: npx @cleargate/cli@${pinVersion} (cold-start ~600ms first call)`);
4735
+ }
4736
+ }
4737
+ var PLANNING_FIRST_REMINDER = `Triage first, draft second:
4738
+ Before any Edit/Write that creates user-facing code, you must:
4739
+ (1) classify the request (Epic / Story / CR / Bug),
4740
+ (2) draft a work item under .cleargate/delivery/pending-sync/ from .cleargate/templates/,
4741
+ (3) halt at Gate 1 (Proposal approval) for human sign-off.
4742
+ Bypass this only if the user has explicitly waived planning in this conversation.`;
4743
+ async function runSessionStart(cwd, stdout, outcome) {
4744
+ const resolverLines = [];
4745
+ emitResolverStatusLine(cwd, (line) => {
4746
+ stdout(line);
4747
+ resolverLines.push(line);
4748
+ });
4749
+ if (outcome && resolverLines.some((l) => l.includes("\u{1F534}"))) {
4750
+ outcome.configError = true;
4751
+ }
4605
4752
  const pendingSyncDir = path24.join(cwd, ".cleargate", "delivery", "pending-sync");
4606
4753
  let files;
4607
4754
  try {
@@ -4610,6 +4757,7 @@ async function runSessionStart(cwd, stdout) {
4610
4757
  return;
4611
4758
  }
4612
4759
  const blocked = [];
4760
+ let hasApprovedStory = false;
4613
4761
  for (const filePath of files) {
4614
4762
  let raw;
4615
4763
  try {
@@ -4624,6 +4772,9 @@ async function runSessionStart(cwd, stdout) {
4624
4772
  } catch {
4625
4773
  continue;
4626
4774
  }
4775
+ if (fm["approved"] === true) {
4776
+ hasApprovedStory = true;
4777
+ }
4627
4778
  const gate2 = parseCachedGateResult2(fm["cached_gate_result"]);
4628
4779
  if (!gate2 || gate2.pass !== false) continue;
4629
4780
  const idKeys = ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id", "sprint_id"];
@@ -4641,9 +4792,19 @@ async function runSessionStart(cwd, stdout) {
4641
4792
  const firstCriterionId = gate2.failing_criteria.length > 0 ? gate2.failing_criteria[0]?.id ?? "" : "";
4642
4793
  blocked.push({ id: itemId, firstCriterionId });
4643
4794
  }
4795
+ const activesentinel = path24.join(cwd, ".cleargate", "sprint-runs", ".active");
4796
+ const sprintActive = fs22.existsSync(activesentinel);
4797
+ const shouldRemind = !hasApprovedStory && !sprintActive;
4798
+ if (shouldRemind) {
4799
+ stdout(PLANNING_FIRST_REMINDER);
4800
+ if (blocked.length > 0) {
4801
+ stdout("");
4802
+ }
4803
+ }
4644
4804
  if (blocked.length === 0) {
4645
4805
  return;
4646
4806
  }
4807
+ if (outcome) outcome.blocker = true;
4647
4808
  const overflow = blocked.length > SESSION_START_MAX_ITEMS ? blocked.length - SESSION_START_MAX_ITEMS : 0;
4648
4809
  const visible = blocked.slice(0, SESSION_START_MAX_ITEMS);
4649
4810
  const lines = [`${blocked.length} items blocked:`];
@@ -4660,10 +4821,11 @@ async function runSessionStart(cwd, stdout) {
4660
4821
  }
4661
4822
  stdout(output);
4662
4823
  }
4663
- async function runPricing(filePath, cwd, stdout, stderr, exit) {
4824
+ async function runPricing(filePath, cwd, stdout, stderr, exit, outcome) {
4664
4825
  if (!filePath) {
4665
4826
  stderr("cleargate doctor --pricing: missing <file> argument");
4666
- exit(1);
4827
+ if (outcome) outcome.configError = true;
4828
+ exit(2);
4667
4829
  return;
4668
4830
  }
4669
4831
  const absPath = path24.isAbsolute(filePath) ? filePath : path24.resolve(cwd, filePath);
@@ -4672,12 +4834,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4672
4834
  raw = fs22.readFileSync(absPath, "utf-8");
4673
4835
  } catch {
4674
4836
  stderr(`cleargate doctor --pricing: cannot read file: ${absPath}`);
4675
- exit(1);
4837
+ if (outcome) outcome.configError = true;
4838
+ exit(2);
4676
4839
  return;
4677
4840
  }
4678
4841
  if (!raw.trimStart().startsWith("---")) {
4679
4842
  stderr(`cleargate doctor --pricing: file has no frontmatter: ${absPath}`);
4680
- exit(1);
4843
+ if (outcome) outcome.configError = true;
4844
+ exit(2);
4681
4845
  return;
4682
4846
  }
4683
4847
  let fm;
@@ -4685,12 +4849,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4685
4849
  fm = parseFrontmatter(raw).fm;
4686
4850
  } catch {
4687
4851
  stderr(`cleargate doctor --pricing: cannot parse frontmatter in: ${absPath}`);
4688
- exit(1);
4852
+ if (outcome) outcome.configError = true;
4853
+ exit(2);
4689
4854
  return;
4690
4855
  }
4691
4856
  const draftTokensRaw = fm["draft_tokens"];
4692
4857
  if (!draftTokensRaw) {
4693
4858
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4859
+ if (outcome) outcome.blocker = true;
4694
4860
  exit(1);
4695
4861
  return;
4696
4862
  }
@@ -4702,16 +4868,19 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4702
4868
  draftTokens = JSON.parse(draftTokensRaw);
4703
4869
  } catch {
4704
4870
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4871
+ if (outcome) outcome.blocker = true;
4705
4872
  exit(1);
4706
4873
  return;
4707
4874
  }
4708
4875
  } else {
4709
4876
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4877
+ if (outcome) outcome.blocker = true;
4710
4878
  exit(1);
4711
4879
  return;
4712
4880
  }
4713
4881
  if (draftTokens.input === null && draftTokens.output === null && draftTokens.cache_read === null && draftTokens.cache_creation === null) {
4714
4882
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4883
+ if (outcome) outcome.blocker = true;
4715
4884
  exit(1);
4716
4885
  return;
4717
4886
  }
@@ -4729,18 +4898,95 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4729
4898
  `${fileName}: ${model} \u2014 input:${input} output:${output} cache_read:${cacheRead} cache_creation:${cacheCreation} \u2248 $${usd.toFixed(4)}`
4730
4899
  );
4731
4900
  }
4901
+ function globMatch(pattern, filePath) {
4902
+ const normalPattern = pattern.replace(/\\/g, "/");
4903
+ const normalFile = filePath.replace(/\\/g, "/");
4904
+ const regexStr = normalPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
4905
+ const re = new RegExp(`^${regexStr}$`);
4906
+ return re.test(normalFile);
4907
+ }
4908
+ async function runCanEdit(filePath, cwd, stdout, exit, outcome) {
4909
+ const activeSentinel = path24.join(cwd, ".cleargate", "sprint-runs", ".active");
4910
+ if (fs22.existsSync(activeSentinel)) {
4911
+ stdout("allowed: sprint active");
4912
+ return;
4913
+ }
4914
+ const pendingSyncDir = path24.join(cwd, ".cleargate", "delivery", "pending-sync");
4915
+ let files;
4916
+ try {
4917
+ files = fs22.readdirSync(pendingSyncDir).filter((f) => f.endsWith(".md")).map((f) => path24.join(pendingSyncDir, f));
4918
+ } catch {
4919
+ stdout("blocked: no_approved_stories");
4920
+ if (outcome) outcome.blocker = true;
4921
+ exit(1);
4922
+ return;
4923
+ }
4924
+ let hasApprovedStory = false;
4925
+ let coveredByStory = false;
4926
+ for (const storyPath of files) {
4927
+ let raw;
4928
+ try {
4929
+ raw = fs22.readFileSync(storyPath, "utf-8");
4930
+ } catch {
4931
+ continue;
4932
+ }
4933
+ if (!raw.trimStart().startsWith("---")) continue;
4934
+ let fm;
4935
+ try {
4936
+ fm = parseFrontmatter(raw).fm;
4937
+ } catch {
4938
+ continue;
4939
+ }
4940
+ if (fm["approved"] !== true) continue;
4941
+ hasApprovedStory = true;
4942
+ const implFilesRaw = fm["implementation_files"];
4943
+ if (implFilesRaw === void 0 || implFilesRaw === null) {
4944
+ coveredByStory = true;
4945
+ break;
4946
+ }
4947
+ if (Array.isArray(implFilesRaw)) {
4948
+ for (const pattern of implFilesRaw) {
4949
+ if (typeof pattern !== "string") continue;
4950
+ if (globMatch(pattern, filePath)) {
4951
+ coveredByStory = true;
4952
+ break;
4953
+ }
4954
+ }
4955
+ }
4956
+ if (coveredByStory) break;
4957
+ }
4958
+ if (!hasApprovedStory) {
4959
+ stdout("blocked: no_approved_stories");
4960
+ if (outcome) outcome.blocker = true;
4961
+ exit(1);
4962
+ return;
4963
+ }
4964
+ if (!coveredByStory) {
4965
+ stdout("blocked: file_not_in_implementation_files");
4966
+ if (outcome) outcome.blocker = true;
4967
+ exit(1);
4968
+ return;
4969
+ }
4970
+ stdout("allowed");
4971
+ }
4732
4972
  async function doctorHandler(flags, cli) {
4733
4973
  const cwd = cli?.cwd ?? process.cwd();
4734
4974
  const now = cli?.now ? cli.now() : /* @__PURE__ */ new Date();
4735
4975
  const stdout = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
4736
4976
  const stderr = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
4737
4977
  const exit = cli?.exit ?? ((code) => process.exit(code));
4978
+ const outcome = { configError: false, blocker: false };
4979
+ let exitedEarly = false;
4980
+ const wrappedExit = (code) => {
4981
+ exitedEarly = true;
4982
+ return exit(code);
4983
+ };
4738
4984
  let mode;
4739
4985
  try {
4740
4986
  mode = selectMode(flags);
4741
4987
  } catch (err) {
4742
4988
  stderr(err.message);
4743
- exit(1);
4989
+ exit(2);
4744
4990
  return;
4745
4991
  }
4746
4992
  switch (mode) {
@@ -4748,27 +4994,39 @@ async function doctorHandler(flags, cli) {
4748
4994
  await runCheckScaffold(flags, cli ?? {}, cwd, now, stdout, stderr);
4749
4995
  break;
4750
4996
  case "hook-health":
4751
- runHookHealth(stdout, cwd, now);
4997
+ runHookHealth(stdout, cwd, now, outcome);
4752
4998
  break;
4753
4999
  case "session-start":
4754
- await runSessionStart(cwd, stdout);
5000
+ await runSessionStart(cwd, stdout, outcome);
4755
5001
  break;
4756
5002
  case "pricing":
4757
- await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr, exit);
5003
+ await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr, wrappedExit, outcome);
5004
+ break;
5005
+ case "can-edit":
5006
+ await runCanEdit(flags.canEditFile ?? "", cwd, stdout, wrappedExit, outcome);
4758
5007
  break;
4759
5008
  default: {
4760
5009
  const exhaustiveCheck = mode;
4761
5010
  stderr(`cleargate doctor: unknown mode '${String(exhaustiveCheck)}'`);
4762
- exit(1);
5011
+ exit(2);
5012
+ return;
4763
5013
  }
4764
5014
  }
5015
+ if (exitedEarly) return;
5016
+ if (outcome.configError) {
5017
+ exit(2);
5018
+ } else if (outcome.blocker) {
5019
+ exit(1);
5020
+ } else {
5021
+ exit(0);
5022
+ }
4765
5023
  }
4766
5024
 
4767
5025
  // src/commands/gate.ts
4768
5026
  init_cjs_shims();
4769
5027
  var fs26 = __toESM(require("fs"), 1);
4770
5028
  var path27 = __toESM(require("path"), 1);
4771
- var import_node_child_process5 = require("child_process");
5029
+ var import_node_child_process7 = require("child_process");
4772
5030
 
4773
5031
  // src/commands/execution-mode.ts
4774
5032
  init_cjs_shims();
@@ -4877,6 +5135,14 @@ function parsePredicate(src) {
4877
5135
  const value = parseValue(rawVal);
4878
5136
  return { kind: "frontmatter", ref, field, op, value };
4879
5137
  }
5138
+ const markerNotMatch = s.match(/^body does not contain marker ['"]([A-Z]+)['"]$/);
5139
+ if (markerNotMatch) {
5140
+ const marker = markerNotMatch[1];
5141
+ if (marker !== "TBD" && marker !== "TODO" && marker !== "FIXME") {
5142
+ throw new Error(`unsupported predicate shape: ${src}`);
5143
+ }
5144
+ return { kind: "marker-absence", marker };
5145
+ }
4880
5146
  const bodyNotMatch = s.match(/^body does not contain ['"](.+)['"]$/);
4881
5147
  if (bodyNotMatch) {
4882
5148
  return { kind: "body-contains", needle: bodyNotMatch[1], negated: true };
@@ -4932,6 +5198,8 @@ function evaluate(predicate, doc, opts) {
4932
5198
  return evalFrontmatter(parsed, doc, projectRoot);
4933
5199
  case "body-contains":
4934
5200
  return evalBodyContains(parsed, doc);
5201
+ case "marker-absence":
5202
+ return evalMarkerAbsence(parsed, doc);
4935
5203
  case "section":
4936
5204
  return evalSection(parsed, doc);
4937
5205
  case "file-exists":
@@ -4954,6 +5222,26 @@ function evalFrontmatter(parsed, doc, projectRoot) {
4954
5222
  detail: `frontmatter key '${parsed.ref}' is missing or null in ${doc.absPath}`
4955
5223
  };
4956
5224
  }
5225
+ const refStr = String(refVal);
5226
+ const looksLikeProse = refStr.length > 200 || /[ —–:()\n]/.test(refStr);
5227
+ if (looksLikeProse) {
5228
+ const waiver = doc.fm["proposal_gate_waiver"];
5229
+ const hasExplicitWaiver = waiver !== null && waiver !== void 0 && waiver !== false && String(waiver).trim() !== "" && String(waiver).trim() !== "false";
5230
+ const approvedBy = doc.fm["approved_by"];
5231
+ const approvedAt = doc.fm["approved_at"];
5232
+ const hasApprovalFields = approvedBy !== null && approvedBy !== void 0 && String(approvedBy).trim() !== "" && approvedAt !== null && approvedAt !== void 0 && String(approvedAt).trim() !== "";
5233
+ const hasWaiver = hasExplicitWaiver || hasApprovalFields;
5234
+ if (hasWaiver) {
5235
+ return {
5236
+ pass: true,
5237
+ detail: `context_source is prose; proposal-gate waiver per frontmatter approved_by/approved_at`
5238
+ };
5239
+ }
5240
+ return {
5241
+ pass: false,
5242
+ detail: `context_source is prose but no proposal_gate_waiver (approved_by + approved_at) found in frontmatter`
5243
+ };
5244
+ }
4957
5245
  const linkedPath = resolveLinkedPath(String(refVal), doc.absPath, projectRoot);
4958
5246
  if (!linkedPath) {
4959
5247
  return {
@@ -5078,6 +5366,38 @@ function evalBodyContains(parsed, doc) {
5078
5366
  return { pass: false, detail: `'${needle}' not found in body` };
5079
5367
  }
5080
5368
  }
5369
+ function evalMarkerAbsence(parsed, doc) {
5370
+ const { marker } = parsed;
5371
+ const lines = doc.body.split("\n");
5372
+ const templateSelfRefRe = /^\s*-\s*\[[x ]\]\s*0\s*"TBDs?"\s*exist/i;
5373
+ const markerRe = new RegExp(
5374
+ `(?:^|(?<=\\())${marker}(?=:)|\\(${marker}\\)|\\[${marker}\\]|(?<=//\\s*)${marker}(?!\\w)|(?<=#\\s*)${marker}(?!\\w)`,
5375
+ // # MARKER (comment)
5376
+ "g"
5377
+ );
5378
+ const bareLineRe = new RegExp(`^${marker}$`);
5379
+ const violations = [];
5380
+ for (let i = 0; i < lines.length; i++) {
5381
+ const line = lines[i];
5382
+ if (templateSelfRefRe.test(line)) continue;
5383
+ const trimmed = line.trim();
5384
+ if (bareLineRe.test(trimmed)) {
5385
+ violations.push(i + 1);
5386
+ continue;
5387
+ }
5388
+ markerRe.lastIndex = 0;
5389
+ if (markerRe.test(line)) {
5390
+ violations.push(i + 1);
5391
+ }
5392
+ }
5393
+ if (violations.length > 0) {
5394
+ return {
5395
+ pass: false,
5396
+ detail: `${violations.length} marker occurrence${violations.length === 1 ? "" : "s"} of '${marker}' at line${violations.length === 1 ? "" : "s"} ${violations.join(", ")}`
5397
+ };
5398
+ }
5399
+ return { pass: true, detail: `no '${marker}' markers found in body` };
5400
+ }
5081
5401
  function evalSection(parsed, doc) {
5082
5402
  const body = doc.body;
5083
5403
  const rawParts = body.split(/^(?=## )/m);
@@ -5446,7 +5766,7 @@ function gateQaHandler(opts, cli) {
5446
5766
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5447
5767
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5448
5768
  const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
5449
- const spawnFn = cli?.spawnFn ?? import_node_child_process5.spawnSync;
5769
+ const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
5450
5770
  const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
5451
5771
  const mode = readSprintExecutionMode(sprintId, {
5452
5772
  sprintFilePath: cli?.sprintFilePath,
@@ -5472,7 +5792,7 @@ function gateArchHandler(opts, cli) {
5472
5792
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5473
5793
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5474
5794
  const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
5475
- const spawnFn = cli?.spawnFn ?? import_node_child_process5.spawnSync;
5795
+ const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
5476
5796
  const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
5477
5797
  const mode = readSprintExecutionMode(sprintId, {
5478
5798
  sprintFilePath: cli?.sprintFilePath,
@@ -5497,13 +5817,13 @@ function gateArchHandler(opts, cli) {
5497
5817
 
5498
5818
  // src/commands/gate-run.ts
5499
5819
  init_cjs_shims();
5500
- var import_node_child_process6 = require("child_process");
5820
+ var import_node_child_process8 = require("child_process");
5501
5821
  var KNOWN_GATES = ["precommit", "test", "typecheck", "lint"];
5502
5822
  function gateRunHandler(name, opts, cli) {
5503
5823
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5504
5824
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5505
5825
  const exitFn = cli?.exit ?? ((code) => process.exit(code));
5506
- const spawnFn = cli?.spawnFn ?? import_node_child_process6.spawnSync;
5826
+ const spawnFn = cli?.spawnFn ?? import_node_child_process8.spawnSync;
5507
5827
  const cwd = cli?.cwd ?? process.cwd();
5508
5828
  const configLoaderFn = cli?.configLoader ?? loadWikiConfig;
5509
5829
  if (!KNOWN_GATES.includes(name)) {
@@ -5536,7 +5856,7 @@ function gateRunHandler(name, opts, cli) {
5536
5856
  init_cjs_shims();
5537
5857
  var fs27 = __toESM(require("fs"), 1);
5538
5858
  var path28 = __toESM(require("path"), 1);
5539
- var import_node_child_process7 = require("child_process");
5859
+ var import_node_child_process9 = require("child_process");
5540
5860
  var import_js_yaml7 = __toESM(require("js-yaml"), 1);
5541
5861
  var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
5542
5862
  function resolveRunScript(opts) {
@@ -5551,7 +5871,7 @@ function sprintInitHandler(opts, cli) {
5551
5871
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5552
5872
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5553
5873
  const exitFn = cli?.exit ?? defaultExit;
5554
- const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
5874
+ const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
5555
5875
  const mode = readSprintExecutionMode(opts.sprintId, {
5556
5876
  sprintFilePath: cli?.sprintFilePath,
5557
5877
  cwd: cli?.cwd
@@ -5573,7 +5893,7 @@ function sprintCloseHandler(opts, cli) {
5573
5893
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5574
5894
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5575
5895
  const exitFn = cli?.exit ?? defaultExit;
5576
- const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
5896
+ const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
5577
5897
  const mode = readSprintExecutionMode(opts.sprintId, {
5578
5898
  sprintFilePath: cli?.sprintFilePath,
5579
5899
  cwd: cli?.cwd
@@ -5678,7 +5998,7 @@ async function sprintArchiveHandler(opts, cli) {
5678
5998
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5679
5999
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5680
6000
  const exitFn = cli?.exit ?? defaultExit;
5681
- const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
6001
+ const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
5682
6002
  const cwd = cli?.cwd ?? process.cwd();
5683
6003
  const wikiBuildFn = cli?.wikiBuildFn ?? (async (wCwd, wStdout) => {
5684
6004
  const fakeExit = (code) => {
@@ -5903,7 +6223,7 @@ async function sprintArchiveHandler(opts, cli) {
5903
6223
  init_cjs_shims();
5904
6224
  var fs28 = __toESM(require("fs"), 1);
5905
6225
  var path29 = __toESM(require("path"), 1);
5906
- var import_node_child_process8 = require("child_process");
6226
+ var import_node_child_process10 = require("child_process");
5907
6227
  function defaultExit2(code) {
5908
6228
  return process.exit(code);
5909
6229
  }
@@ -5929,7 +6249,7 @@ function storyStartHandler(opts, cli) {
5929
6249
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5930
6250
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5931
6251
  const exitFn = cli?.exit ?? defaultExit2;
5932
- const spawnFn = cli?.spawnFn ?? import_node_child_process8.spawnSync;
6252
+ const spawnFn = cli?.spawnFn ?? import_node_child_process10.spawnSync;
5933
6253
  const cwd = cli?.cwd ?? process.cwd();
5934
6254
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
5935
6255
  const mode = readSprintExecutionMode(sprintId, {
@@ -6007,7 +6327,7 @@ function storyCompleteHandler(opts, cli) {
6007
6327
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
6008
6328
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
6009
6329
  const exitFn = cli?.exit ?? defaultExit2;
6010
- const spawnFn = cli?.spawnFn ?? import_node_child_process8.spawnSync;
6330
+ const spawnFn = cli?.spawnFn ?? import_node_child_process10.spawnSync;
6011
6331
  const cwd = cli?.cwd ?? process.cwd();
6012
6332
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
6013
6333
  const mode = readSprintExecutionMode(sprintId, {
@@ -6118,7 +6438,7 @@ function storyCompleteHandler(opts, cli) {
6118
6438
  // src/commands/state.ts
6119
6439
  init_cjs_shims();
6120
6440
  var path30 = __toESM(require("path"), 1);
6121
- var import_node_child_process9 = require("child_process");
6441
+ var import_node_child_process11 = require("child_process");
6122
6442
  function defaultExit3(code) {
6123
6443
  return process.exit(code);
6124
6444
  }
@@ -6131,7 +6451,7 @@ function stateUpdateHandler(opts, cli) {
6131
6451
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
6132
6452
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
6133
6453
  const exitFn = cli?.exit ?? defaultExit3;
6134
- const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
6454
+ const spawnFn = cli?.spawnFn ?? import_node_child_process11.spawnSync;
6135
6455
  const cwd = cli?.cwd;
6136
6456
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
6137
6457
  const mode = readSprintExecutionMode(sprintId, {
@@ -6158,7 +6478,7 @@ function stateValidateHandler(opts, cli) {
6158
6478
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
6159
6479
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
6160
6480
  const exitFn = cli?.exit ?? defaultExit3;
6161
- const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
6481
+ const spawnFn = cli?.spawnFn ?? import_node_child_process11.spawnSync;
6162
6482
  const mode = readSprintExecutionMode(opts.sprintId, {
6163
6483
  sprintFilePath: cli?.sprintFilePath,
6164
6484
  cwd: cli?.cwd
@@ -6635,7 +6955,7 @@ async function promptMergeChoice(opts) {
6635
6955
 
6636
6956
  // src/lib/editor.ts
6637
6957
  init_cjs_shims();
6638
- var import_node_child_process10 = require("child_process");
6958
+ var import_node_child_process12 = require("child_process");
6639
6959
  async function openInEditor(filePath, opts) {
6640
6960
  const env = opts?.env ?? process.env;
6641
6961
  const editor = opts?.editor ?? env["EDITOR"] ?? env["VISUAL"];
@@ -6643,7 +6963,7 @@ async function openInEditor(filePath, opts) {
6643
6963
  throw new Error("$EDITOR not set; cannot [e]dit option. Set the EDITOR environment variable.");
6644
6964
  }
6645
6965
  return new Promise((resolve14, reject) => {
6646
- const child = (0, import_node_child_process10.spawn)(editor, [filePath], {
6966
+ const child = (0, import_node_child_process12.spawn)(editor, [filePath], {
6647
6967
  stdio: "inherit",
6648
6968
  env: { ...env }
6649
6969
  });
@@ -6918,7 +7238,7 @@ init_cjs_shims();
6918
7238
  var fs31 = __toESM(require("fs"), 1);
6919
7239
  var fsp2 = __toESM(require("fs/promises"), 1);
6920
7240
  var path34 = __toESM(require("path"), 1);
6921
- var import_node_child_process11 = require("child_process");
7241
+ var import_node_child_process13 = require("child_process");
6922
7242
  var USER_ARTIFACT_TIERS = ["user-artifact"];
6923
7243
  var FRAMEWORK_TIERS = ["protocol", "template", "agent", "hook", "skill", "cli-config", "derived"];
6924
7244
  function parseTierList(raw) {
@@ -6959,7 +7279,7 @@ function resolveProjectName(target) {
6959
7279
  function detectUncommittedChanges(target, manifestPaths, gitRunner) {
6960
7280
  const run = gitRunner ?? ((args) => {
6961
7281
  try {
6962
- const out = (0, import_node_child_process11.execSync)(["git", ...args].join(" "), {
7282
+ const out = (0, import_node_child_process13.execSync)(["git", ...args].join(" "), {
6963
7283
  cwd: target,
6964
7284
  stdio: ["pipe", "pipe", "pipe"],
6965
7285
  encoding: "utf-8"
@@ -9165,6 +9485,109 @@ async function adminLoginHandler(opts = {}) {
9165
9485
  stdout(`Credentials saved to ${authFilePath} (chmod 600).`);
9166
9486
  }
9167
9487
 
9488
+ // src/commands/hotfix.ts
9489
+ init_cjs_shims();
9490
+ var fs36 = __toESM(require("fs"), 1);
9491
+ var path47 = __toESM(require("path"), 1);
9492
+ function defaultExit4(code) {
9493
+ return process.exit(code);
9494
+ }
9495
+ var SLUG_RE = /^[a-z0-9-]+$/;
9496
+ var HOTFIX_FILE_RE = /^HOTFIX-(\d+)_.*\.md$/;
9497
+ function maxHotfixId(pendingDir) {
9498
+ let max = 0;
9499
+ let entries;
9500
+ try {
9501
+ entries = fs36.readdirSync(pendingDir);
9502
+ } catch {
9503
+ return 0;
9504
+ }
9505
+ for (const entry of entries) {
9506
+ const m = HOTFIX_FILE_RE.exec(entry);
9507
+ if (m) {
9508
+ const n = parseInt(m[1], 10);
9509
+ if (n > max) max = n;
9510
+ }
9511
+ }
9512
+ return max;
9513
+ }
9514
+ function countActiveHotfixes(repoRoot) {
9515
+ const pendingDir = path47.join(repoRoot, ".cleargate", "delivery", "pending-sync");
9516
+ const archiveDir = path47.join(repoRoot, ".cleargate", "delivery", "archive");
9517
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
9518
+ let count = 0;
9519
+ let pendingEntries = [];
9520
+ try {
9521
+ pendingEntries = fs36.readdirSync(pendingDir);
9522
+ } catch {
9523
+ }
9524
+ for (const entry of pendingEntries) {
9525
+ if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) count++;
9526
+ }
9527
+ let archiveEntries = [];
9528
+ try {
9529
+ archiveEntries = fs36.readdirSync(archiveDir);
9530
+ } catch {
9531
+ }
9532
+ for (const entry of archiveEntries) {
9533
+ if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) {
9534
+ try {
9535
+ const stat = fs36.statSync(path47.join(archiveDir, entry));
9536
+ if (stat.mtimeMs >= sevenDaysAgo) count++;
9537
+ } catch {
9538
+ }
9539
+ }
9540
+ }
9541
+ return count;
9542
+ }
9543
+ function resolveTemplatePath(repoRoot) {
9544
+ return path47.join(repoRoot, ".cleargate", "templates", "hotfix.md");
9545
+ }
9546
+ function hotfixNewHandler(opts, cli) {
9547
+ const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
9548
+ const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
9549
+ const exitFn = cli?.exit ?? defaultExit4;
9550
+ const repoRoot = cli?.cwd ?? process.cwd();
9551
+ const now = cli?.now ?? (/* @__PURE__ */ new Date()).toISOString();
9552
+ if (!SLUG_RE.test(opts.slug)) {
9553
+ stderrFn(`[cleargate hotfix new] slug must match ^[a-z0-9-]+$ (got: "${opts.slug}")`);
9554
+ return exitFn(1);
9555
+ }
9556
+ const activeCount = countActiveHotfixes(repoRoot);
9557
+ if (activeCount >= 3) {
9558
+ stderrFn(
9559
+ `Hotfix cap: \u22643 per rolling 7-day window. Currently ${activeCount} active. Bundle into a sprint or downgrade one to a CR.`
9560
+ );
9561
+ return exitFn(1);
9562
+ }
9563
+ const pendingDir = path47.join(repoRoot, ".cleargate", "delivery", "pending-sync");
9564
+ const maxId = maxHotfixId(pendingDir);
9565
+ const nextId = maxId + 1;
9566
+ const idStr = `HOTFIX-${String(nextId).padStart(3, "0")}`;
9567
+ const templatePath = resolveTemplatePath(repoRoot);
9568
+ let templateContent;
9569
+ try {
9570
+ templateContent = fs36.readFileSync(templatePath, "utf8");
9571
+ } catch {
9572
+ stderrFn(`[cleargate hotfix new] template not found: ${templatePath}`);
9573
+ return exitFn(2);
9574
+ }
9575
+ const content = templateContent.replace(/\{ID\}/g, idStr).replace(/\{SLUG\}/g, opts.slug).replace(/\{ISO\}/g, now);
9576
+ const fileSlug = opts.slug.replace(/-/g, "_");
9577
+ const fileName = `${idStr}_${fileSlug}.md`;
9578
+ const outPath = path47.join(pendingDir, fileName);
9579
+ try {
9580
+ fs36.mkdirSync(pendingDir, { recursive: true });
9581
+ fs36.writeFileSync(outPath, content, "utf8");
9582
+ } catch (err) {
9583
+ const msg = err instanceof Error ? err.message : String(err);
9584
+ stderrFn(`[cleargate hotfix new] write failed: ${msg}`);
9585
+ return exitFn(1);
9586
+ }
9587
+ stdoutFn(`[cleargate hotfix new] created: ${outPath}`);
9588
+ return exitFn(0);
9589
+ }
9590
+
9168
9591
  // src/cli.ts
9169
9592
  var program = new import_commander.Command();
9170
9593
  program.name("cleargate").description("ClearGate CLI \u2014 connects AI agent teams to the ClearGate MCP server").version(package_default.version, "-V, --version").option("--profile <name>", "configuration profile to use", "default").option("--mcp-url <url>", "MCP server URL (overrides config file and env)").showHelpAfterError("(use `cleargate --help`)");
@@ -9181,8 +9604,8 @@ program.command("join <invite-url>").description("join a ClearGate workspace usi
9181
9604
  ...cmdOpts.code !== void 0 ? { code: cmdOpts.code } : {}
9182
9605
  });
9183
9606
  });
9184
- program.command("init").description("initialise a repo with ClearGate scaffold (CLAUDE.md block, hook config, agents, templates)").option("--force", "overwrite existing files that differ from the bundled payload").option("--yes", "non-interactive: accept all defaults without prompting").action(async (opts) => {
9185
- await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false });
9607
+ program.command("init").description("initialise a repo with ClearGate scaffold (CLAUDE.md block, hook config, agents, templates)").option("--force", "overwrite existing files that differ from the bundled payload").option("--yes", "non-interactive: accept all defaults without prompting").option("--pin <ver>", "CR-009: pin hook resolver to a specific cleargate CLI version (default: package version)").action(async (opts) => {
9608
+ await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false, pin: opts.pin });
9186
9609
  });
9187
9610
  program.command("whoami").description("print the currently authenticated agent identity").action(async () => {
9188
9611
  const { whoamiHandler: whoamiHandler2 } = await Promise.resolve().then(() => (init_whoami(), whoami_exports));
@@ -9304,14 +9727,20 @@ admin.command("bootstrap-root <handle>").description("seed the first root admin
9304
9727
  const { bootstrapRootHandler: bootstrapRootHandler2 } = await Promise.resolve().then(() => (init_bootstrap_root(), bootstrap_root_exports));
9305
9728
  await bootstrapRootHandler2({ handle, databaseUrl: opts.databaseUrl, force: opts.force ?? false });
9306
9729
  });
9307
- program.command("doctor").description("diagnose scaffold drift, hook health, blocked items, and token cost").option("--check-scaffold", "check scaffold files for drift against install snapshot").option("--session-start-mode", "hidden: enables daily throttle (used by session-start hook)", false).option("--session-start", "emit blocked pending-sync items summary (used by SessionStart hook)").option("--pricing <file>", "compute USD cost estimate from a work item's draft_tokens").option("-v, --verbose", "show per-file drift detail").addHelpText("after", [
9730
+ program.command("doctor").description("diagnose scaffold drift, hook health, blocked items, and token cost").option("--check-scaffold", "check scaffold files for drift against install snapshot").option("--session-start-mode", "hidden: enables daily throttle (used by session-start hook)", false).option("--session-start", "emit blocked pending-sync items summary (used by SessionStart hook)").option("--pricing <file>", "compute USD cost estimate from a work item's draft_tokens").option("--can-edit <file>", "CR-008: exit 0 if editing file is allowed, exit 1 if planning required").option("--cwd <dir>", "working directory for the doctor check (default: process.cwd())").option("-v, --verbose", "show per-file drift detail").addHelpText("after", [
9308
9731
  "",
9309
9732
  "Modes (mutually exclusive):",
9310
9733
  " --check-scaffold Compute drift for all tracked scaffold files.",
9311
9734
  " Writes .cleargate/.drift-state.json.",
9312
9735
  " --session-start List blocked pending-sync items (\u226410, \u2264100 tokens).",
9313
9736
  " --pricing <file> Compute USD estimate from a work item's draft_tokens.",
9314
- " (default) Print a minimal hook-config health report."
9737
+ " --can-edit <file> Check if editing a file requires a planning work item.",
9738
+ " (default) Print a minimal hook-config health report.",
9739
+ "",
9740
+ "Exit codes:",
9741
+ " 0 Clean \u2014 no blockers, no config errors.",
9742
+ " 1 Blocked items or advisory issues \u2014 see stdout.",
9743
+ " 2 ClearGate misconfigured or partially installed \u2014 see stdout for remediation."
9315
9744
  ].join("\n")).action(async (opts) => {
9316
9745
  await doctorHandler({
9317
9746
  checkScaffold: opts.checkScaffold,
@@ -9319,8 +9748,10 @@ program.command("doctor").description("diagnose scaffold drift, hook health, blo
9319
9748
  sessionStart: opts.sessionStart,
9320
9749
  pricing: !!opts.pricing,
9321
9750
  pricingFile: opts.pricing,
9751
+ canEdit: !!opts.canEdit,
9752
+ canEditFile: opts.canEdit,
9322
9753
  verbose: opts.verbose
9323
- });
9754
+ }, opts.cwd ? { cwd: opts.cwd } : void 0);
9324
9755
  });
9325
9756
  program.command("upgrade").description("three-way merge scaffold files with upstream changes").option("--dry-run", "print plan without making any changes").option("--yes", 'auto-accept "take theirs" for all merge-3way files (non-interactive)').option("--only <tier>", "restrict to a specific scaffold tier (protocol/template/agent/hook/skill/cli-config)").addHelpText("after", [
9326
9757
  "",
@@ -9342,7 +9773,7 @@ program.command("uninstall").description("remove ClearGate scaffold from a proje
9342
9773
  "",
9343
9774
  "Always removed (no prompt): .claude/agents/*.md, ClearGate hooks,",
9344
9775
  " .claude/skills/flashcard/, CLAUDE.md CLEARGATE block,",
9345
- " @cleargate/cli from package.json, .install-manifest.json, .drift-state.json.",
9776
+ " `cleargate` from package.json, .install-manifest.json, .drift-state.json.",
9346
9777
  "",
9347
9778
  "Non-git targets: uncommitted-changes check is skipped silently."
9348
9779
  ].join("\n")).action(async (opts) => {
@@ -9395,5 +9826,9 @@ program.command("sync-log").description("filter and print sync-log entries").opt
9395
9826
  limit: opts.limit !== void 0 ? parseInt(opts.limit, 10) : 50
9396
9827
  });
9397
9828
  });
9829
+ var hotfix = program.command("hotfix").description("hotfix lane commands (off-sprint trivial fix scaffolding)");
9830
+ hotfix.command("new <slug>").description("scaffold a new HOTFIX-NNN_<slug>.md in pending-sync/").action((slug) => {
9831
+ hotfixNewHandler({ slug });
9832
+ });
9398
9833
  void program.parseAsync(process.argv);
9399
9834
  //# sourceMappingURL=cli.cjs.map