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