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.js CHANGED
@@ -14,7 +14,7 @@ import { Command } from "commander";
14
14
  // package.json
15
15
  var package_default = {
16
16
  name: "cleargate",
17
- version: "0.5.0",
17
+ version: "0.6.0",
18
18
  private: false,
19
19
  type: "module",
20
20
  description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, four-agent loop (architect/developer/qa/reporter), Karpathy-style awareness wiki.",
@@ -1275,10 +1275,16 @@ async function stampHandler(file, opts, cli) {
1275
1275
  import * as fs13 from "fs";
1276
1276
  import * as path13 from "path";
1277
1277
  import { fileURLToPath as fileURLToPath5 } from "url";
1278
+ import { spawnSync as spawnSync3 } from "child_process";
1278
1279
 
1279
1280
  // src/init/copy-payload.ts
1280
1281
  import * as fs5 from "fs";
1281
1282
  import * as path4 from "path";
1283
+ var PIN_PLACEHOLDER = "__CLEARGATE_VERSION__";
1284
+ var HOOK_FILES_WITH_PIN = /* @__PURE__ */ new Set([
1285
+ ".claude/hooks/stamp-and-gate.sh",
1286
+ ".claude/hooks/session-start.sh"
1287
+ ]);
1282
1288
  function listFilesRecursive(dir) {
1283
1289
  const results = [];
1284
1290
  function walk(current, rel) {
@@ -1306,10 +1312,15 @@ function copyPayload(payloadDir, targetCwd, opts) {
1306
1312
  const srcPath = path4.join(payloadDir, relPath);
1307
1313
  const dstPath = path4.join(targetCwd, relPath);
1308
1314
  fs5.mkdirSync(path4.dirname(dstPath), { recursive: true });
1309
- const srcContent = fs5.readFileSync(srcPath);
1315
+ let srcContent = fs5.readFileSync(srcPath);
1316
+ if (opts.pinVersion && HOOK_FILES_WITH_PIN.has(relPath)) {
1317
+ const text = srcContent.toString("utf8").replaceAll(PIN_PLACEHOLDER, opts.pinVersion);
1318
+ srcContent = text;
1319
+ }
1320
+ const srcBuffer = typeof srcContent === "string" ? Buffer.from(srcContent, "utf8") : srcContent;
1310
1321
  if (fs5.existsSync(dstPath)) {
1311
1322
  const dstContent = fs5.readFileSync(dstPath);
1312
- if (srcContent.equals(dstContent)) {
1323
+ if (srcBuffer.equals(dstContent)) {
1313
1324
  report.skipped++;
1314
1325
  report.actions.push({ action: "skipped", relPath });
1315
1326
  continue;
@@ -1319,11 +1330,11 @@ function copyPayload(payloadDir, targetCwd, opts) {
1319
1330
  report.actions.push({ action: "skipped", relPath });
1320
1331
  continue;
1321
1332
  }
1322
- fs5.writeFileSync(dstPath, srcContent);
1333
+ fs5.writeFileSync(dstPath, srcBuffer);
1323
1334
  report.overwritten++;
1324
1335
  report.actions.push({ action: "overwritten", relPath });
1325
1336
  } else {
1326
- fs5.writeFileSync(dstPath, srcContent);
1337
+ fs5.writeFileSync(dstPath, srcBuffer);
1327
1338
  report.created++;
1328
1339
  report.actions.push({ action: "created", relPath });
1329
1340
  }
@@ -2156,7 +2167,7 @@ async function readDriftState(projectRoot) {
2156
2167
  import * as readline3 from "readline";
2157
2168
  async function promptYesNo(question, defaultYes, opts) {
2158
2169
  const stdoutFn = opts?.stdout ?? ((s) => process.stdout.write(s));
2159
- stdoutFn(question + "\n");
2170
+ stdoutFn(question + " ");
2160
2171
  const inputStream = opts?.stdin ?? process.stdin;
2161
2172
  return new Promise((resolve14) => {
2162
2173
  const rl = readline3.createInterface({
@@ -2187,7 +2198,7 @@ async function promptYesNo(question, defaultYes, opts) {
2187
2198
  }
2188
2199
  async function promptEmail(question, defaultValue, opts) {
2189
2200
  const stdoutFn = opts?.stdout ?? ((s) => process.stdout.write(s));
2190
- stdoutFn(question + "\n");
2201
+ stdoutFn(question + " ");
2191
2202
  const inputStream = opts?.stdin ?? process.stdin;
2192
2203
  return new Promise((resolve14) => {
2193
2204
  const rl = readline3.createInterface({
@@ -2279,6 +2290,17 @@ function resolveIdentity(projectRoot, opts = {}) {
2279
2290
  // src/commands/init.ts
2280
2291
  var HOOK_ADDITION = {
2281
2292
  hooks: {
2293
+ PreToolUse: [
2294
+ {
2295
+ matcher: "Edit|Write",
2296
+ hooks: [
2297
+ {
2298
+ type: "command",
2299
+ command: "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-edit-gate.sh"
2300
+ }
2301
+ ]
2302
+ }
2303
+ ],
2282
2304
  PostToolUse: [
2283
2305
  {
2284
2306
  matcher: "Edit|Write",
@@ -2315,6 +2337,17 @@ function writeAtomic(filePath, content) {
2315
2337
  fs13.writeFileSync(tmpPath, content, "utf8");
2316
2338
  fs13.renameSync(tmpPath, filePath);
2317
2339
  }
2340
+ function readPackageVersion(packageJsonPath) {
2341
+ try {
2342
+ const raw = fs13.readFileSync(packageJsonPath, "utf8");
2343
+ const pkg = JSON.parse(raw);
2344
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
2345
+ return pkg.version;
2346
+ }
2347
+ } catch {
2348
+ }
2349
+ return null;
2350
+ }
2318
2351
  async function initHandler(opts = {}) {
2319
2352
  const cwd = opts.cwd ?? process.cwd();
2320
2353
  const force = opts.force ?? false;
@@ -2325,6 +2358,7 @@ async function initHandler(opts = {}) {
2325
2358
  const runWikiBuild = opts.runWikiBuild ?? wikiBuildHandler;
2326
2359
  const promptYesNoFn = opts.promptYesNo ?? promptYesNo;
2327
2360
  const promptEmailFn = opts.promptEmail ?? promptEmail;
2361
+ const spawnSyncFn = opts.spawnSyncFn ?? spawnSync3;
2328
2362
  if (!fs13.existsSync(cwd)) {
2329
2363
  stderr(`[cleargate init] ERROR: target directory does not exist: ${cwd}
2330
2364
  `);
@@ -2386,7 +2420,14 @@ async function initHandler(opts = {}) {
2386
2420
  }
2387
2421
  }
2388
2422
  }
2389
- const copyReport = copyPayload(payloadDir, cwd, { force });
2423
+ let pinVersion;
2424
+ if (opts.pin) {
2425
+ pinVersion = opts.pin;
2426
+ } else {
2427
+ const payloadParent = path13.resolve(payloadDir, "..", "..");
2428
+ pinVersion = readPackageVersion(path13.join(payloadParent, "package.json")) ?? readPackageVersion(path13.join(path13.dirname(fileURLToPath5(import.meta.url)), "..", "package.json")) ?? "latest";
2429
+ }
2430
+ const copyReport = copyPayload(payloadDir, cwd, { force, pinVersion });
2390
2431
  for (const action of copyReport.actions) {
2391
2432
  const verb = action.action === "created" ? "Created" : action.action === "overwritten" ? "Overwritten" : "Skipped (exists)";
2392
2433
  stdout(`[cleargate init] ${verb} ${action.relPath}
@@ -2467,6 +2508,47 @@ async function initHandler(opts = {}) {
2467
2508
  `);
2468
2509
  }
2469
2510
  }
2511
+ {
2512
+ const distCliPath = path13.join(cwd, "cleargate-cli", "dist", "cli.js");
2513
+ let branch = null;
2514
+ let branchLabel = "";
2515
+ if (fs13.existsSync(distCliPath)) {
2516
+ branch = { cmd: "node", args: [distCliPath, "--version"] };
2517
+ branchLabel = `local dist (${distCliPath})`;
2518
+ } else {
2519
+ const whichResult = spawnSyncFn("command", ["-v", "cleargate"], {
2520
+ shell: true,
2521
+ encoding: "utf8",
2522
+ timeout: 3e3
2523
+ });
2524
+ if (whichResult.status === 0) {
2525
+ branch = { cmd: "cleargate", args: ["--version"] };
2526
+ branchLabel = "PATH (global install)";
2527
+ } else {
2528
+ branch = { cmd: "npx", args: ["-y", `cleargate@${pinVersion}`, "--version"] };
2529
+ branchLabel = `npx cleargate@${pinVersion} (cold-start ~600ms first call)`;
2530
+ }
2531
+ }
2532
+ if (branch !== null) {
2533
+ const probeResult = spawnSyncFn(branch.cmd, branch.args, {
2534
+ encoding: "utf8",
2535
+ timeout: 15e3
2536
+ });
2537
+ if (probeResult.status === 0) {
2538
+ stdout(`[cleargate init] \u{1F7E2} cleargate CLI resolved via ${branchLabel}
2539
+ `);
2540
+ } else {
2541
+ stdout(
2542
+ `[cleargate init] \u{1F534} cleargate CLI: not resolvable \u2014 hooks will no-op.
2543
+ [cleargate init] Attempted: ${branchLabel}
2544
+ [cleargate init] Fix: npm i -g cleargate@${pinVersion} or npx cleargate@${pinVersion} doctor
2545
+ `
2546
+ );
2547
+ exit(1);
2548
+ return;
2549
+ }
2550
+ }
2551
+ }
2470
2552
  const existingParticipant = readParticipant(cwd);
2471
2553
  if (existingParticipant === null) {
2472
2554
  const identityOpts = opts.identityOpts ?? {};
@@ -2484,8 +2566,10 @@ async function initHandler(opts = {}) {
2484
2566
  stdout(`[cleargate init] Participant identity: ${finalEmail} (inferred)
2485
2567
  `);
2486
2568
  } else {
2487
- const defaultEmail = gitEmail ?? "user@localhost";
2488
- const question = `[cleargate init] Participant email [${defaultEmail}]:`;
2569
+ const isNoreply = gitEmail !== null && /@users\.noreply\.github\.com$/i.test(gitEmail);
2570
+ const defaultEmail = gitEmail !== null && !isNoreply ? gitEmail : "user@localhost";
2571
+ stdout("\n");
2572
+ const question = `Participant email (press Enter for default) [${defaultEmail}]:`;
2489
2573
  const answer = await promptEmailFn(question, defaultEmail);
2490
2574
  await writeParticipant(cwd, answer, "prompted", now);
2491
2575
  stdout(`[cleargate init] Participant identity: ${answer} (prompted)
@@ -2502,7 +2586,7 @@ async function initHandler(opts = {}) {
2502
2586
  // src/commands/wiki-ingest.ts
2503
2587
  import * as fs14 from "fs";
2504
2588
  import * as path14 from "path";
2505
- import { spawnSync as spawnSync3 } from "child_process";
2589
+ import { spawnSync as spawnSync4 } from "child_process";
2506
2590
  var EXCLUDED_SUFFIXES2 = [
2507
2591
  ".cleargate/knowledge/",
2508
2592
  ".cleargate/templates/",
@@ -2654,7 +2738,7 @@ function checkContentUnchanged(absRawPath, sha, relRawPath, gitRunner) {
2654
2738
  }
2655
2739
  }
2656
2740
  function defaultGitRunner(cmd, args) {
2657
- const result = spawnSync3(cmd, args, { encoding: "utf8" });
2741
+ const result = spawnSync4(cmd, args, { encoding: "utf8" });
2658
2742
  if (result.status !== 0) return "\0__NONZERO__";
2659
2743
  return result.stdout ?? "";
2660
2744
  }
@@ -2903,7 +2987,7 @@ function loadWikiPages(wikiRoot) {
2903
2987
  // src/wiki/lint-checks.ts
2904
2988
  import * as fs16 from "fs";
2905
2989
  import * as path16 from "path";
2906
- import { spawnSync as spawnSync4 } from "child_process";
2990
+ import { spawnSync as spawnSync5 } from "child_process";
2907
2991
  import yaml3 from "js-yaml";
2908
2992
 
2909
2993
  // src/lib/work-item-type.ts
@@ -2998,7 +3082,7 @@ function checkStaleCommit(page, repoRoot, gitRunner) {
2998
3082
  if (gitRunner) {
2999
3083
  currentSha = gitRunner("git", ["log", "-1", "--format=%H", "--", rawPath]).trim();
3000
3084
  } else {
3001
- const result = spawnSync4("git", ["log", "-1", "--format=%H", "--", rawPath], {
3085
+ const result = spawnSync5("git", ["log", "-1", "--format=%H", "--", rawPath], {
3002
3086
  encoding: "utf8",
3003
3087
  cwd: repoRoot
3004
3088
  });
@@ -3739,6 +3823,7 @@ function applyStatusFix(rawText, newStatus) {
3739
3823
  // src/commands/doctor.ts
3740
3824
  import * as fs20 from "fs";
3741
3825
  import * as path21 from "path";
3826
+ import { spawnSync as spawnSync6 } from "child_process";
3742
3827
 
3743
3828
  // src/lib/pricing.ts
3744
3829
  var PRICING_TABLE = {
@@ -3787,6 +3872,7 @@ function selectMode(flags) {
3787
3872
  if (flags.checkScaffold) modes.push("check-scaffold");
3788
3873
  if (flags.sessionStart) modes.push("session-start");
3789
3874
  if (flags.pricing) modes.push("pricing");
3875
+ if (flags.canEdit) modes.push("can-edit");
3790
3876
  if (modes.length > 1) {
3791
3877
  throw new Error(
3792
3878
  `cleargate doctor: mutually exclusive flags set: ${modes.join(", ")}. Use only one mode flag at a time.`
@@ -3811,7 +3897,18 @@ function parseHookLogLine(line) {
3811
3897
  file: m[5].trim()
3812
3898
  };
3813
3899
  }
3814
- function runHookHealth(stdout, cwd, now) {
3900
+ function runHookHealth(stdout, cwd, now, outcome) {
3901
+ const cleargateDir = path21.join(cwd, ".cleargate");
3902
+ if (!fs20.existsSync(cleargateDir)) {
3903
+ stdout("cleargate misconfigured: no .cleargate/ found. Run: cleargate init");
3904
+ if (outcome) outcome.configError = true;
3905
+ return;
3906
+ }
3907
+ const manifestPath = path21.join(cwd, "cleargate-planning", "MANIFEST.json");
3908
+ if (!fs20.existsSync(manifestPath)) {
3909
+ stdout(`cleargate misconfigured: cleargate-planning/MANIFEST.json not found. Run: cleargate init`);
3910
+ if (outcome) outcome.configError = true;
3911
+ }
3815
3912
  const settingsPath = path21.join(cwd, ".claude", "settings.json");
3816
3913
  if (!fs20.existsSync(settingsPath)) {
3817
3914
  stdout("[doctor] No .claude/settings.json found \u2014 hook config unavailable.");
@@ -3948,7 +4045,57 @@ function parseCachedGateResult2(raw) {
3948
4045
  failing_criteria: parsed.failing_criteria ?? []
3949
4046
  };
3950
4047
  }
3951
- async function runSessionStart(cwd, stdout) {
4048
+ function emitResolverStatusLine(cwd, stdout) {
4049
+ const distCliPath = path21.join(cwd, "cleargate-cli", "dist", "cli.js");
4050
+ if (fs20.existsSync(distCliPath)) {
4051
+ stdout(`cleargate CLI: local dist \u2014 ${distCliPath}`);
4052
+ return;
4053
+ }
4054
+ const whichResult = spawnSync6("command", ["-v", "cleargate"], {
4055
+ shell: true,
4056
+ encoding: "utf8",
4057
+ timeout: 3e3
4058
+ });
4059
+ if (whichResult.status === 0) {
4060
+ stdout("cleargate CLI: PATH (global install) \u2014 cleargate");
4061
+ return;
4062
+ }
4063
+ let pinVersion = "unknown";
4064
+ const hookPath = path21.join(cwd, ".claude", "hooks", "stamp-and-gate.sh");
4065
+ if (fs20.existsSync(hookPath)) {
4066
+ try {
4067
+ const hookContent = fs20.readFileSync(hookPath, "utf-8");
4068
+ const pinMatch = hookContent.match(/^#\s*cleargate-pin:\s*(\S+)\s*$/m);
4069
+ if (pinMatch?.[1]) {
4070
+ pinVersion = pinMatch[1];
4071
+ } else {
4072
+ const npxMatch = hookContent.match(/@cleargate\/cli@([^\s"']+)/);
4073
+ if (npxMatch?.[1]) pinVersion = npxMatch[1];
4074
+ }
4075
+ } catch {
4076
+ }
4077
+ }
4078
+ if (pinVersion === "unknown") {
4079
+ stdout("cleargate CLI: \u{1F534} not resolvable \u2014 hooks will no-op. Fix: npm i -g cleargate or npx cleargate doctor");
4080
+ } else {
4081
+ stdout(`cleargate CLI: npx @cleargate/cli@${pinVersion} (cold-start ~600ms first call)`);
4082
+ }
4083
+ }
4084
+ var PLANNING_FIRST_REMINDER = `Triage first, draft second:
4085
+ Before any Edit/Write that creates user-facing code, you must:
4086
+ (1) classify the request (Epic / Story / CR / Bug),
4087
+ (2) draft a work item under .cleargate/delivery/pending-sync/ from .cleargate/templates/,
4088
+ (3) halt at Gate 1 (Proposal approval) for human sign-off.
4089
+ Bypass this only if the user has explicitly waived planning in this conversation.`;
4090
+ async function runSessionStart(cwd, stdout, outcome) {
4091
+ const resolverLines = [];
4092
+ emitResolverStatusLine(cwd, (line) => {
4093
+ stdout(line);
4094
+ resolverLines.push(line);
4095
+ });
4096
+ if (outcome && resolverLines.some((l) => l.includes("\u{1F534}"))) {
4097
+ outcome.configError = true;
4098
+ }
3952
4099
  const pendingSyncDir = path21.join(cwd, ".cleargate", "delivery", "pending-sync");
3953
4100
  let files;
3954
4101
  try {
@@ -3957,6 +4104,7 @@ async function runSessionStart(cwd, stdout) {
3957
4104
  return;
3958
4105
  }
3959
4106
  const blocked = [];
4107
+ let hasApprovedStory = false;
3960
4108
  for (const filePath of files) {
3961
4109
  let raw;
3962
4110
  try {
@@ -3971,6 +4119,9 @@ async function runSessionStart(cwd, stdout) {
3971
4119
  } catch {
3972
4120
  continue;
3973
4121
  }
4122
+ if (fm["approved"] === true) {
4123
+ hasApprovedStory = true;
4124
+ }
3974
4125
  const gate2 = parseCachedGateResult2(fm["cached_gate_result"]);
3975
4126
  if (!gate2 || gate2.pass !== false) continue;
3976
4127
  const idKeys = ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id", "sprint_id"];
@@ -3988,9 +4139,19 @@ async function runSessionStart(cwd, stdout) {
3988
4139
  const firstCriterionId = gate2.failing_criteria.length > 0 ? gate2.failing_criteria[0]?.id ?? "" : "";
3989
4140
  blocked.push({ id: itemId, firstCriterionId });
3990
4141
  }
4142
+ const activesentinel = path21.join(cwd, ".cleargate", "sprint-runs", ".active");
4143
+ const sprintActive = fs20.existsSync(activesentinel);
4144
+ const shouldRemind = !hasApprovedStory && !sprintActive;
4145
+ if (shouldRemind) {
4146
+ stdout(PLANNING_FIRST_REMINDER);
4147
+ if (blocked.length > 0) {
4148
+ stdout("");
4149
+ }
4150
+ }
3991
4151
  if (blocked.length === 0) {
3992
4152
  return;
3993
4153
  }
4154
+ if (outcome) outcome.blocker = true;
3994
4155
  const overflow = blocked.length > SESSION_START_MAX_ITEMS ? blocked.length - SESSION_START_MAX_ITEMS : 0;
3995
4156
  const visible = blocked.slice(0, SESSION_START_MAX_ITEMS);
3996
4157
  const lines = [`${blocked.length} items blocked:`];
@@ -4007,10 +4168,11 @@ async function runSessionStart(cwd, stdout) {
4007
4168
  }
4008
4169
  stdout(output);
4009
4170
  }
4010
- async function runPricing(filePath, cwd, stdout, stderr, exit) {
4171
+ async function runPricing(filePath, cwd, stdout, stderr, exit, outcome) {
4011
4172
  if (!filePath) {
4012
4173
  stderr("cleargate doctor --pricing: missing <file> argument");
4013
- exit(1);
4174
+ if (outcome) outcome.configError = true;
4175
+ exit(2);
4014
4176
  return;
4015
4177
  }
4016
4178
  const absPath = path21.isAbsolute(filePath) ? filePath : path21.resolve(cwd, filePath);
@@ -4019,12 +4181,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4019
4181
  raw = fs20.readFileSync(absPath, "utf-8");
4020
4182
  } catch {
4021
4183
  stderr(`cleargate doctor --pricing: cannot read file: ${absPath}`);
4022
- exit(1);
4184
+ if (outcome) outcome.configError = true;
4185
+ exit(2);
4023
4186
  return;
4024
4187
  }
4025
4188
  if (!raw.trimStart().startsWith("---")) {
4026
4189
  stderr(`cleargate doctor --pricing: file has no frontmatter: ${absPath}`);
4027
- exit(1);
4190
+ if (outcome) outcome.configError = true;
4191
+ exit(2);
4028
4192
  return;
4029
4193
  }
4030
4194
  let fm;
@@ -4032,12 +4196,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4032
4196
  fm = parseFrontmatter(raw).fm;
4033
4197
  } catch {
4034
4198
  stderr(`cleargate doctor --pricing: cannot parse frontmatter in: ${absPath}`);
4035
- exit(1);
4199
+ if (outcome) outcome.configError = true;
4200
+ exit(2);
4036
4201
  return;
4037
4202
  }
4038
4203
  const draftTokensRaw = fm["draft_tokens"];
4039
4204
  if (!draftTokensRaw) {
4040
4205
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4206
+ if (outcome) outcome.blocker = true;
4041
4207
  exit(1);
4042
4208
  return;
4043
4209
  }
@@ -4049,16 +4215,19 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4049
4215
  draftTokens = JSON.parse(draftTokensRaw);
4050
4216
  } catch {
4051
4217
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4218
+ if (outcome) outcome.blocker = true;
4052
4219
  exit(1);
4053
4220
  return;
4054
4221
  }
4055
4222
  } else {
4056
4223
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4224
+ if (outcome) outcome.blocker = true;
4057
4225
  exit(1);
4058
4226
  return;
4059
4227
  }
4060
4228
  if (draftTokens.input === null && draftTokens.output === null && draftTokens.cache_read === null && draftTokens.cache_creation === null) {
4061
4229
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
4230
+ if (outcome) outcome.blocker = true;
4062
4231
  exit(1);
4063
4232
  return;
4064
4233
  }
@@ -4076,18 +4245,95 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
4076
4245
  `${fileName}: ${model} \u2014 input:${input} output:${output} cache_read:${cacheRead} cache_creation:${cacheCreation} \u2248 $${usd.toFixed(4)}`
4077
4246
  );
4078
4247
  }
4248
+ function globMatch(pattern, filePath) {
4249
+ const normalPattern = pattern.replace(/\\/g, "/");
4250
+ const normalFile = filePath.replace(/\\/g, "/");
4251
+ const regexStr = normalPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
4252
+ const re = new RegExp(`^${regexStr}$`);
4253
+ return re.test(normalFile);
4254
+ }
4255
+ async function runCanEdit(filePath, cwd, stdout, exit, outcome) {
4256
+ const activeSentinel = path21.join(cwd, ".cleargate", "sprint-runs", ".active");
4257
+ if (fs20.existsSync(activeSentinel)) {
4258
+ stdout("allowed: sprint active");
4259
+ return;
4260
+ }
4261
+ const pendingSyncDir = path21.join(cwd, ".cleargate", "delivery", "pending-sync");
4262
+ let files;
4263
+ try {
4264
+ files = fs20.readdirSync(pendingSyncDir).filter((f) => f.endsWith(".md")).map((f) => path21.join(pendingSyncDir, f));
4265
+ } catch {
4266
+ stdout("blocked: no_approved_stories");
4267
+ if (outcome) outcome.blocker = true;
4268
+ exit(1);
4269
+ return;
4270
+ }
4271
+ let hasApprovedStory = false;
4272
+ let coveredByStory = false;
4273
+ for (const storyPath of files) {
4274
+ let raw;
4275
+ try {
4276
+ raw = fs20.readFileSync(storyPath, "utf-8");
4277
+ } catch {
4278
+ continue;
4279
+ }
4280
+ if (!raw.trimStart().startsWith("---")) continue;
4281
+ let fm;
4282
+ try {
4283
+ fm = parseFrontmatter(raw).fm;
4284
+ } catch {
4285
+ continue;
4286
+ }
4287
+ if (fm["approved"] !== true) continue;
4288
+ hasApprovedStory = true;
4289
+ const implFilesRaw = fm["implementation_files"];
4290
+ if (implFilesRaw === void 0 || implFilesRaw === null) {
4291
+ coveredByStory = true;
4292
+ break;
4293
+ }
4294
+ if (Array.isArray(implFilesRaw)) {
4295
+ for (const pattern of implFilesRaw) {
4296
+ if (typeof pattern !== "string") continue;
4297
+ if (globMatch(pattern, filePath)) {
4298
+ coveredByStory = true;
4299
+ break;
4300
+ }
4301
+ }
4302
+ }
4303
+ if (coveredByStory) break;
4304
+ }
4305
+ if (!hasApprovedStory) {
4306
+ stdout("blocked: no_approved_stories");
4307
+ if (outcome) outcome.blocker = true;
4308
+ exit(1);
4309
+ return;
4310
+ }
4311
+ if (!coveredByStory) {
4312
+ stdout("blocked: file_not_in_implementation_files");
4313
+ if (outcome) outcome.blocker = true;
4314
+ exit(1);
4315
+ return;
4316
+ }
4317
+ stdout("allowed");
4318
+ }
4079
4319
  async function doctorHandler(flags, cli) {
4080
4320
  const cwd = cli?.cwd ?? process.cwd();
4081
4321
  const now = cli?.now ? cli.now() : /* @__PURE__ */ new Date();
4082
4322
  const stdout = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
4083
4323
  const stderr = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
4084
4324
  const exit = cli?.exit ?? ((code) => process.exit(code));
4325
+ const outcome = { configError: false, blocker: false };
4326
+ let exitedEarly = false;
4327
+ const wrappedExit = (code) => {
4328
+ exitedEarly = true;
4329
+ return exit(code);
4330
+ };
4085
4331
  let mode;
4086
4332
  try {
4087
4333
  mode = selectMode(flags);
4088
4334
  } catch (err) {
4089
4335
  stderr(err.message);
4090
- exit(1);
4336
+ exit(2);
4091
4337
  return;
4092
4338
  }
4093
4339
  switch (mode) {
@@ -4095,26 +4341,38 @@ async function doctorHandler(flags, cli) {
4095
4341
  await runCheckScaffold(flags, cli ?? {}, cwd, now, stdout, stderr);
4096
4342
  break;
4097
4343
  case "hook-health":
4098
- runHookHealth(stdout, cwd, now);
4344
+ runHookHealth(stdout, cwd, now, outcome);
4099
4345
  break;
4100
4346
  case "session-start":
4101
- await runSessionStart(cwd, stdout);
4347
+ await runSessionStart(cwd, stdout, outcome);
4102
4348
  break;
4103
4349
  case "pricing":
4104
- await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr, exit);
4350
+ await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr, wrappedExit, outcome);
4351
+ break;
4352
+ case "can-edit":
4353
+ await runCanEdit(flags.canEditFile ?? "", cwd, stdout, wrappedExit, outcome);
4105
4354
  break;
4106
4355
  default: {
4107
4356
  const exhaustiveCheck = mode;
4108
4357
  stderr(`cleargate doctor: unknown mode '${String(exhaustiveCheck)}'`);
4109
- exit(1);
4358
+ exit(2);
4359
+ return;
4110
4360
  }
4111
4361
  }
4362
+ if (exitedEarly) return;
4363
+ if (outcome.configError) {
4364
+ exit(2);
4365
+ } else if (outcome.blocker) {
4366
+ exit(1);
4367
+ } else {
4368
+ exit(0);
4369
+ }
4112
4370
  }
4113
4371
 
4114
4372
  // src/commands/gate.ts
4115
4373
  import * as fs24 from "fs";
4116
4374
  import * as path24 from "path";
4117
- import { spawnSync as spawnSync5 } from "child_process";
4375
+ import { spawnSync as spawnSync7 } from "child_process";
4118
4376
 
4119
4377
  // src/commands/execution-mode.ts
4120
4378
  import * as fs21 from "fs";
@@ -4221,6 +4479,14 @@ function parsePredicate(src) {
4221
4479
  const value = parseValue(rawVal);
4222
4480
  return { kind: "frontmatter", ref, field, op, value };
4223
4481
  }
4482
+ const markerNotMatch = s.match(/^body does not contain marker ['"]([A-Z]+)['"]$/);
4483
+ if (markerNotMatch) {
4484
+ const marker = markerNotMatch[1];
4485
+ if (marker !== "TBD" && marker !== "TODO" && marker !== "FIXME") {
4486
+ throw new Error(`unsupported predicate shape: ${src}`);
4487
+ }
4488
+ return { kind: "marker-absence", marker };
4489
+ }
4224
4490
  const bodyNotMatch = s.match(/^body does not contain ['"](.+)['"]$/);
4225
4491
  if (bodyNotMatch) {
4226
4492
  return { kind: "body-contains", needle: bodyNotMatch[1], negated: true };
@@ -4276,6 +4542,8 @@ function evaluate(predicate, doc, opts) {
4276
4542
  return evalFrontmatter(parsed, doc, projectRoot);
4277
4543
  case "body-contains":
4278
4544
  return evalBodyContains(parsed, doc);
4545
+ case "marker-absence":
4546
+ return evalMarkerAbsence(parsed, doc);
4279
4547
  case "section":
4280
4548
  return evalSection(parsed, doc);
4281
4549
  case "file-exists":
@@ -4298,6 +4566,26 @@ function evalFrontmatter(parsed, doc, projectRoot) {
4298
4566
  detail: `frontmatter key '${parsed.ref}' is missing or null in ${doc.absPath}`
4299
4567
  };
4300
4568
  }
4569
+ const refStr = String(refVal);
4570
+ const looksLikeProse = refStr.length > 200 || /[ —–:()\n]/.test(refStr);
4571
+ if (looksLikeProse) {
4572
+ const waiver = doc.fm["proposal_gate_waiver"];
4573
+ const hasExplicitWaiver = waiver !== null && waiver !== void 0 && waiver !== false && String(waiver).trim() !== "" && String(waiver).trim() !== "false";
4574
+ const approvedBy = doc.fm["approved_by"];
4575
+ const approvedAt = doc.fm["approved_at"];
4576
+ const hasApprovalFields = approvedBy !== null && approvedBy !== void 0 && String(approvedBy).trim() !== "" && approvedAt !== null && approvedAt !== void 0 && String(approvedAt).trim() !== "";
4577
+ const hasWaiver = hasExplicitWaiver || hasApprovalFields;
4578
+ if (hasWaiver) {
4579
+ return {
4580
+ pass: true,
4581
+ detail: `context_source is prose; proposal-gate waiver per frontmatter approved_by/approved_at`
4582
+ };
4583
+ }
4584
+ return {
4585
+ pass: false,
4586
+ detail: `context_source is prose but no proposal_gate_waiver (approved_by + approved_at) found in frontmatter`
4587
+ };
4588
+ }
4301
4589
  const linkedPath = resolveLinkedPath(String(refVal), doc.absPath, projectRoot);
4302
4590
  if (!linkedPath) {
4303
4591
  return {
@@ -4422,6 +4710,38 @@ function evalBodyContains(parsed, doc) {
4422
4710
  return { pass: false, detail: `'${needle}' not found in body` };
4423
4711
  }
4424
4712
  }
4713
+ function evalMarkerAbsence(parsed, doc) {
4714
+ const { marker } = parsed;
4715
+ const lines = doc.body.split("\n");
4716
+ const templateSelfRefRe = /^\s*-\s*\[[x ]\]\s*0\s*"TBDs?"\s*exist/i;
4717
+ const markerRe = new RegExp(
4718
+ `(?:^|(?<=\\())${marker}(?=:)|\\(${marker}\\)|\\[${marker}\\]|(?<=//\\s*)${marker}(?!\\w)|(?<=#\\s*)${marker}(?!\\w)`,
4719
+ // # MARKER (comment)
4720
+ "g"
4721
+ );
4722
+ const bareLineRe = new RegExp(`^${marker}$`);
4723
+ const violations = [];
4724
+ for (let i = 0; i < lines.length; i++) {
4725
+ const line = lines[i];
4726
+ if (templateSelfRefRe.test(line)) continue;
4727
+ const trimmed = line.trim();
4728
+ if (bareLineRe.test(trimmed)) {
4729
+ violations.push(i + 1);
4730
+ continue;
4731
+ }
4732
+ markerRe.lastIndex = 0;
4733
+ if (markerRe.test(line)) {
4734
+ violations.push(i + 1);
4735
+ }
4736
+ }
4737
+ if (violations.length > 0) {
4738
+ return {
4739
+ pass: false,
4740
+ detail: `${violations.length} marker occurrence${violations.length === 1 ? "" : "s"} of '${marker}' at line${violations.length === 1 ? "" : "s"} ${violations.join(", ")}`
4741
+ };
4742
+ }
4743
+ return { pass: true, detail: `no '${marker}' markers found in body` };
4744
+ }
4425
4745
  function evalSection(parsed, doc) {
4426
4746
  const body = doc.body;
4427
4747
  const rawParts = body.split(/^(?=## )/m);
@@ -4789,7 +5109,7 @@ function gateQaHandler(opts, cli) {
4789
5109
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
4790
5110
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
4791
5111
  const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
4792
- const spawnFn = cli?.spawnFn ?? spawnSync5;
5112
+ const spawnFn = cli?.spawnFn ?? spawnSync7;
4793
5113
  const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
4794
5114
  const mode = readSprintExecutionMode(sprintId, {
4795
5115
  sprintFilePath: cli?.sprintFilePath,
@@ -4815,7 +5135,7 @@ function gateArchHandler(opts, cli) {
4815
5135
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
4816
5136
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
4817
5137
  const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
4818
- const spawnFn = cli?.spawnFn ?? spawnSync5;
5138
+ const spawnFn = cli?.spawnFn ?? spawnSync7;
4819
5139
  const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
4820
5140
  const mode = readSprintExecutionMode(sprintId, {
4821
5141
  sprintFilePath: cli?.sprintFilePath,
@@ -4839,13 +5159,13 @@ function gateArchHandler(opts, cli) {
4839
5159
  }
4840
5160
 
4841
5161
  // src/commands/gate-run.ts
4842
- import { spawnSync as spawnSync6 } from "child_process";
5162
+ import { spawnSync as spawnSync8 } from "child_process";
4843
5163
  var KNOWN_GATES = ["precommit", "test", "typecheck", "lint"];
4844
5164
  function gateRunHandler(name, opts, cli) {
4845
5165
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
4846
5166
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
4847
5167
  const exitFn = cli?.exit ?? ((code) => process.exit(code));
4848
- const spawnFn = cli?.spawnFn ?? spawnSync6;
5168
+ const spawnFn = cli?.spawnFn ?? spawnSync8;
4849
5169
  const cwd = cli?.cwd ?? process.cwd();
4850
5170
  const configLoaderFn = cli?.configLoader ?? loadWikiConfig;
4851
5171
  if (!KNOWN_GATES.includes(name)) {
@@ -4877,7 +5197,7 @@ function gateRunHandler(name, opts, cli) {
4877
5197
  // src/commands/sprint.ts
4878
5198
  import * as fs25 from "fs";
4879
5199
  import * as path25 from "path";
4880
- import { spawnSync as spawnSync7 } from "child_process";
5200
+ import { spawnSync as spawnSync9 } from "child_process";
4881
5201
  import yaml7 from "js-yaml";
4882
5202
  var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
4883
5203
  function resolveRunScript(opts) {
@@ -4892,7 +5212,7 @@ function sprintInitHandler(opts, cli) {
4892
5212
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
4893
5213
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
4894
5214
  const exitFn = cli?.exit ?? defaultExit;
4895
- const spawnFn = cli?.spawnFn ?? spawnSync7;
5215
+ const spawnFn = cli?.spawnFn ?? spawnSync9;
4896
5216
  const mode = readSprintExecutionMode(opts.sprintId, {
4897
5217
  sprintFilePath: cli?.sprintFilePath,
4898
5218
  cwd: cli?.cwd
@@ -4914,7 +5234,7 @@ function sprintCloseHandler(opts, cli) {
4914
5234
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
4915
5235
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
4916
5236
  const exitFn = cli?.exit ?? defaultExit;
4917
- const spawnFn = cli?.spawnFn ?? spawnSync7;
5237
+ const spawnFn = cli?.spawnFn ?? spawnSync9;
4918
5238
  const mode = readSprintExecutionMode(opts.sprintId, {
4919
5239
  sprintFilePath: cli?.sprintFilePath,
4920
5240
  cwd: cli?.cwd
@@ -5019,7 +5339,7 @@ async function sprintArchiveHandler(opts, cli) {
5019
5339
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5020
5340
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5021
5341
  const exitFn = cli?.exit ?? defaultExit;
5022
- const spawnFn = cli?.spawnFn ?? spawnSync7;
5342
+ const spawnFn = cli?.spawnFn ?? spawnSync9;
5023
5343
  const cwd = cli?.cwd ?? process.cwd();
5024
5344
  const wikiBuildFn = cli?.wikiBuildFn ?? (async (wCwd, wStdout) => {
5025
5345
  const fakeExit = (code) => {
@@ -5243,7 +5563,7 @@ async function sprintArchiveHandler(opts, cli) {
5243
5563
  // src/commands/story.ts
5244
5564
  import * as fs26 from "fs";
5245
5565
  import * as path26 from "path";
5246
- import { spawnSync as spawnSync8 } from "child_process";
5566
+ import { spawnSync as spawnSync10 } from "child_process";
5247
5567
  function defaultExit2(code) {
5248
5568
  return process.exit(code);
5249
5569
  }
@@ -5269,7 +5589,7 @@ function storyStartHandler(opts, cli) {
5269
5589
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5270
5590
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5271
5591
  const exitFn = cli?.exit ?? defaultExit2;
5272
- const spawnFn = cli?.spawnFn ?? spawnSync8;
5592
+ const spawnFn = cli?.spawnFn ?? spawnSync10;
5273
5593
  const cwd = cli?.cwd ?? process.cwd();
5274
5594
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
5275
5595
  const mode = readSprintExecutionMode(sprintId, {
@@ -5347,7 +5667,7 @@ function storyCompleteHandler(opts, cli) {
5347
5667
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5348
5668
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5349
5669
  const exitFn = cli?.exit ?? defaultExit2;
5350
- const spawnFn = cli?.spawnFn ?? spawnSync8;
5670
+ const spawnFn = cli?.spawnFn ?? spawnSync10;
5351
5671
  const cwd = cli?.cwd ?? process.cwd();
5352
5672
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
5353
5673
  const mode = readSprintExecutionMode(sprintId, {
@@ -5457,7 +5777,7 @@ function storyCompleteHandler(opts, cli) {
5457
5777
 
5458
5778
  // src/commands/state.ts
5459
5779
  import * as path27 from "path";
5460
- import { spawnSync as spawnSync9 } from "child_process";
5780
+ import { spawnSync as spawnSync11 } from "child_process";
5461
5781
  function defaultExit3(code) {
5462
5782
  return process.exit(code);
5463
5783
  }
@@ -5470,7 +5790,7 @@ function stateUpdateHandler(opts, cli) {
5470
5790
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5471
5791
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5472
5792
  const exitFn = cli?.exit ?? defaultExit3;
5473
- const spawnFn = cli?.spawnFn ?? spawnSync9;
5793
+ const spawnFn = cli?.spawnFn ?? spawnSync11;
5474
5794
  const cwd = cli?.cwd;
5475
5795
  const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
5476
5796
  const mode = readSprintExecutionMode(sprintId, {
@@ -5497,7 +5817,7 @@ function stateValidateHandler(opts, cli) {
5497
5817
  const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
5498
5818
  const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
5499
5819
  const exitFn = cli?.exit ?? defaultExit3;
5500
- const spawnFn = cli?.spawnFn ?? spawnSync9;
5820
+ const spawnFn = cli?.spawnFn ?? spawnSync11;
5501
5821
  const mode = readSprintExecutionMode(opts.sprintId, {
5502
5822
  sprintFilePath: cli?.sprintFilePath,
5503
5823
  cwd: cli?.cwd
@@ -8471,6 +8791,108 @@ async function adminLoginHandler(opts = {}) {
8471
8791
  stdout(`Credentials saved to ${authFilePath} (chmod 600).`);
8472
8792
  }
8473
8793
 
8794
+ // src/commands/hotfix.ts
8795
+ import * as fs34 from "fs";
8796
+ import * as path44 from "path";
8797
+ function defaultExit4(code) {
8798
+ return process.exit(code);
8799
+ }
8800
+ var SLUG_RE = /^[a-z0-9-]+$/;
8801
+ var HOTFIX_FILE_RE = /^HOTFIX-(\d+)_.*\.md$/;
8802
+ function maxHotfixId(pendingDir) {
8803
+ let max = 0;
8804
+ let entries;
8805
+ try {
8806
+ entries = fs34.readdirSync(pendingDir);
8807
+ } catch {
8808
+ return 0;
8809
+ }
8810
+ for (const entry of entries) {
8811
+ const m = HOTFIX_FILE_RE.exec(entry);
8812
+ if (m) {
8813
+ const n = parseInt(m[1], 10);
8814
+ if (n > max) max = n;
8815
+ }
8816
+ }
8817
+ return max;
8818
+ }
8819
+ function countActiveHotfixes(repoRoot) {
8820
+ const pendingDir = path44.join(repoRoot, ".cleargate", "delivery", "pending-sync");
8821
+ const archiveDir = path44.join(repoRoot, ".cleargate", "delivery", "archive");
8822
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
8823
+ let count = 0;
8824
+ let pendingEntries = [];
8825
+ try {
8826
+ pendingEntries = fs34.readdirSync(pendingDir);
8827
+ } catch {
8828
+ }
8829
+ for (const entry of pendingEntries) {
8830
+ if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) count++;
8831
+ }
8832
+ let archiveEntries = [];
8833
+ try {
8834
+ archiveEntries = fs34.readdirSync(archiveDir);
8835
+ } catch {
8836
+ }
8837
+ for (const entry of archiveEntries) {
8838
+ if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) {
8839
+ try {
8840
+ const stat = fs34.statSync(path44.join(archiveDir, entry));
8841
+ if (stat.mtimeMs >= sevenDaysAgo) count++;
8842
+ } catch {
8843
+ }
8844
+ }
8845
+ }
8846
+ return count;
8847
+ }
8848
+ function resolveTemplatePath(repoRoot) {
8849
+ return path44.join(repoRoot, ".cleargate", "templates", "hotfix.md");
8850
+ }
8851
+ function hotfixNewHandler(opts, cli) {
8852
+ const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
8853
+ const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
8854
+ const exitFn = cli?.exit ?? defaultExit4;
8855
+ const repoRoot = cli?.cwd ?? process.cwd();
8856
+ const now = cli?.now ?? (/* @__PURE__ */ new Date()).toISOString();
8857
+ if (!SLUG_RE.test(opts.slug)) {
8858
+ stderrFn(`[cleargate hotfix new] slug must match ^[a-z0-9-]+$ (got: "${opts.slug}")`);
8859
+ return exitFn(1);
8860
+ }
8861
+ const activeCount = countActiveHotfixes(repoRoot);
8862
+ if (activeCount >= 3) {
8863
+ stderrFn(
8864
+ `Hotfix cap: \u22643 per rolling 7-day window. Currently ${activeCount} active. Bundle into a sprint or downgrade one to a CR.`
8865
+ );
8866
+ return exitFn(1);
8867
+ }
8868
+ const pendingDir = path44.join(repoRoot, ".cleargate", "delivery", "pending-sync");
8869
+ const maxId = maxHotfixId(pendingDir);
8870
+ const nextId = maxId + 1;
8871
+ const idStr = `HOTFIX-${String(nextId).padStart(3, "0")}`;
8872
+ const templatePath = resolveTemplatePath(repoRoot);
8873
+ let templateContent;
8874
+ try {
8875
+ templateContent = fs34.readFileSync(templatePath, "utf8");
8876
+ } catch {
8877
+ stderrFn(`[cleargate hotfix new] template not found: ${templatePath}`);
8878
+ return exitFn(2);
8879
+ }
8880
+ const content = templateContent.replace(/\{ID\}/g, idStr).replace(/\{SLUG\}/g, opts.slug).replace(/\{ISO\}/g, now);
8881
+ const fileSlug = opts.slug.replace(/-/g, "_");
8882
+ const fileName = `${idStr}_${fileSlug}.md`;
8883
+ const outPath = path44.join(pendingDir, fileName);
8884
+ try {
8885
+ fs34.mkdirSync(pendingDir, { recursive: true });
8886
+ fs34.writeFileSync(outPath, content, "utf8");
8887
+ } catch (err) {
8888
+ const msg = err instanceof Error ? err.message : String(err);
8889
+ stderrFn(`[cleargate hotfix new] write failed: ${msg}`);
8890
+ return exitFn(1);
8891
+ }
8892
+ stdoutFn(`[cleargate hotfix new] created: ${outPath}`);
8893
+ return exitFn(0);
8894
+ }
8895
+
8474
8896
  // src/cli.ts
8475
8897
  var program = new Command();
8476
8898
  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`)");
@@ -8487,8 +8909,8 @@ program.command("join <invite-url>").description("join a ClearGate workspace usi
8487
8909
  ...cmdOpts.code !== void 0 ? { code: cmdOpts.code } : {}
8488
8910
  });
8489
8911
  });
8490
- 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) => {
8491
- await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false });
8912
+ 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) => {
8913
+ await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false, pin: opts.pin });
8492
8914
  });
8493
8915
  program.command("whoami").description("print the currently authenticated agent identity").action(async () => {
8494
8916
  const { whoamiHandler } = await import("./whoami-CX7CXJD5.js");
@@ -8610,14 +9032,20 @@ admin.command("bootstrap-root <handle>").description("seed the first root admin
8610
9032
  const { bootstrapRootHandler } = await import("./bootstrap-root-FGWDICDT.js");
8611
9033
  await bootstrapRootHandler({ handle, databaseUrl: opts.databaseUrl, force: opts.force ?? false });
8612
9034
  });
8613
- 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", [
9035
+ 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", [
8614
9036
  "",
8615
9037
  "Modes (mutually exclusive):",
8616
9038
  " --check-scaffold Compute drift for all tracked scaffold files.",
8617
9039
  " Writes .cleargate/.drift-state.json.",
8618
9040
  " --session-start List blocked pending-sync items (\u226410, \u2264100 tokens).",
8619
9041
  " --pricing <file> Compute USD estimate from a work item's draft_tokens.",
8620
- " (default) Print a minimal hook-config health report."
9042
+ " --can-edit <file> Check if editing a file requires a planning work item.",
9043
+ " (default) Print a minimal hook-config health report.",
9044
+ "",
9045
+ "Exit codes:",
9046
+ " 0 Clean \u2014 no blockers, no config errors.",
9047
+ " 1 Blocked items or advisory issues \u2014 see stdout.",
9048
+ " 2 ClearGate misconfigured or partially installed \u2014 see stdout for remediation."
8621
9049
  ].join("\n")).action(async (opts) => {
8622
9050
  await doctorHandler({
8623
9051
  checkScaffold: opts.checkScaffold,
@@ -8625,8 +9053,10 @@ program.command("doctor").description("diagnose scaffold drift, hook health, blo
8625
9053
  sessionStart: opts.sessionStart,
8626
9054
  pricing: !!opts.pricing,
8627
9055
  pricingFile: opts.pricing,
9056
+ canEdit: !!opts.canEdit,
9057
+ canEditFile: opts.canEdit,
8628
9058
  verbose: opts.verbose
8629
- });
9059
+ }, opts.cwd ? { cwd: opts.cwd } : void 0);
8630
9060
  });
8631
9061
  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", [
8632
9062
  "",
@@ -8648,7 +9078,7 @@ program.command("uninstall").description("remove ClearGate scaffold from a proje
8648
9078
  "",
8649
9079
  "Always removed (no prompt): .claude/agents/*.md, ClearGate hooks,",
8650
9080
  " .claude/skills/flashcard/, CLAUDE.md CLEARGATE block,",
8651
- " @cleargate/cli from package.json, .install-manifest.json, .drift-state.json.",
9081
+ " `cleargate` from package.json, .install-manifest.json, .drift-state.json.",
8652
9082
  "",
8653
9083
  "Non-git targets: uncommitted-changes check is skipped silently."
8654
9084
  ].join("\n")).action(async (opts) => {
@@ -8701,5 +9131,9 @@ program.command("sync-log").description("filter and print sync-log entries").opt
8701
9131
  limit: opts.limit !== void 0 ? parseInt(opts.limit, 10) : 50
8702
9132
  });
8703
9133
  });
9134
+ var hotfix = program.command("hotfix").description("hotfix lane commands (off-sprint trivial fix scaffolding)");
9135
+ hotfix.command("new <slug>").description("scaffold a new HOTFIX-NNN_<slug>.md in pending-sync/").action((slug) => {
9136
+ hotfixNewHandler({ slug });
9137
+ });
8704
9138
  void program.parseAsync(process.argv);
8705
9139
  //# sourceMappingURL=cli.js.map