cleargate 0.5.0 → 0.6.1

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 +485 -51
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.js +480 -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.1",
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,46 @@ 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{1F7E1} cleargate CLI: not resolvable in this environment.
3186
+ [cleargate init] Attempted: ${branchLabel}
3187
+ [cleargate init] This is a warning, not a fatal error. Hooks will no-op until resolved.
3188
+ [cleargate init] Fix: npm i -g cleargate@${pinVersion} or npx cleargate@${pinVersion} doctor
3189
+ `
3190
+ );
3191
+ }
3192
+ }
3193
+ }
3113
3194
  const existingParticipant = readParticipant(cwd);
3114
3195
  if (existingParticipant === null) {
3115
3196
  const identityOpts = opts.identityOpts ?? {};
@@ -3127,8 +3208,10 @@ async function initHandler(opts = {}) {
3127
3208
  stdout(`[cleargate init] Participant identity: ${finalEmail} (inferred)
3128
3209
  `);
3129
3210
  } else {
3130
- const defaultEmail = gitEmail ?? "user@localhost";
3131
- const question = `[cleargate init] Participant email [${defaultEmail}]:`;
3211
+ const isNoreply = gitEmail !== null && /@users\.noreply\.github\.com$/i.test(gitEmail);
3212
+ const defaultEmail = gitEmail !== null && !isNoreply ? gitEmail : "user@localhost";
3213
+ stdout("\n");
3214
+ const question = `Participant email (press Enter for default) [${defaultEmail}]:`;
3132
3215
  const answer = await promptEmailFn(question, defaultEmail);
3133
3216
  await writeParticipant(cwd, answer, "prompted", now);
3134
3217
  stdout(`[cleargate init] Participant identity: ${answer} (prompted)
@@ -3146,7 +3229,7 @@ async function initHandler(opts = {}) {
3146
3229
  init_cjs_shims();
3147
3230
  var fs16 = __toESM(require("fs"), 1);
3148
3231
  var path17 = __toESM(require("path"), 1);
3149
- var import_node_child_process3 = require("child_process");
3232
+ var import_node_child_process4 = require("child_process");
3150
3233
  var EXCLUDED_SUFFIXES2 = [
3151
3234
  ".cleargate/knowledge/",
3152
3235
  ".cleargate/templates/",
@@ -3298,7 +3381,7 @@ function checkContentUnchanged(absRawPath, sha, relRawPath, gitRunner) {
3298
3381
  }
3299
3382
  }
3300
3383
  function defaultGitRunner(cmd, args) {
3301
- const result = (0, import_node_child_process3.spawnSync)(cmd, args, { encoding: "utf8" });
3384
+ const result = (0, import_node_child_process4.spawnSync)(cmd, args, { encoding: "utf8" });
3302
3385
  if (result.status !== 0) return "\0__NONZERO__";
3303
3386
  return result.stdout ?? "";
3304
3387
  }
@@ -3550,7 +3633,7 @@ function loadWikiPages(wikiRoot) {
3550
3633
  init_cjs_shims();
3551
3634
  var fs18 = __toESM(require("fs"), 1);
3552
3635
  var path19 = __toESM(require("path"), 1);
3553
- var import_node_child_process4 = require("child_process");
3636
+ var import_node_child_process5 = require("child_process");
3554
3637
  var import_js_yaml3 = __toESM(require("js-yaml"), 1);
3555
3638
 
3556
3639
  // src/lib/work-item-type.ts
@@ -3646,7 +3729,7 @@ function checkStaleCommit(page, repoRoot, gitRunner) {
3646
3729
  if (gitRunner) {
3647
3730
  currentSha = gitRunner("git", ["log", "-1", "--format=%H", "--", rawPath]).trim();
3648
3731
  } else {
3649
- const result = (0, import_node_child_process4.spawnSync)("git", ["log", "-1", "--format=%H", "--", rawPath], {
3732
+ const result = (0, import_node_child_process5.spawnSync)("git", ["log", "-1", "--format=%H", "--", rawPath], {
3650
3733
  encoding: "utf8",
3651
3734
  cwd: repoRoot
3652
3735
  });
@@ -4391,6 +4474,7 @@ function applyStatusFix(rawText, newStatus) {
4391
4474
  init_cjs_shims();
4392
4475
  var fs22 = __toESM(require("fs"), 1);
4393
4476
  var path24 = __toESM(require("path"), 1);
4477
+ var import_node_child_process6 = require("child_process");
4394
4478
 
4395
4479
  // src/lib/pricing.ts
4396
4480
  init_cjs_shims();
@@ -4440,6 +4524,7 @@ function selectMode(flags) {
4440
4524
  if (flags.checkScaffold) modes.push("check-scaffold");
4441
4525
  if (flags.sessionStart) modes.push("session-start");
4442
4526
  if (flags.pricing) modes.push("pricing");
4527
+ if (flags.canEdit) modes.push("can-edit");
4443
4528
  if (modes.length > 1) {
4444
4529
  throw new Error(
4445
4530
  `cleargate doctor: mutually exclusive flags set: ${modes.join(", ")}. Use only one mode flag at a time.`
@@ -4464,7 +4549,18 @@ function parseHookLogLine(line) {
4464
4549
  file: m[5].trim()
4465
4550
  };
4466
4551
  }
4467
- function runHookHealth(stdout, cwd, now) {
4552
+ function runHookHealth(stdout, cwd, now, outcome) {
4553
+ const cleargateDir = path24.join(cwd, ".cleargate");
4554
+ if (!fs22.existsSync(cleargateDir)) {
4555
+ stdout("cleargate misconfigured: no .cleargate/ found. Run: cleargate init");
4556
+ if (outcome) outcome.configError = true;
4557
+ return;
4558
+ }
4559
+ const manifestPath = path24.join(cwd, "cleargate-planning", "MANIFEST.json");
4560
+ if (!fs22.existsSync(manifestPath)) {
4561
+ stdout(`cleargate misconfigured: cleargate-planning/MANIFEST.json not found. Run: cleargate init`);
4562
+ if (outcome) outcome.configError = true;
4563
+ }
4468
4564
  const settingsPath = path24.join(cwd, ".claude", "settings.json");
4469
4565
  if (!fs22.existsSync(settingsPath)) {
4470
4566
  stdout("[doctor] No .claude/settings.json found \u2014 hook config unavailable.");
@@ -4601,7 +4697,57 @@ function parseCachedGateResult2(raw) {
4601
4697
  failing_criteria: parsed.failing_criteria ?? []
4602
4698
  };
4603
4699
  }
4604
- async function runSessionStart(cwd, stdout) {
4700
+ function emitResolverStatusLine(cwd, stdout) {
4701
+ const distCliPath = path24.join(cwd, "cleargate-cli", "dist", "cli.js");
4702
+ if (fs22.existsSync(distCliPath)) {
4703
+ stdout(`cleargate CLI: local dist \u2014 ${distCliPath}`);
4704
+ return;
4705
+ }
4706
+ const whichResult = (0, import_node_child_process6.spawnSync)("command", ["-v", "cleargate"], {
4707
+ shell: true,
4708
+ encoding: "utf8",
4709
+ timeout: 3e3
4710
+ });
4711
+ if (whichResult.status === 0) {
4712
+ stdout("cleargate CLI: PATH (global install) \u2014 cleargate");
4713
+ return;
4714
+ }
4715
+ let pinVersion = "unknown";
4716
+ const hookPath = path24.join(cwd, ".claude", "hooks", "stamp-and-gate.sh");
4717
+ if (fs22.existsSync(hookPath)) {
4718
+ try {
4719
+ const hookContent = fs22.readFileSync(hookPath, "utf-8");
4720
+ const pinMatch = hookContent.match(/^#\s*cleargate-pin:\s*(\S+)\s*$/m);
4721
+ if (pinMatch?.[1]) {
4722
+ pinVersion = pinMatch[1];
4723
+ } else {
4724
+ const npxMatch = hookContent.match(/@cleargate\/cli@([^\s"']+)/);
4725
+ if (npxMatch?.[1]) pinVersion = npxMatch[1];
4726
+ }
4727
+ } catch {
4728
+ }
4729
+ }
4730
+ if (pinVersion === "unknown") {
4731
+ stdout("cleargate CLI: \u{1F534} not resolvable \u2014 hooks will no-op. Fix: npm i -g cleargate or npx cleargate doctor");
4732
+ } else {
4733
+ stdout(`cleargate CLI: npx @cleargate/cli@${pinVersion} (cold-start ~600ms first call)`);
4734
+ }
4735
+ }
4736
+ var PLANNING_FIRST_REMINDER = `Triage first, draft second:
4737
+ Before any Edit/Write that creates user-facing code, you must:
4738
+ (1) classify the request (Epic / Story / CR / Bug),
4739
+ (2) draft a work item under .cleargate/delivery/pending-sync/ from .cleargate/templates/,
4740
+ (3) halt at Gate 1 (Proposal approval) for human sign-off.
4741
+ Bypass this only if the user has explicitly waived planning in this conversation.`;
4742
+ async function runSessionStart(cwd, stdout, outcome) {
4743
+ const resolverLines = [];
4744
+ emitResolverStatusLine(cwd, (line) => {
4745
+ stdout(line);
4746
+ resolverLines.push(line);
4747
+ });
4748
+ if (outcome && resolverLines.some((l) => l.includes("\u{1F534}"))) {
4749
+ outcome.configError = true;
4750
+ }
4605
4751
  const pendingSyncDir = path24.join(cwd, ".cleargate", "delivery", "pending-sync");
4606
4752
  let files;
4607
4753
  try {
@@ -4610,6 +4756,7 @@ async function runSessionStart(cwd, stdout) {
4610
4756
  return;
4611
4757
  }
4612
4758
  const blocked = [];
4759
+ let hasApprovedStory = false;
4613
4760
  for (const filePath of files) {
4614
4761
  let raw;
4615
4762
  try {
@@ -4624,6 +4771,9 @@ async function runSessionStart(cwd, stdout) {
4624
4771
  } catch {
4625
4772
  continue;
4626
4773
  }
4774
+ if (fm["approved"] === true) {
4775
+ hasApprovedStory = true;
4776
+ }
4627
4777
  const gate2 = parseCachedGateResult2(fm["cached_gate_result"]);
4628
4778
  if (!gate2 || gate2.pass !== false) continue;
4629
4779
  const idKeys = ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id", "sprint_id"];
@@ -4641,9 +4791,19 @@ async function runSessionStart(cwd, stdout) {
4641
4791
  const firstCriterionId = gate2.failing_criteria.length > 0 ? gate2.failing_criteria[0]?.id ?? "" : "";
4642
4792
  blocked.push({ id: itemId, firstCriterionId });
4643
4793
  }
4794
+ const activesentinel = path24.join(cwd, ".cleargate", "sprint-runs", ".active");
4795
+ const sprintActive = fs22.existsSync(activesentinel);
4796
+ const shouldRemind = !hasApprovedStory && !sprintActive;
4797
+ if (shouldRemind) {
4798
+ stdout(PLANNING_FIRST_REMINDER);
4799
+ if (blocked.length > 0) {
4800
+ stdout("");
4801
+ }
4802
+ }
4644
4803
  if (blocked.length === 0) {
4645
4804
  return;
4646
4805
  }
4806
+ if (outcome) outcome.blocker = true;
4647
4807
  const overflow = blocked.length > SESSION_START_MAX_ITEMS ? blocked.length - SESSION_START_MAX_ITEMS : 0;
4648
4808
  const visible = blocked.slice(0, SESSION_START_MAX_ITEMS);
4649
4809
  const lines = [`${blocked.length} items blocked:`];
@@ -4660,10 +4820,11 @@ async function runSessionStart(cwd, stdout) {
4660
4820
  }
4661
4821
  stdout(output);
4662
4822
  }
4663
- async function runPricing(filePath, cwd, stdout, stderr, exit) {
4823
+ async function runPricing(filePath, cwd, stdout, stderr, exit, outcome) {
4664
4824
  if (!filePath) {
4665
4825
  stderr("cleargate doctor --pricing: missing <file> argument");
4666
- exit(1);
4826
+ if (outcome) outcome.configError = true;
4827
+ exit(2);
4667
4828
  return;
4668
4829
  }
4669
4830
  const absPath = path24.isAbsolute(filePath) ? filePath : path24.resolve(cwd, filePath);
@@ -4672,12 +4833,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4672
4833
  raw = fs22.readFileSync(absPath, "utf-8");
4673
4834
  } catch {
4674
4835
  stderr(`cleargate doctor --pricing: cannot read file: ${absPath}`);
4675
- exit(1);
4836
+ if (outcome) outcome.configError = true;
4837
+ exit(2);
4676
4838
  return;
4677
4839
  }
4678
4840
  if (!raw.trimStart().startsWith("---")) {
4679
4841
  stderr(`cleargate doctor --pricing: file has no frontmatter: ${absPath}`);
4680
- exit(1);
4842
+ if (outcome) outcome.configError = true;
4843
+ exit(2);
4681
4844
  return;
4682
4845
  }
4683
4846
  let fm;
@@ -4685,12 +4848,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4685
4848
  fm = parseFrontmatter(raw).fm;
4686
4849
  } catch {
4687
4850
  stderr(`cleargate doctor --pricing: cannot parse frontmatter in: ${absPath}`);
4688
- exit(1);
4851
+ if (outcome) outcome.configError = true;
4852
+ exit(2);
4689
4853
  return;
4690
4854
  }
4691
4855
  const draftTokensRaw = fm["draft_tokens"];
4692
4856
  if (!draftTokensRaw) {
4693
4857
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4858
+ if (outcome) outcome.blocker = true;
4694
4859
  exit(1);
4695
4860
  return;
4696
4861
  }
@@ -4702,16 +4867,19 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4702
4867
  draftTokens = JSON.parse(draftTokensRaw);
4703
4868
  } catch {
4704
4869
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4870
+ if (outcome) outcome.blocker = true;
4705
4871
  exit(1);
4706
4872
  return;
4707
4873
  }
4708
4874
  } else {
4709
4875
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4876
+ if (outcome) outcome.blocker = true;
4710
4877
  exit(1);
4711
4878
  return;
4712
4879
  }
4713
4880
  if (draftTokens.input === null && draftTokens.output === null && draftTokens.cache_read === null && draftTokens.cache_creation === null) {
4714
4881
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4882
+ if (outcome) outcome.blocker = true;
4715
4883
  exit(1);
4716
4884
  return;
4717
4885
  }
@@ -4729,18 +4897,95 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4729
4897
  `${fileName}: ${model} \u2014 input:${input} output:${output} cache_read:${cacheRead} cache_creation:${cacheCreation} \u2248 $${usd.toFixed(4)}`
4730
4898
  );
4731
4899
  }
4900
+ function globMatch(pattern, filePath) {
4901
+ const normalPattern = pattern.replace(/\\/g, "/");
4902
+ const normalFile = filePath.replace(/\\/g, "/");
4903
+ const regexStr = normalPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
4904
+ const re = new RegExp(`^${regexStr}$`);
4905
+ return re.test(normalFile);
4906
+ }
4907
+ async function runCanEdit(filePath, cwd, stdout, exit, outcome) {
4908
+ const activeSentinel = path24.join(cwd, ".cleargate", "sprint-runs", ".active");
4909
+ if (fs22.existsSync(activeSentinel)) {
4910
+ stdout("allowed: sprint active");
4911
+ return;
4912
+ }
4913
+ const pendingSyncDir = path24.join(cwd, ".cleargate", "delivery", "pending-sync");
4914
+ let files;
4915
+ try {
4916
+ files = fs22.readdirSync(pendingSyncDir).filter((f) => f.endsWith(".md")).map((f) => path24.join(pendingSyncDir, f));
4917
+ } catch {
4918
+ stdout("blocked: no_approved_stories");
4919
+ if (outcome) outcome.blocker = true;
4920
+ exit(1);
4921
+ return;
4922
+ }
4923
+ let hasApprovedStory = false;
4924
+ let coveredByStory = false;
4925
+ for (const storyPath of files) {
4926
+ let raw;
4927
+ try {
4928
+ raw = fs22.readFileSync(storyPath, "utf-8");
4929
+ } catch {
4930
+ continue;
4931
+ }
4932
+ if (!raw.trimStart().startsWith("---")) continue;
4933
+ let fm;
4934
+ try {
4935
+ fm = parseFrontmatter(raw).fm;
4936
+ } catch {
4937
+ continue;
4938
+ }
4939
+ if (fm["approved"] !== true) continue;
4940
+ hasApprovedStory = true;
4941
+ const implFilesRaw = fm["implementation_files"];
4942
+ if (implFilesRaw === void 0 || implFilesRaw === null) {
4943
+ coveredByStory = true;
4944
+ break;
4945
+ }
4946
+ if (Array.isArray(implFilesRaw)) {
4947
+ for (const pattern of implFilesRaw) {
4948
+ if (typeof pattern !== "string") continue;
4949
+ if (globMatch(pattern, filePath)) {
4950
+ coveredByStory = true;
4951
+ break;
4952
+ }
4953
+ }
4954
+ }
4955
+ if (coveredByStory) break;
4956
+ }
4957
+ if (!hasApprovedStory) {
4958
+ stdout("blocked: no_approved_stories");
4959
+ if (outcome) outcome.blocker = true;
4960
+ exit(1);
4961
+ return;
4962
+ }
4963
+ if (!coveredByStory) {
4964
+ stdout("blocked: file_not_in_implementation_files");
4965
+ if (outcome) outcome.blocker = true;
4966
+ exit(1);
4967
+ return;
4968
+ }
4969
+ stdout("allowed");
4970
+ }
4732
4971
  async function doctorHandler(flags, cli) {
4733
4972
  const cwd = cli?.cwd ?? process.cwd();
4734
4973
  const now = cli?.now ? cli.now() : /* @__PURE__ */ new Date();
4735
4974
  const stdout = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
4736
4975
  const stderr = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
4737
4976
  const exit = cli?.exit ?? ((code) => process.exit(code));
4977
+ const outcome = { configError: false, blocker: false };
4978
+ let exitedEarly = false;
4979
+ const wrappedExit = (code) => {
4980
+ exitedEarly = true;
4981
+ return exit(code);
4982
+ };
4738
4983
  let mode;
4739
4984
  try {
4740
4985
  mode = selectMode(flags);
4741
4986
  } catch (err) {
4742
4987
  stderr(err.message);
4743
- exit(1);
4988
+ exit(2);
4744
4989
  return;
4745
4990
  }
4746
4991
  switch (mode) {
@@ -4748,27 +4993,39 @@ async function doctorHandler(flags, cli) {
4748
4993
  await runCheckScaffold(flags, cli ?? {}, cwd, now, stdout, stderr);
4749
4994
  break;
4750
4995
  case "hook-health":
4751
- runHookHealth(stdout, cwd, now);
4996
+ runHookHealth(stdout, cwd, now, outcome);
4752
4997
  break;
4753
4998
  case "session-start":
4754
- await runSessionStart(cwd, stdout);
4999
+ await runSessionStart(cwd, stdout, outcome);
4755
5000
  break;
4756
5001
  case "pricing":
4757
- await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr, exit);
5002
+ await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr, wrappedExit, outcome);
5003
+ break;
5004
+ case "can-edit":
5005
+ await runCanEdit(flags.canEditFile ?? "", cwd, stdout, wrappedExit, outcome);
4758
5006
  break;
4759
5007
  default: {
4760
5008
  const exhaustiveCheck = mode;
4761
5009
  stderr(`cleargate doctor: unknown mode '${String(exhaustiveCheck)}'`);
4762
- exit(1);
5010
+ exit(2);
5011
+ return;
4763
5012
  }
4764
5013
  }
5014
+ if (exitedEarly) return;
5015
+ if (outcome.configError) {
5016
+ exit(2);
5017
+ } else if (outcome.blocker) {
5018
+ exit(1);
5019
+ } else {
5020
+ exit(0);
5021
+ }
4765
5022
  }
4766
5023
 
4767
5024
  // src/commands/gate.ts
4768
5025
  init_cjs_shims();
4769
5026
  var fs26 = __toESM(require("fs"), 1);
4770
5027
  var path27 = __toESM(require("path"), 1);
4771
- var import_node_child_process5 = require("child_process");
5028
+ var import_node_child_process7 = require("child_process");
4772
5029
 
4773
5030
  // src/commands/execution-mode.ts
4774
5031
  init_cjs_shims();
@@ -4877,6 +5134,14 @@ function parsePredicate(src) {
4877
5134
  const value = parseValue(rawVal);
4878
5135
  return { kind: "frontmatter", ref, field, op, value };
4879
5136
  }
5137
+ const markerNotMatch = s.match(/^body does not contain marker ['"]([A-Z]+)['"]$/);
5138
+ if (markerNotMatch) {
5139
+ const marker = markerNotMatch[1];
5140
+ if (marker !== "TBD" && marker !== "TODO" && marker !== "FIXME") {
5141
+ throw new Error(`unsupported predicate shape: ${src}`);
5142
+ }
5143
+ return { kind: "marker-absence", marker };
5144
+ }
4880
5145
  const bodyNotMatch = s.match(/^body does not contain ['"](.+)['"]$/);
4881
5146
  if (bodyNotMatch) {
4882
5147
  return { kind: "body-contains", needle: bodyNotMatch[1], negated: true };
@@ -4932,6 +5197,8 @@ function evaluate(predicate, doc, opts) {
4932
5197
  return evalFrontmatter(parsed, doc, projectRoot);
4933
5198
  case "body-contains":
4934
5199
  return evalBodyContains(parsed, doc);
5200
+ case "marker-absence":
5201
+ return evalMarkerAbsence(parsed, doc);
4935
5202
  case "section":
4936
5203
  return evalSection(parsed, doc);
4937
5204
  case "file-exists":
@@ -4954,6 +5221,26 @@ function evalFrontmatter(parsed, doc, projectRoot) {
4954
5221
  detail: `frontmatter key '${parsed.ref}' is missing or null in ${doc.absPath}`
4955
5222
  };
4956
5223
  }
5224
+ const refStr = String(refVal);
5225
+ const looksLikeProse = refStr.length > 200 || /[ —–:()\n]/.test(refStr);
5226
+ if (looksLikeProse) {
5227
+ const waiver = doc.fm["proposal_gate_waiver"];
5228
+ const hasExplicitWaiver = waiver !== null && waiver !== void 0 && waiver !== false && String(waiver).trim() !== "" && String(waiver).trim() !== "false";
5229
+ const approvedBy = doc.fm["approved_by"];
5230
+ const approvedAt = doc.fm["approved_at"];
5231
+ const hasApprovalFields = approvedBy !== null && approvedBy !== void 0 && String(approvedBy).trim() !== "" && approvedAt !== null && approvedAt !== void 0 && String(approvedAt).trim() !== "";
5232
+ const hasWaiver = hasExplicitWaiver || hasApprovalFields;
5233
+ if (hasWaiver) {
5234
+ return {
5235
+ pass: true,
5236
+ detail: `context_source is prose; proposal-gate waiver per frontmatter approved_by/approved_at`
5237
+ };
5238
+ }
5239
+ return {
5240
+ pass: false,
5241
+ detail: `context_source is prose but no proposal_gate_waiver (approved_by + approved_at) found in frontmatter`
5242
+ };
5243
+ }
4957
5244
  const linkedPath = resolveLinkedPath(String(refVal), doc.absPath, projectRoot);
4958
5245
  if (!linkedPath) {
4959
5246
  return {
@@ -5078,6 +5365,38 @@ function evalBodyContains(parsed, doc) {
5078
5365
  return { pass: false, detail: `'${needle}' not found in body` };
5079
5366
  }
5080
5367
  }
5368
+ function evalMarkerAbsence(parsed, doc) {
5369
+ const { marker } = parsed;
5370
+ const lines = doc.body.split("\n");
5371
+ const templateSelfRefRe = /^\s*-\s*\[[x ]\]\s*0\s*"TBDs?"\s*exist/i;
5372
+ const markerRe = new RegExp(
5373
+ `(?:^|(?<=\\())${marker}(?=:)|\\(${marker}\\)|\\[${marker}\\]|(?<=//\\s*)${marker}(?!\\w)|(?<=#\\s*)${marker}(?!\\w)`,
5374
+ // # MARKER (comment)
5375
+ "g"
5376
+ );
5377
+ const bareLineRe = new RegExp(`^${marker}$`);
5378
+ const violations = [];
5379
+ for (let i = 0; i < lines.length; i++) {
5380
+ const line = lines[i];
5381
+ if (templateSelfRefRe.test(line)) continue;
5382
+ const trimmed = line.trim();
5383
+ if (bareLineRe.test(trimmed)) {
5384
+ violations.push(i + 1);
5385
+ continue;
5386
+ }
5387
+ markerRe.lastIndex = 0;
5388
+ if (markerRe.test(line)) {
5389
+ violations.push(i + 1);
5390
+ }
5391
+ }
5392
+ if (violations.length > 0) {
5393
+ return {
5394
+ pass: false,
5395
+ detail: `${violations.length} marker occurrence${violations.length === 1 ? "" : "s"} of '${marker}' at line${violations.length === 1 ? "" : "s"} ${violations.join(", ")}`
5396
+ };
5397
+ }
5398
+ return { pass: true, detail: `no '${marker}' markers found in body` };
5399
+ }
5081
5400
  function evalSection(parsed, doc) {
5082
5401
  const body = doc.body;
5083
5402
  const rawParts = body.split(/^(?=## )/m);
@@ -5446,7 +5765,7 @@ function gateQaHandler(opts, cli) {
5446
5765
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5447
5766
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5448
5767
  const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
5449
- const spawnFn = cli?.spawnFn ?? import_node_child_process5.spawnSync;
5768
+ const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
5450
5769
  const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
5451
5770
  const mode = readSprintExecutionMode(sprintId, {
5452
5771
  sprintFilePath: cli?.sprintFilePath,
@@ -5472,7 +5791,7 @@ function gateArchHandler(opts, cli) {
5472
5791
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5473
5792
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5474
5793
  const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
5475
- const spawnFn = cli?.spawnFn ?? import_node_child_process5.spawnSync;
5794
+ const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
5476
5795
  const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
5477
5796
  const mode = readSprintExecutionMode(sprintId, {
5478
5797
  sprintFilePath: cli?.sprintFilePath,
@@ -5497,13 +5816,13 @@ function gateArchHandler(opts, cli) {
5497
5816
 
5498
5817
  // src/commands/gate-run.ts
5499
5818
  init_cjs_shims();
5500
- var import_node_child_process6 = require("child_process");
5819
+ var import_node_child_process8 = require("child_process");
5501
5820
  var KNOWN_GATES = ["precommit", "test", "typecheck", "lint"];
5502
5821
  function gateRunHandler(name, opts, cli) {
5503
5822
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5504
5823
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5505
5824
  const exitFn = cli?.exit ?? ((code) => process.exit(code));
5506
- const spawnFn = cli?.spawnFn ?? import_node_child_process6.spawnSync;
5825
+ const spawnFn = cli?.spawnFn ?? import_node_child_process8.spawnSync;
5507
5826
  const cwd = cli?.cwd ?? process.cwd();
5508
5827
  const configLoaderFn = cli?.configLoader ?? loadWikiConfig;
5509
5828
  if (!KNOWN_GATES.includes(name)) {
@@ -5536,7 +5855,7 @@ function gateRunHandler(name, opts, cli) {
5536
5855
  init_cjs_shims();
5537
5856
  var fs27 = __toESM(require("fs"), 1);
5538
5857
  var path28 = __toESM(require("path"), 1);
5539
- var import_node_child_process7 = require("child_process");
5858
+ var import_node_child_process9 = require("child_process");
5540
5859
  var import_js_yaml7 = __toESM(require("js-yaml"), 1);
5541
5860
  var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
5542
5861
  function resolveRunScript(opts) {
@@ -5551,7 +5870,7 @@ function sprintInitHandler(opts, cli) {
5551
5870
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5552
5871
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5553
5872
  const exitFn = cli?.exit ?? defaultExit;
5554
- const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
5873
+ const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
5555
5874
  const mode = readSprintExecutionMode(opts.sprintId, {
5556
5875
  sprintFilePath: cli?.sprintFilePath,
5557
5876
  cwd: cli?.cwd
@@ -5573,7 +5892,7 @@ function sprintCloseHandler(opts, cli) {
5573
5892
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5574
5893
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5575
5894
  const exitFn = cli?.exit ?? defaultExit;
5576
- const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
5895
+ const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
5577
5896
  const mode = readSprintExecutionMode(opts.sprintId, {
5578
5897
  sprintFilePath: cli?.sprintFilePath,
5579
5898
  cwd: cli?.cwd
@@ -5678,7 +5997,7 @@ async function sprintArchiveHandler(opts, cli) {
5678
5997
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5679
5998
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5680
5999
  const exitFn = cli?.exit ?? defaultExit;
5681
- const spawnFn = cli?.spawnFn ?? import_node_child_process7.spawnSync;
6000
+ const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
5682
6001
  const cwd = cli?.cwd ?? process.cwd();
5683
6002
  const wikiBuildFn = cli?.wikiBuildFn ?? (async (wCwd, wStdout) => {
5684
6003
  const fakeExit = (code) => {
@@ -5903,7 +6222,7 @@ async function sprintArchiveHandler(opts, cli) {
5903
6222
  init_cjs_shims();
5904
6223
  var fs28 = __toESM(require("fs"), 1);
5905
6224
  var path29 = __toESM(require("path"), 1);
5906
- var import_node_child_process8 = require("child_process");
6225
+ var import_node_child_process10 = require("child_process");
5907
6226
  function defaultExit2(code) {
5908
6227
  return process.exit(code);
5909
6228
  }
@@ -5929,7 +6248,7 @@ function storyStartHandler(opts, cli) {
5929
6248
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5930
6249
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5931
6250
  const exitFn = cli?.exit ?? defaultExit2;
5932
- const spawnFn = cli?.spawnFn ?? import_node_child_process8.spawnSync;
6251
+ const spawnFn = cli?.spawnFn ?? import_node_child_process10.spawnSync;
5933
6252
  const cwd = cli?.cwd ?? process.cwd();
5934
6253
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
5935
6254
  const mode = readSprintExecutionMode(sprintId, {
@@ -6007,7 +6326,7 @@ function storyCompleteHandler(opts, cli) {
6007
6326
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
6008
6327
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
6009
6328
  const exitFn = cli?.exit ?? defaultExit2;
6010
- const spawnFn = cli?.spawnFn ?? import_node_child_process8.spawnSync;
6329
+ const spawnFn = cli?.spawnFn ?? import_node_child_process10.spawnSync;
6011
6330
  const cwd = cli?.cwd ?? process.cwd();
6012
6331
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
6013
6332
  const mode = readSprintExecutionMode(sprintId, {
@@ -6118,7 +6437,7 @@ function storyCompleteHandler(opts, cli) {
6118
6437
  // src/commands/state.ts
6119
6438
  init_cjs_shims();
6120
6439
  var path30 = __toESM(require("path"), 1);
6121
- var import_node_child_process9 = require("child_process");
6440
+ var import_node_child_process11 = require("child_process");
6122
6441
  function defaultExit3(code) {
6123
6442
  return process.exit(code);
6124
6443
  }
@@ -6131,7 +6450,7 @@ function stateUpdateHandler(opts, cli) {
6131
6450
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
6132
6451
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
6133
6452
  const exitFn = cli?.exit ?? defaultExit3;
6134
- const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
6453
+ const spawnFn = cli?.spawnFn ?? import_node_child_process11.spawnSync;
6135
6454
  const cwd = cli?.cwd;
6136
6455
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
6137
6456
  const mode = readSprintExecutionMode(sprintId, {
@@ -6158,7 +6477,7 @@ function stateValidateHandler(opts, cli) {
6158
6477
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
6159
6478
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
6160
6479
  const exitFn = cli?.exit ?? defaultExit3;
6161
- const spawnFn = cli?.spawnFn ?? import_node_child_process9.spawnSync;
6480
+ const spawnFn = cli?.spawnFn ?? import_node_child_process11.spawnSync;
6162
6481
  const mode = readSprintExecutionMode(opts.sprintId, {
6163
6482
  sprintFilePath: cli?.sprintFilePath,
6164
6483
  cwd: cli?.cwd
@@ -6635,7 +6954,7 @@ async function promptMergeChoice(opts) {
6635
6954
 
6636
6955
  // src/lib/editor.ts
6637
6956
  init_cjs_shims();
6638
- var import_node_child_process10 = require("child_process");
6957
+ var import_node_child_process12 = require("child_process");
6639
6958
  async function openInEditor(filePath, opts) {
6640
6959
  const env = opts?.env ?? process.env;
6641
6960
  const editor = opts?.editor ?? env["EDITOR"] ?? env["VISUAL"];
@@ -6643,7 +6962,7 @@ async function openInEditor(filePath, opts) {
6643
6962
  throw new Error("$EDITOR not set; cannot [e]dit option. Set the EDITOR environment variable.");
6644
6963
  }
6645
6964
  return new Promise((resolve14, reject) => {
6646
- const child = (0, import_node_child_process10.spawn)(editor, [filePath], {
6965
+ const child = (0, import_node_child_process12.spawn)(editor, [filePath], {
6647
6966
  stdio: "inherit",
6648
6967
  env: { ...env }
6649
6968
  });
@@ -6918,7 +7237,7 @@ init_cjs_shims();
6918
7237
  var fs31 = __toESM(require("fs"), 1);
6919
7238
  var fsp2 = __toESM(require("fs/promises"), 1);
6920
7239
  var path34 = __toESM(require("path"), 1);
6921
- var import_node_child_process11 = require("child_process");
7240
+ var import_node_child_process13 = require("child_process");
6922
7241
  var USER_ARTIFACT_TIERS = ["user-artifact"];
6923
7242
  var FRAMEWORK_TIERS = ["protocol", "template", "agent", "hook", "skill", "cli-config", "derived"];
6924
7243
  function parseTierList(raw) {
@@ -6959,7 +7278,7 @@ function resolveProjectName(target) {
6959
7278
  function detectUncommittedChanges(target, manifestPaths, gitRunner) {
6960
7279
  const run = gitRunner ?? ((args) => {
6961
7280
  try {
6962
- const out = (0, import_node_child_process11.execSync)(["git", ...args].join(" "), {
7281
+ const out = (0, import_node_child_process13.execSync)(["git", ...args].join(" "), {
6963
7282
  cwd: target,
6964
7283
  stdio: ["pipe", "pipe", "pipe"],
6965
7284
  encoding: "utf-8"
@@ -9165,6 +9484,109 @@ async function adminLoginHandler(opts = {}) {
9165
9484
  stdout(`Credentials saved to ${authFilePath} (chmod 600).`);
9166
9485
  }
9167
9486
 
9487
+ // src/commands/hotfix.ts
9488
+ init_cjs_shims();
9489
+ var fs36 = __toESM(require("fs"), 1);
9490
+ var path47 = __toESM(require("path"), 1);
9491
+ function defaultExit4(code) {
9492
+ return process.exit(code);
9493
+ }
9494
+ var SLUG_RE = /^[a-z0-9-]+$/;
9495
+ var HOTFIX_FILE_RE = /^HOTFIX-(\d+)_.*\.md$/;
9496
+ function maxHotfixId(pendingDir) {
9497
+ let max = 0;
9498
+ let entries;
9499
+ try {
9500
+ entries = fs36.readdirSync(pendingDir);
9501
+ } catch {
9502
+ return 0;
9503
+ }
9504
+ for (const entry of entries) {
9505
+ const m = HOTFIX_FILE_RE.exec(entry);
9506
+ if (m) {
9507
+ const n = parseInt(m[1], 10);
9508
+ if (n > max) max = n;
9509
+ }
9510
+ }
9511
+ return max;
9512
+ }
9513
+ function countActiveHotfixes(repoRoot) {
9514
+ const pendingDir = path47.join(repoRoot, ".cleargate", "delivery", "pending-sync");
9515
+ const archiveDir = path47.join(repoRoot, ".cleargate", "delivery", "archive");
9516
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
9517
+ let count = 0;
9518
+ let pendingEntries = [];
9519
+ try {
9520
+ pendingEntries = fs36.readdirSync(pendingDir);
9521
+ } catch {
9522
+ }
9523
+ for (const entry of pendingEntries) {
9524
+ if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) count++;
9525
+ }
9526
+ let archiveEntries = [];
9527
+ try {
9528
+ archiveEntries = fs36.readdirSync(archiveDir);
9529
+ } catch {
9530
+ }
9531
+ for (const entry of archiveEntries) {
9532
+ if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) {
9533
+ try {
9534
+ const stat = fs36.statSync(path47.join(archiveDir, entry));
9535
+ if (stat.mtimeMs >= sevenDaysAgo) count++;
9536
+ } catch {
9537
+ }
9538
+ }
9539
+ }
9540
+ return count;
9541
+ }
9542
+ function resolveTemplatePath(repoRoot) {
9543
+ return path47.join(repoRoot, ".cleargate", "templates", "hotfix.md");
9544
+ }
9545
+ function hotfixNewHandler(opts, cli) {
9546
+ const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
9547
+ const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
9548
+ const exitFn = cli?.exit ?? defaultExit4;
9549
+ const repoRoot = cli?.cwd ?? process.cwd();
9550
+ const now = cli?.now ?? (/* @__PURE__ */ new Date()).toISOString();
9551
+ if (!SLUG_RE.test(opts.slug)) {
9552
+ stderrFn(`[cleargate hotfix new] slug must match ^[a-z0-9-]+$ (got: "${opts.slug}")`);
9553
+ return exitFn(1);
9554
+ }
9555
+ const activeCount = countActiveHotfixes(repoRoot);
9556
+ if (activeCount >= 3) {
9557
+ stderrFn(
9558
+ `Hotfix cap: \u22643 per rolling 7-day window. Currently ${activeCount} active. Bundle into a sprint or downgrade one to a CR.`
9559
+ );
9560
+ return exitFn(1);
9561
+ }
9562
+ const pendingDir = path47.join(repoRoot, ".cleargate", "delivery", "pending-sync");
9563
+ const maxId = maxHotfixId(pendingDir);
9564
+ const nextId = maxId + 1;
9565
+ const idStr = `HOTFIX-${String(nextId).padStart(3, "0")}`;
9566
+ const templatePath = resolveTemplatePath(repoRoot);
9567
+ let templateContent;
9568
+ try {
9569
+ templateContent = fs36.readFileSync(templatePath, "utf8");
9570
+ } catch {
9571
+ stderrFn(`[cleargate hotfix new] template not found: ${templatePath}`);
9572
+ return exitFn(2);
9573
+ }
9574
+ const content = templateContent.replace(/\{ID\}/g, idStr).replace(/\{SLUG\}/g, opts.slug).replace(/\{ISO\}/g, now);
9575
+ const fileSlug = opts.slug.replace(/-/g, "_");
9576
+ const fileName = `${idStr}_${fileSlug}.md`;
9577
+ const outPath = path47.join(pendingDir, fileName);
9578
+ try {
9579
+ fs36.mkdirSync(pendingDir, { recursive: true });
9580
+ fs36.writeFileSync(outPath, content, "utf8");
9581
+ } catch (err) {
9582
+ const msg = err instanceof Error ? err.message : String(err);
9583
+ stderrFn(`[cleargate hotfix new] write failed: ${msg}`);
9584
+ return exitFn(1);
9585
+ }
9586
+ stdoutFn(`[cleargate hotfix new] created: ${outPath}`);
9587
+ return exitFn(0);
9588
+ }
9589
+
9168
9590
  // src/cli.ts
9169
9591
  var program = new import_commander.Command();
9170
9592
  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 +9603,8 @@ program.command("join <invite-url>").description("join a ClearGate workspace usi
9181
9603
  ...cmdOpts.code !== void 0 ? { code: cmdOpts.code } : {}
9182
9604
  });
9183
9605
  });
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 });
9606
+ 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) => {
9607
+ await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false, pin: opts.pin });
9186
9608
  });
9187
9609
  program.command("whoami").description("print the currently authenticated agent identity").action(async () => {
9188
9610
  const { whoamiHandler: whoamiHandler2 } = await Promise.resolve().then(() => (init_whoami(), whoami_exports));
@@ -9304,14 +9726,20 @@ admin.command("bootstrap-root <handle>").description("seed the first root admin
9304
9726
  const { bootstrapRootHandler: bootstrapRootHandler2 } = await Promise.resolve().then(() => (init_bootstrap_root(), bootstrap_root_exports));
9305
9727
  await bootstrapRootHandler2({ handle, databaseUrl: opts.databaseUrl, force: opts.force ?? false });
9306
9728
  });
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", [
9729
+ 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
9730
  "",
9309
9731
  "Modes (mutually exclusive):",
9310
9732
  " --check-scaffold Compute drift for all tracked scaffold files.",
9311
9733
  " Writes .cleargate/.drift-state.json.",
9312
9734
  " --session-start List blocked pending-sync items (\u226410, \u2264100 tokens).",
9313
9735
  " --pricing <file> Compute USD estimate from a work item's draft_tokens.",
9314
- " (default) Print a minimal hook-config health report."
9736
+ " --can-edit <file> Check if editing a file requires a planning work item.",
9737
+ " (default) Print a minimal hook-config health report.",
9738
+ "",
9739
+ "Exit codes:",
9740
+ " 0 Clean \u2014 no blockers, no config errors.",
9741
+ " 1 Blocked items or advisory issues \u2014 see stdout.",
9742
+ " 2 ClearGate misconfigured or partially installed \u2014 see stdout for remediation."
9315
9743
  ].join("\n")).action(async (opts) => {
9316
9744
  await doctorHandler({
9317
9745
  checkScaffold: opts.checkScaffold,
@@ -9319,8 +9747,10 @@ program.command("doctor").description("diagnose scaffold drift, hook health, blo
9319
9747
  sessionStart: opts.sessionStart,
9320
9748
  pricing: !!opts.pricing,
9321
9749
  pricingFile: opts.pricing,
9750
+ canEdit: !!opts.canEdit,
9751
+ canEditFile: opts.canEdit,
9322
9752
  verbose: opts.verbose
9323
- });
9753
+ }, opts.cwd ? { cwd: opts.cwd } : void 0);
9324
9754
  });
9325
9755
  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
9756
  "",
@@ -9342,7 +9772,7 @@ program.command("uninstall").description("remove ClearGate scaffold from a proje
9342
9772
  "",
9343
9773
  "Always removed (no prompt): .claude/agents/*.md, ClearGate hooks,",
9344
9774
  " .claude/skills/flashcard/, CLAUDE.md CLEARGATE block,",
9345
- " @cleargate/cli from package.json, .install-manifest.json, .drift-state.json.",
9775
+ " `cleargate` from package.json, .install-manifest.json, .drift-state.json.",
9346
9776
  "",
9347
9777
  "Non-git targets: uncommitted-changes check is skipped silently."
9348
9778
  ].join("\n")).action(async (opts) => {
@@ -9395,5 +9825,9 @@ program.command("sync-log").description("filter and print sync-log entries").opt
9395
9825
  limit: opts.limit !== void 0 ? parseInt(opts.limit, 10) : 50
9396
9826
  });
9397
9827
  });
9828
+ var hotfix = program.command("hotfix").description("hotfix lane commands (off-sprint trivial fix scaffolding)");
9829
+ hotfix.command("new <slug>").description("scaffold a new HOTFIX-NNN_<slug>.md in pending-sync/").action((slug) => {
9830
+ hotfixNewHandler({ slug });
9831
+ });
9398
9832
  void program.parseAsync(process.argv);
9399
9833
  //# sourceMappingURL=cli.cjs.map