cleargate 0.2.0 → 0.2.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.
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { Command } from "commander";
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "cleargate",
12
- version: "0.2.0",
12
+ version: "0.2.1",
13
13
  private: false,
14
14
  type: "module",
15
15
  description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, four-agent loop (architect/developer/qa/reporter), Karpathy-style awareness wiki.",
@@ -358,6 +358,7 @@ function getCodebaseVersion(opts) {
358
358
  import * as fs3 from "fs/promises";
359
359
 
360
360
  // src/wiki/parse-frontmatter.ts
361
+ import yaml from "js-yaml";
361
362
  function parseFrontmatter(raw) {
362
363
  const lines = raw.split("\n");
363
364
  if (lines[0] !== "---") {
@@ -373,71 +374,45 @@ function parseFrontmatter(raw) {
373
374
  if (closeIdx === -1) {
374
375
  throw new Error("parseFrontmatter: missing closing ---");
375
376
  }
376
- const fmLines = lines.slice(1, closeIdx);
377
+ const yamlText = lines.slice(1, closeIdx).join("\n");
377
378
  const bodyLines = lines.slice(closeIdx + 1);
378
379
  if (bodyLines[0] === "") bodyLines.shift();
379
- const fm = {};
380
- for (const line of fmLines) {
381
- if (line.trim() === "" || line.trim().startsWith("#")) continue;
382
- const colon = line.indexOf(":");
383
- if (colon === -1) continue;
384
- const key = line.slice(0, colon).trim();
385
- const val = line.slice(colon + 1).trim();
386
- if (val === "" || val === "[]") {
387
- fm[key] = [];
388
- continue;
389
- }
390
- if (val.startsWith("[") && val.endsWith("]")) {
391
- const inner = val.slice(1, -1).trim();
392
- if (inner === "") {
393
- fm[key] = [];
394
- continue;
395
- }
396
- fm[key] = inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""));
397
- continue;
398
- }
399
- if (val.startsWith("{")) {
400
- fm[key] = val;
401
- continue;
402
- }
403
- fm[key] = val.replace(/^["']|["']$/g, "");
380
+ const body = bodyLines.join("\n");
381
+ if (yamlText.trim() === "") {
382
+ return { fm: {}, body };
383
+ }
384
+ let parsed;
385
+ try {
386
+ parsed = yaml.load(yamlText, { schema: yaml.CORE_SCHEMA });
387
+ } catch (err) {
388
+ throw new Error(`parseFrontmatter: invalid YAML: ${err.message}`);
389
+ }
390
+ if (parsed === null || parsed === void 0) {
391
+ return { fm: {}, body };
392
+ }
393
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
394
+ throw new Error("parseFrontmatter: frontmatter is not a YAML mapping");
404
395
  }
405
- return { fm, body: bodyLines.join("\n") };
396
+ return { fm: parsed, body };
406
397
  }
407
398
 
408
399
  // src/lib/frontmatter-yaml.ts
400
+ import yaml2 from "js-yaml";
409
401
  function serializeFrontmatter(fm) {
410
- const lines = ["---"];
411
- for (const [key, val] of Object.entries(fm)) {
412
- if (val === null) {
413
- lines.push(`${key}: null`);
414
- } else if (typeof val === "boolean") {
415
- lines.push(`${key}: ${val}`);
416
- } else if (typeof val === "number") {
417
- lines.push(`${key}: ${val}`);
418
- } else if (Array.isArray(val)) {
419
- if (val.length === 0) {
420
- lines.push(`${key}: []`);
421
- } else {
422
- const items = val.map((v) => `"${String(v)}"`).join(", ");
423
- lines.push(`${key}: [${items}]`);
424
- }
425
- } else {
426
- const s = String(val);
427
- if (s.startsWith("{")) {
428
- lines.push(`${key}: ${s}`);
429
- } else {
430
- const needsQuotes = /[:#\[\]{}&*!|>'"%@`\n]/.test(s) || s.trim() !== s || s === "" || s === "null" || s === "true" || s === "false";
431
- if (needsQuotes) {
432
- lines.push(`${key}: "${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
433
- } else {
434
- lines.push(`${key}: ${s}`);
435
- }
436
- }
437
- }
438
- }
439
- lines.push("---");
440
- return lines.join("\n");
402
+ if (Object.keys(fm).length === 0) {
403
+ return "---\n---";
404
+ }
405
+ const yamlBody = yaml2.dump(fm, {
406
+ schema: yaml2.CORE_SCHEMA,
407
+ lineWidth: -1,
408
+ noRefs: true,
409
+ noCompatMode: true,
410
+ quotingType: '"',
411
+ forceQuotes: false
412
+ });
413
+ return `---
414
+ ${yamlBody.replace(/\n+$/, "")}
415
+ ---`;
441
416
  }
442
417
  function toIsoSecond(d) {
443
418
  return d.toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -2034,7 +2009,7 @@ function loadWikiPages(wikiRoot) {
2034
2009
  import * as fs15 from "fs";
2035
2010
  import * as path15 from "path";
2036
2011
  import { spawnSync as spawnSync3 } from "child_process";
2037
- import yaml from "js-yaml";
2012
+ import yaml3 from "js-yaml";
2038
2013
 
2039
2014
  // src/lib/work-item-type.ts
2040
2015
  var FM_KEY_MAP = [
@@ -2281,7 +2256,7 @@ function parseCachedGateResult(raw) {
2281
2256
  if (typeof raw === "string") {
2282
2257
  if (!raw.startsWith("{")) return null;
2283
2258
  try {
2284
- const parsed = yaml.load(raw);
2259
+ const parsed = yaml3.load(raw);
2285
2260
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
2286
2261
  const p = parsed;
2287
2262
  return { pass: p["pass"], failing_criteria: p["failing_criteria"], last_gate_check: p["last_gate_check"] };
@@ -2764,15 +2739,23 @@ function emitSummary(driftMap, verbose, stdout) {
2764
2739
  var SESSION_START_MAX_ITEMS = 10;
2765
2740
  var SESSION_START_MAX_CHARS = 400;
2766
2741
  function parseCachedGateResult2(raw) {
2767
- try {
2768
- const parsed = JSON.parse(raw);
2769
- return {
2770
- pass: parsed.pass ?? null,
2771
- failing_criteria: parsed.failing_criteria ?? []
2772
- };
2773
- } catch {
2742
+ if (raw == null) return null;
2743
+ let parsed = null;
2744
+ if (typeof raw === "object" && !Array.isArray(raw)) {
2745
+ parsed = raw;
2746
+ } else if (typeof raw === "string") {
2747
+ try {
2748
+ parsed = JSON.parse(raw);
2749
+ } catch {
2750
+ return null;
2751
+ }
2752
+ } else {
2774
2753
  return null;
2775
2754
  }
2755
+ return {
2756
+ pass: parsed.pass ?? null,
2757
+ failing_criteria: parsed.failing_criteria ?? []
2758
+ };
2776
2759
  }
2777
2760
  async function runSessionStart(cwd, stdout) {
2778
2761
  const pendingSyncDir = path18.join(cwd, ".cleargate", "delivery", "pending-sync");
@@ -2797,9 +2780,7 @@ async function runSessionStart(cwd, stdout) {
2797
2780
  } catch {
2798
2781
  continue;
2799
2782
  }
2800
- const gateRaw = fm["cached_gate_result"];
2801
- if (typeof gateRaw !== "string") continue;
2802
- const gate2 = parseCachedGateResult2(gateRaw);
2783
+ const gate2 = parseCachedGateResult2(fm["cached_gate_result"]);
2803
2784
  if (!gate2 || gate2.pass !== false) continue;
2804
2785
  const idKeys = ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id", "sprint_id"];
2805
2786
  let itemId = "";
@@ -2864,15 +2845,23 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
2864
2845
  return;
2865
2846
  }
2866
2847
  const draftTokensRaw = fm["draft_tokens"];
2867
- if (!draftTokensRaw || typeof draftTokensRaw !== "string") {
2848
+ if (!draftTokensRaw) {
2868
2849
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
2869
2850
  exit(1);
2870
2851
  return;
2871
2852
  }
2872
2853
  let draftTokens;
2873
- try {
2874
- draftTokens = JSON.parse(draftTokensRaw);
2875
- } catch {
2854
+ if (typeof draftTokensRaw === "object" && !Array.isArray(draftTokensRaw)) {
2855
+ draftTokens = draftTokensRaw;
2856
+ } else if (typeof draftTokensRaw === "string") {
2857
+ try {
2858
+ draftTokens = JSON.parse(draftTokensRaw);
2859
+ } catch {
2860
+ stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
2861
+ exit(1);
2862
+ return;
2863
+ }
2864
+ } else {
2876
2865
  stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
2877
2866
  exit(1);
2878
2867
  return;
@@ -2934,7 +2923,7 @@ async function doctorHandler(flags, cli) {
2934
2923
  // src/commands/gate.ts
2935
2924
  import * as fs20 from "fs";
2936
2925
  import * as path20 from "path";
2937
- import yaml3 from "js-yaml";
2926
+ import yaml5 from "js-yaml";
2938
2927
 
2939
2928
  // src/lib/readiness-predicates.ts
2940
2929
  import * as fs18 from "fs";
@@ -3262,7 +3251,7 @@ function evalStatusOf(parsed, opts, projectRoot) {
3262
3251
 
3263
3252
  // src/lib/frontmatter-cache.ts
3264
3253
  import * as fs19 from "fs/promises";
3265
- import yaml2 from "js-yaml";
3254
+ import yaml4 from "js-yaml";
3266
3255
  async function readCachedGate(absPath) {
3267
3256
  let raw;
3268
3257
  try {
@@ -3276,20 +3265,7 @@ async function readCachedGate(absPath) {
3276
3265
  } catch {
3277
3266
  return null;
3278
3267
  }
3279
- const cached = fm["cached_gate_result"];
3280
- if (cached === void 0 || cached === null) return null;
3281
- if (typeof cached === "string") {
3282
- return parseCachedGateString(cached);
3283
- }
3284
- if (typeof cached === "object" && !Array.isArray(cached)) {
3285
- const c = cached;
3286
- return {
3287
- pass: Boolean(c["pass"]),
3288
- failing_criteria: Array.isArray(c["failing_criteria"]) ? c["failing_criteria"] : [],
3289
- last_gate_check: String(c["last_gate_check"] ?? "")
3290
- };
3291
- }
3292
- return null;
3268
+ return coerceCachedGate(fm["cached_gate_result"]);
3293
3269
  }
3294
3270
  async function writeCachedGate(absPath, result, opts) {
3295
3271
  const nowFn = opts?.now ?? (() => /* @__PURE__ */ new Date());
@@ -3307,29 +3283,22 @@ async function writeCachedGate(absPath, result, opts) {
3307
3283
  } catch {
3308
3284
  throw new Error(`writeCachedGate: failed to parse frontmatter in ${absPath}`);
3309
3285
  }
3310
- const existingCached = fm["cached_gate_result"];
3311
- if (existingCached !== void 0 && existingCached !== null) {
3312
- try {
3313
- const existingParsed = typeof existingCached === "string" ? parseCachedGateString(existingCached) : null;
3314
- if (existingParsed && JSON.stringify(existingParsed) === JSON.stringify(newResult)) {
3315
- return;
3316
- }
3317
- } catch {
3318
- }
3286
+ const existing = coerceCachedGate(fm["cached_gate_result"]);
3287
+ if (existing && JSON.stringify(existing) === JSON.stringify(newResult)) {
3288
+ return;
3319
3289
  }
3320
- const cachedStr = serializeCachedGate(newResult);
3321
3290
  const newFm = {};
3322
3291
  let inserted = false;
3323
3292
  for (const [k, v] of Object.entries(fm)) {
3324
3293
  if (k === "cached_gate_result") {
3325
- newFm["cached_gate_result"] = cachedStr;
3294
+ newFm["cached_gate_result"] = newResult;
3326
3295
  inserted = true;
3327
3296
  } else {
3328
3297
  newFm[k] = v;
3329
3298
  }
3330
3299
  }
3331
3300
  if (!inserted) {
3332
- newFm["cached_gate_result"] = cachedStr;
3301
+ newFm["cached_gate_result"] = newResult;
3333
3302
  }
3334
3303
  const fmBlock = serializeFrontmatter(newFm);
3335
3304
  const newContent = body.length > 0 ? `${fmBlock}
@@ -3338,24 +3307,31 @@ ${body}` : `${fmBlock}
3338
3307
  `;
3339
3308
  await fs19.writeFile(absPath, newContent, "utf8");
3340
3309
  }
3341
- function serializeCachedGate(result) {
3342
- const criteriaStr = result.failing_criteria.length === 0 ? "[]" : "[" + result.failing_criteria.map((c) => `{id: ${JSON.stringify(c.id)}, detail: ${JSON.stringify(c.detail)}}`).join(", ") + "]";
3343
- return `{pass: ${result.pass}, failing_criteria: ${criteriaStr}, last_gate_check: ${JSON.stringify(result.last_gate_check)}}`;
3344
- }
3345
- function parseCachedGateString(s) {
3346
- if (!s.startsWith("{")) return null;
3347
- try {
3348
- const parsed = yaml2.load(s);
3349
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
3350
- const p = parsed;
3310
+ function coerceCachedGate(val) {
3311
+ if (val === void 0 || val === null) return null;
3312
+ if (typeof val === "object" && !Array.isArray(val)) {
3313
+ const c = val;
3351
3314
  return {
3352
- pass: Boolean(p["pass"]),
3353
- failing_criteria: Array.isArray(p["failing_criteria"]) ? p["failing_criteria"] : [],
3354
- last_gate_check: String(p["last_gate_check"] ?? "")
3315
+ pass: Boolean(c["pass"]),
3316
+ failing_criteria: Array.isArray(c["failing_criteria"]) ? c["failing_criteria"] : [],
3317
+ last_gate_check: String(c["last_gate_check"] ?? "")
3355
3318
  };
3356
- } catch {
3357
- return null;
3358
3319
  }
3320
+ if (typeof val === "string" && val.startsWith("{")) {
3321
+ try {
3322
+ const parsed = yaml4.load(val, { schema: yaml4.CORE_SCHEMA });
3323
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
3324
+ const p = parsed;
3325
+ return {
3326
+ pass: Boolean(p["pass"]),
3327
+ failing_criteria: Array.isArray(p["failing_criteria"]) ? p["failing_criteria"] : [],
3328
+ last_gate_check: String(p["last_gate_check"] ?? "")
3329
+ };
3330
+ } catch {
3331
+ return null;
3332
+ }
3333
+ }
3334
+ return null;
3359
3335
  }
3360
3336
 
3361
3337
  // src/commands/gate.ts
@@ -3366,7 +3342,7 @@ function loadGateBlocks(gatesDocPath) {
3366
3342
  let match;
3367
3343
  while ((match = fenceRe.exec(raw)) !== null) {
3368
3344
  const yamlContent = match[1];
3369
- const parsed = yaml3.load(yamlContent);
3345
+ const parsed = yaml5.load(yamlContent);
3370
3346
  const block = Array.isArray(parsed) ? parsed[0] : parsed;
3371
3347
  if (block && typeof block === "object" && "work_item_type" in block && "transition" in block && "severity" in block && "criteria" in block) {
3372
3348
  blocks.push(block);
@@ -3694,7 +3670,7 @@ async function stampTokensHandler(file, opts, cli) {
3694
3670
  exitFn(1);
3695
3671
  return;
3696
3672
  }
3697
- const existingDraftTokens = parseDraftTokens(fm["draft_tokens"]);
3673
+ const existingDraftTokens = coerceDraftTokens(fm["draft_tokens"]);
3698
3674
  const existingLastStamp = existingDraftTokens?.last_stamp ?? null;
3699
3675
  const buckets = readLedgerForWorkItem(workItemId, { sprintRunsRoot: cli?.sprintRunsRoot });
3700
3676
  if (existingLastStamp && buckets.length > 0) {
@@ -3730,7 +3706,7 @@ async function stampTokensHandler(file, opts, cli) {
3730
3706
  if (opts.dryRun) {
3731
3707
  stdoutFn(`[dry-run] stamp-tokens would write draft_tokens for ${workItemId}:`);
3732
3708
  const draftTokensVal = newFm["draft_tokens"];
3733
- stdoutFn(` draft_tokens: ${String(draftTokensVal)}`);
3709
+ stdoutFn(` draft_tokens: ${JSON.stringify(draftTokensVal)}`);
3734
3710
  if (stampError) {
3735
3711
  stdoutFn(` stamp_error: "${stampError}"`);
3736
3712
  }
@@ -3769,15 +3745,29 @@ function extractWorkItemId(fm, absPath) {
3769
3745
  }
3770
3746
  return null;
3771
3747
  }
3772
- function parseDraftTokens(val) {
3748
+ function coerceDraftTokens(val) {
3773
3749
  if (val == null) return null;
3774
- if (typeof val !== "string") return null;
3775
- try {
3776
- const parsed = JSON.parse(val);
3777
- return parsed;
3778
- } catch {
3779
- return null;
3750
+ if (typeof val === "object" && !Array.isArray(val)) {
3751
+ const o = val;
3752
+ return {
3753
+ input: typeof o["input"] === "number" ? o["input"] : null,
3754
+ output: typeof o["output"] === "number" ? o["output"] : null,
3755
+ cache_creation: typeof o["cache_creation"] === "number" ? o["cache_creation"] : null,
3756
+ cache_read: typeof o["cache_read"] === "number" ? o["cache_read"] : null,
3757
+ model: typeof o["model"] === "string" ? o["model"] : null,
3758
+ last_stamp: typeof o["last_stamp"] === "string" ? o["last_stamp"] : "",
3759
+ sessions: Array.isArray(o["sessions"]) ? o["sessions"] : []
3760
+ };
3780
3761
  }
3762
+ if (typeof val === "string") {
3763
+ try {
3764
+ const parsed = JSON.parse(val);
3765
+ return parsed;
3766
+ } catch {
3767
+ return null;
3768
+ }
3769
+ }
3770
+ return null;
3781
3771
  }
3782
3772
  function aggregateBuckets(buckets, nowIso) {
3783
3773
  let totalInput = 0;
@@ -3830,12 +3820,9 @@ function buildNewFrontmatter(existingFm, tokens, stampError) {
3830
3820
  if (stampError) {
3831
3821
  newFm["stamp_error"] = stampError;
3832
3822
  }
3833
- newFm["draft_tokens"] = serializeDraftTokens(tokens);
3823
+ newFm["draft_tokens"] = tokens;
3834
3824
  return newFm;
3835
3825
  }
3836
- function serializeDraftTokens(tokens) {
3837
- return JSON.stringify(tokens);
3838
- }
3839
3826
  function buildSerializedContent(fm, body) {
3840
3827
  const fmBlock = serializeFrontmatter(fm);
3841
3828
  if (body.length > 0) {