cleargate 0.11.5 → 0.13.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 (50) hide show
  1. package/dist/MANIFEST.json +15 -15
  2. package/dist/admin-api/index.cjs +3 -1
  3. package/dist/admin-api/index.cjs.map +1 -1
  4. package/dist/admin-api/index.d.cts +1 -0
  5. package/dist/admin-api/index.d.ts +1 -0
  6. package/dist/admin-api/index.js +3 -1
  7. package/dist/admin-api/index.js.map +1 -1
  8. package/dist/{chunk-HZPJ5QX4.js → chunk-EG6YGT2O.js} +315 -33
  9. package/dist/chunk-EG6YGT2O.js.map +1 -0
  10. package/dist/cli.cjs +855 -359
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +316 -108
  13. package/dist/cli.js.map +1 -1
  14. package/dist/lib/lifecycle-reconcile.cjs +318 -34
  15. package/dist/lib/lifecycle-reconcile.cjs.map +1 -1
  16. package/dist/lib/lifecycle-reconcile.d.cts +55 -4
  17. package/dist/lib/lifecycle-reconcile.d.ts +55 -4
  18. package/dist/lib/lifecycle-reconcile.js +7 -3
  19. package/dist/templates/cleargate-planning/.claude/agents/architect.md +6 -4
  20. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +1 -1
  21. package/dist/templates/cleargate-planning/.claude/agents/developer.md +8 -4
  22. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +2 -0
  23. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +138 -0
  24. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +162 -0
  25. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +1 -1
  26. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +1 -1
  27. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +1 -1
  28. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +1 -1
  29. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
  30. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +1 -1
  31. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +1 -1
  32. package/dist/templates/cleargate-planning/CLAUDE.md +4 -0
  33. package/dist/templates/cleargate-planning/MANIFEST.json +15 -15
  34. package/package.json +8 -9
  35. package/templates/cleargate-planning/.claude/agents/architect.md +6 -4
  36. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +1 -1
  37. package/templates/cleargate-planning/.claude/agents/developer.md +8 -4
  38. package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +2 -0
  39. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +138 -0
  40. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +162 -0
  41. package/templates/cleargate-planning/.cleargate/templates/Bug.md +1 -1
  42. package/templates/cleargate-planning/.cleargate/templates/CR.md +1 -1
  43. package/templates/cleargate-planning/.cleargate/templates/epic.md +1 -1
  44. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +1 -1
  45. package/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
  46. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +1 -1
  47. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -1
  48. package/templates/cleargate-planning/CLAUDE.md +4 -0
  49. package/templates/cleargate-planning/MANIFEST.json +15 -15
  50. package/dist/chunk-HZPJ5QX4.js.map +0 -1
package/dist/cli.js CHANGED
@@ -3,8 +3,9 @@ import {
3
3
  checkVerbMismatch,
4
4
  parseFrontmatter,
5
5
  reconcileDecomposition,
6
- reconcileLifecycle
7
- } from "./chunk-HZPJ5QX4.js";
6
+ reconcileLifecycle,
7
+ walkActiveParents
8
+ } from "./chunk-EG6YGT2O.js";
8
9
  import {
9
10
  AcquireError,
10
11
  acquireAccessToken,
@@ -22,7 +23,7 @@ import { Command } from "commander";
22
23
  // package.json
23
24
  var package_default = {
24
25
  name: "cleargate",
25
- version: "0.11.5",
26
+ version: "0.13.0",
26
27
  private: false,
27
28
  type: "module",
28
29
  description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, five-role agent team (architect/developer/qa/devops/reporter), Karpathy-style awareness wiki.",
@@ -69,12 +70,11 @@ var package_default = {
69
70
  build: "tsup",
70
71
  dev: "tsup --watch",
71
72
  typecheck: "tsc --noEmit",
72
- test: "tsx --test --test-reporter=spec 'test/**/*.node.test.ts'",
73
- "test:file": "tsx --test --test-reporter=spec",
74
- "test:vitest": "npm run build && vitest run",
75
- "test:vitest:watch": "vitest",
76
- "test:node": "tsx --test --test-reporter=spec 'test/**/*.node.test.ts'",
77
- "test:node:file": "tsx --test --test-reporter=spec"
73
+ test: "tsx --test --test-concurrency=1 --experimental-test-module-mocks --test-reporter=spec 'test/**/*.node.test.ts' '!test/fixtures/**'",
74
+ "test:file": "tsx --test --test-concurrency=1 --experimental-test-module-mocks --test-reporter=spec",
75
+ "test:node": "tsx --test --test-concurrency=1 --experimental-test-module-mocks --test-reporter=spec 'test/**/*.node.test.ts' '!test/fixtures/**'",
76
+ "test:node:file": "tsx --test --test-concurrency=1 --experimental-test-module-mocks --test-reporter=spec",
77
+ "check:no-vitest": `node -e "const r=require('child_process').execSync('grep -rE \\"\\\\b(vitest|vi\\\\.fn|vi\\\\.mock|vi\\\\.spyOn|vi\\\\.stubGlobal|vi\\\\.useFakeTimers|vi\\\\.useRealTimers|vi\\\\.advanceTimersByTime|vi\\\\.hoisted)\\\\b\\" --include=\\"*.ts\\" --include=\\"*.js\\" --include=\\"*.mjs\\" --exclude-dir=test/fixtures . 2>/dev/null || true', {encoding:'utf8'}); if(r.trim()){console.error('vitest residue detected:\\\\n'+r); process.exit(1)} console.log('no vitest residue')"`
78
78
  },
79
79
  dependencies: {
80
80
  "@napi-rs/keyring": "^1.2.0",
@@ -89,10 +89,10 @@ var package_default = {
89
89
  "@types/js-yaml": "^4.0.9",
90
90
  "@types/node": "^24.0.0",
91
91
  "@types/pg": "^8.11.10",
92
+ "ts-morph": "28.0.0",
92
93
  tsup: "^8",
93
94
  tsx: "^4.21.0",
94
- typescript: "^5.8.0",
95
- vitest: "^2.1.0"
95
+ typescript: "^5.8.0"
96
96
  }
97
97
  };
98
98
 
@@ -1485,6 +1485,24 @@ var PREFIX_MAP = [
1485
1485
  { prefix: "BUG-", type: "bug", bucket: "bugs" },
1486
1486
  { prefix: "INITIATIVE-", type: "initiative", bucket: "initiatives" }
1487
1487
  ];
1488
+ var SPRINT_REPORT_FILENAMES = ["REPORT.md"];
1489
+ var SPRINT_REPORT_CANONICAL_RE = /^SPRINT-\d{2,}_REPORT\.md$/;
1490
+ function isSprintReportPath(relPath) {
1491
+ const normalised = relPath.replace(/\\/g, "/");
1492
+ const match = /\.cleargate\/sprint-runs\/(SPRINT-\d{2,})\/([^/]+)$/.exec(normalised);
1493
+ if (!match) return false;
1494
+ const filename = match[2];
1495
+ return SPRINT_REPORT_FILENAMES.includes(filename) || SPRINT_REPORT_CANONICAL_RE.test(filename);
1496
+ }
1497
+ function deriveBucketFromReportPath(relPath) {
1498
+ const normalised = relPath.replace(/\\/g, "/");
1499
+ const match = /\.cleargate\/sprint-runs\/(SPRINT-\d{2,})\//.exec(normalised);
1500
+ if (!match) {
1501
+ throw new Error(`deriveBucketFromReportPath: cannot extract SPRINT-NN from: ${relPath}`);
1502
+ }
1503
+ const id = match[1];
1504
+ return { type: "sprint", id, bucket: "sprints" };
1505
+ }
1488
1506
  function deriveBucket(filename) {
1489
1507
  const base = filename.includes("/") ? filename.split("/").pop() : filename;
1490
1508
  const stem = base.endsWith(".md") ? base.slice(0, -3) : base;
@@ -1605,6 +1623,12 @@ function serializePage(page, body) {
1605
1623
  if (page.sprint_cleargate_id !== void 0) {
1606
1624
  lines.push(`sprint_cleargate_id: "${page.sprint_cleargate_id}"`);
1607
1625
  }
1626
+ if (page.report_raw_path !== void 0) {
1627
+ lines.push(`report_raw_path: "${page.report_raw_path}"`);
1628
+ }
1629
+ if (page.last_report_ingest_commit !== void 0) {
1630
+ lines.push(`last_report_ingest_commit: "${page.last_report_ingest_commit}"`);
1631
+ }
1608
1632
  lines.push("---");
1609
1633
  const fm = lines.join("\n");
1610
1634
  return `${fm}
@@ -1627,7 +1651,9 @@ function parsePage(raw) {
1627
1651
  const last_contradict_sha = fm["last_contradict_sha"] !== void 0 ? String(fm["last_contradict_sha"]) : void 0;
1628
1652
  const parent_cleargate_id = fm["parent_cleargate_id"] !== void 0 ? String(fm["parent_cleargate_id"]) : void 0;
1629
1653
  const sprint_cleargate_id = fm["sprint_cleargate_id"] !== void 0 ? String(fm["sprint_cleargate_id"]) : void 0;
1630
- return { type, id, parent, children, status, remote_id, raw_path, last_ingest, last_ingest_commit, repo, last_contradict_sha, parent_cleargate_id, sprint_cleargate_id };
1654
+ const report_raw_path = fm["report_raw_path"] !== void 0 ? String(fm["report_raw_path"]) : void 0;
1655
+ const last_report_ingest_commit = fm["last_report_ingest_commit"] !== void 0 ? String(fm["last_report_ingest_commit"]) : void 0;
1656
+ return { type, id, parent, children, status, remote_id, raw_path, last_ingest, last_ingest_commit, repo, last_contradict_sha, parent_cleargate_id, sprint_cleargate_id, report_raw_path, last_report_ingest_commit };
1631
1657
  }
1632
1658
  function parseFmRaw(raw) {
1633
1659
  const lines = raw.split("\n");
@@ -3146,28 +3172,33 @@ async function wikiIngestHandler(opts) {
3146
3172
  const rawPath = opts.rawPath;
3147
3173
  const absRawPath = path17.isAbsolute(rawPath) ? rawPath : path17.resolve(cwd, rawPath);
3148
3174
  const relRawPath = path17.relative(cwd, absRawPath).replace(/\\/g, "/");
3149
- const deliveryRoot = path17.join(cwd, ".cleargate", "delivery");
3150
- const deliveryRootNorm = deliveryRoot.replace(/\\/g, "/");
3151
- const absDeliveryRoot = deliveryRoot;
3152
- const relToDelivery = path17.relative(absDeliveryRoot, absRawPath);
3153
- if (relToDelivery.startsWith("..") || path17.isAbsolute(relToDelivery)) {
3154
- stderr(`wiki ingest: ${rawPath} not under .cleargate/delivery/
3175
+ const isSprintReport = isSprintReportPath(relRawPath);
3176
+ if (!isSprintReport) {
3177
+ const deliveryRoot = path17.join(cwd, ".cleargate", "delivery");
3178
+ const absDeliveryRoot = deliveryRoot;
3179
+ const relToDelivery = path17.relative(absDeliveryRoot, absRawPath);
3180
+ if (relToDelivery.startsWith("..") || path17.isAbsolute(relToDelivery)) {
3181
+ stderr(`wiki ingest: ${rawPath} not under .cleargate/delivery/
3155
3182
  `);
3156
- exit(2);
3157
- return;
3158
- }
3159
- void deliveryRootNorm;
3160
- const isExcluded = EXCLUDED_SUFFIXES2.some((excl) => relRawPath.startsWith(excl));
3161
- if (isExcluded) {
3162
- stdout(`wiki ingest: ${rawPath} excluded (skip)
3183
+ exit(2);
3184
+ return;
3185
+ }
3186
+ const isExcluded = EXCLUDED_SUFFIXES2.some((excl) => relRawPath.startsWith(excl));
3187
+ if (isExcluded) {
3188
+ stdout(`wiki ingest: ${rawPath} excluded (skip)
3163
3189
  `);
3164
- exit(0);
3165
- return;
3190
+ exit(0);
3191
+ return;
3192
+ }
3166
3193
  }
3167
3194
  const filename = path17.basename(absRawPath);
3168
3195
  let bucketInfo;
3169
3196
  try {
3170
- bucketInfo = deriveBucket(filename);
3197
+ if (isSprintReport) {
3198
+ bucketInfo = deriveBucketFromReportPath(relRawPath);
3199
+ } else {
3200
+ bucketInfo = deriveBucket(filename);
3201
+ }
3171
3202
  } catch (e) {
3172
3203
  stderr(`wiki ingest: cannot determine bucket for ${rawPath}: ${e.message}
3173
3204
  `);
@@ -3210,16 +3241,26 @@ async function wikiIngestHandler(opts) {
3210
3241
  }
3211
3242
  const currentSha = getGitSha(absRawPath, gitRunner) ?? "";
3212
3243
  const pageExists = fs17.existsSync(pagePath);
3213
- if (pageExists && currentSha !== "") {
3214
- let isNoOp = false;
3244
+ let existingPage;
3245
+ if (pageExists) {
3215
3246
  try {
3216
3247
  const existingPageContent = fs17.readFileSync(pagePath, "utf8");
3217
- const existingPage = parsePage(existingPageContent);
3218
- if (existingPage.last_ingest_commit === currentSha) {
3219
- const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
3220
- if (contentUnchanged) {
3248
+ existingPage = parsePage(existingPageContent);
3249
+ } catch {
3250
+ }
3251
+ }
3252
+ if (existingPage !== void 0 && currentSha !== "") {
3253
+ let isNoOp = false;
3254
+ try {
3255
+ if (isSprintReport) {
3256
+ if (existingPage.last_report_ingest_commit === currentSha) {
3221
3257
  isNoOp = true;
3222
3258
  }
3259
+ } else {
3260
+ if (existingPage.last_ingest_commit === currentSha) {
3261
+ const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
3262
+ if (contentUnchanged) isNoOp = true;
3263
+ }
3223
3264
  }
3224
3265
  } catch {
3225
3266
  }
@@ -3231,36 +3272,53 @@ async function wikiIngestHandler(opts) {
3231
3272
  }
3232
3273
  }
3233
3274
  const action = pageExists ? "update" : "create";
3234
- let existingLastContradictSha;
3235
- if (pageExists) {
3236
- try {
3237
- const existingPageContent = fs17.readFileSync(pagePath, "utf8");
3238
- const existingPage = parsePage(existingPageContent);
3239
- existingLastContradictSha = existingPage.last_contradict_sha;
3240
- } catch {
3241
- }
3242
- }
3275
+ const timestamp = now();
3276
+ const existingLastContradictSha = existingPage?.last_contradict_sha;
3277
+ const existingReportRawPath = existingPage?.report_raw_path;
3278
+ const existingLastReportIngestCommit = existingPage?.last_report_ingest_commit;
3243
3279
  const parent = buildParentRef2(fm);
3244
3280
  const children = buildChildrenRefs2(fm);
3245
- const timestamp = now();
3246
3281
  const wikiPage = {
3247
3282
  type,
3248
3283
  id,
3249
- parent,
3250
- children,
3251
- status: String(fm["status"] ?? ""),
3252
- remote_id: String(fm["remote_id"] ?? ""),
3253
- raw_path: relRawPath,
3284
+ parent: isSprintReport ? existingPage?.parent ?? "" : parent,
3285
+ children: isSprintReport ? existingPage?.children ?? [] : children,
3286
+ status: isSprintReport ? existingPage?.status ?? String(fm["status"] ?? "") : String(fm["status"] ?? ""),
3287
+ remote_id: isSprintReport ? existingPage?.remote_id ?? String(fm["remote_id"] ?? "") : String(fm["remote_id"] ?? ""),
3288
+ // raw_path tracks the plan file path; for report-only ingest preserve existing or use relRawPath as fallback
3289
+ raw_path: isSprintReport ? existingPage?.raw_path ?? relRawPath : relRawPath,
3254
3290
  last_ingest: timestamp,
3255
- last_ingest_commit: currentSha,
3291
+ // last_ingest_commit tracks the plan source; preserve when re-ingesting report
3292
+ last_ingest_commit: isSprintReport ? existingPage?.last_ingest_commit ?? "" : currentSha,
3256
3293
  repo,
3257
3294
  // Carry forward last_contradict_sha so Phase 4 idempotency survives re-ingest
3258
3295
  ...existingLastContradictSha !== void 0 ? { last_contradict_sha: existingLastContradictSha } : {},
3259
- // Hierarchy keys (§11.7): read from raw fm stamped at raw-side, not wiki-side
3260
- ...typeof fm["parent_cleargate_id"] === "string" ? { parent_cleargate_id: fm["parent_cleargate_id"] } : {},
3261
- ...typeof fm["sprint_cleargate_id"] === "string" ? { sprint_cleargate_id: fm["sprint_cleargate_id"] } : {}
3296
+ // Hierarchy keys (§11.7): read from raw fm for plan ingest; preserve for report ingest
3297
+ ...isSprintReport ? existingPage?.parent_cleargate_id !== void 0 ? { parent_cleargate_id: existingPage.parent_cleargate_id } : {} : typeof fm["parent_cleargate_id"] === "string" ? { parent_cleargate_id: fm["parent_cleargate_id"] } : {},
3298
+ ...isSprintReport ? existingPage?.sprint_cleargate_id !== void 0 ? { sprint_cleargate_id: existingPage.sprint_cleargate_id } : {} : typeof fm["sprint_cleargate_id"] === "string" ? { sprint_cleargate_id: fm["sprint_cleargate_id"] } : {},
3299
+ // CR-063 sprint-report fields
3300
+ report_raw_path: isSprintReport ? relRawPath : existingReportRawPath ?? void 0,
3301
+ last_report_ingest_commit: isSprintReport ? currentSha : existingLastReportIngestCommit ?? void 0
3262
3302
  };
3263
- const pageBody = buildPageBody2({ id, fm, body });
3303
+ let existingPageBody = "";
3304
+ if (existingPage !== void 0 && pageExists) {
3305
+ try {
3306
+ const existingPageContent = fs17.readFileSync(pagePath, "utf8");
3307
+ const lines = existingPageContent.split("\n");
3308
+ let closingDash = -1;
3309
+ for (let i = 1; i < lines.length; i++) {
3310
+ if (lines[i] === "---") {
3311
+ closingDash = i;
3312
+ break;
3313
+ }
3314
+ }
3315
+ if (closingDash !== -1) {
3316
+ existingPageBody = lines.slice(closingDash + 1).join("\n").replace(/^\n/, "");
3317
+ }
3318
+ } catch {
3319
+ }
3320
+ }
3321
+ const pageBody = buildPageBody2({ id, fm, body, isSprintReport, existingPageBody });
3264
3322
  const pageContent = serializePage(wikiPage, pageBody);
3265
3323
  fs17.mkdirSync(pageDir, { recursive: true });
3266
3324
  fs17.writeFileSync(pagePath, pageContent, "utf8");
@@ -3338,7 +3396,37 @@ function buildChildrenRefs2(fm) {
3338
3396
  return `[[${s}]]`;
3339
3397
  });
3340
3398
  }
3399
+ function extractReportBlock(pageBody) {
3400
+ const beginMarker = "<!-- BEGIN sprint-report -->";
3401
+ const endMarker = "<!-- END sprint-report -->";
3402
+ const beginIdx = pageBody.indexOf(beginMarker);
3403
+ const endIdx = pageBody.indexOf(endMarker);
3404
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) return void 0;
3405
+ return pageBody.slice(beginIdx, endIdx + endMarker.length);
3406
+ }
3407
+ function extractPlanStub(pageBody) {
3408
+ const beginMarker = "<!-- BEGIN sprint-report -->";
3409
+ const beginIdx = pageBody.indexOf(beginMarker);
3410
+ if (beginIdx === -1) return pageBody;
3411
+ return pageBody.slice(0, beginIdx);
3412
+ }
3341
3413
  function buildPageBody2(item) {
3414
+ const { isSprintReport, existingPageBody } = item;
3415
+ if (isSprintReport) {
3416
+ const planStub = existingPageBody ? extractPlanStub(existingPageBody) : buildPlanStub(item);
3417
+ const reportBlock = buildReportBlock(item.body);
3418
+ return planStub + reportBlock;
3419
+ } else {
3420
+ const planStub = buildPlanStub(item);
3421
+ const existingReportBlock = existingPageBody ? extractReportBlock(existingPageBody) : void 0;
3422
+ if (existingReportBlock !== void 0) {
3423
+ const stub = planStub.trimEnd() + "\n\n";
3424
+ return stub + existingReportBlock + "\n";
3425
+ }
3426
+ return planStub;
3427
+ }
3428
+ }
3429
+ function buildPlanStub(item) {
3342
3430
  const title = String(item.fm["title"] ?? item.id);
3343
3431
  const summary = String(
3344
3432
  item.fm["description"] ?? item.body.split("\n")[0] ?? "No summary available."
@@ -3362,6 +3450,16 @@ function buildPageBody2(item) {
3362
3450
  ""
3363
3451
  ].join("\n");
3364
3452
  }
3453
+ function buildReportBlock(reportBody) {
3454
+ return [
3455
+ "<!-- BEGIN sprint-report -->",
3456
+ "## Sprint Report",
3457
+ "",
3458
+ reportBody.trim(),
3459
+ "<!-- END sprint-report -->",
3460
+ ""
3461
+ ].join("\n");
3462
+ }
3365
3463
  function appendLogEntry(wikiRoot, entry) {
3366
3464
  const logPath = path17.join(wikiRoot, "log.md");
3367
3465
  const logEntry = [
@@ -5823,7 +5921,7 @@ async function writeCachedGate(absPath, result, opts) {
5823
5921
  throw new Error(`writeCachedGate: failed to parse frontmatter in ${absPath}`);
5824
5922
  }
5825
5923
  const existing = coerceCachedGate(fm["cached_gate_result"]);
5826
- if (existing && JSON.stringify(existing) === JSON.stringify(newResult)) {
5924
+ if (existing && existing.pass === newResult.pass && JSON.stringify(existing.failing_criteria) === JSON.stringify(newResult.failing_criteria)) {
5827
5925
  return;
5828
5926
  }
5829
5927
  const newFm = {};
@@ -6365,22 +6463,50 @@ function reconcileLifecycleCliHandler(opts, cli) {
6365
6463
  });
6366
6464
  if (result.drift.length === 0) {
6367
6465
  stdoutFn(`lifecycle: clean (${result.clean} artifacts reconciled)`);
6368
- return exitFn(0);
6369
- }
6370
- stderrFn(`lifecycle: DRIFT detected (${result.drift.length} unreconciled artifacts):`);
6371
- for (const item of result.drift) {
6372
- stderrFn(
6373
- ` DRIFT: ${item.id} status=${item.actual_status ?? "missing"} in ${item.in_archive ? "archive" : "pending-sync"}, expected ${item.expected_status} (commit ${item.commit_shas[0] ?? "unknown"})`
6374
- );
6375
- stderrFn(
6376
- ` Remediation: git mv .cleargate/delivery/pending-sync/${item.file_path?.replace("pending-sync/", "") ?? item.id + "_*.md"} .cleargate/delivery/archive/ && update status: ${item.expected_status}`
6377
- );
6466
+ } else {
6467
+ stderrFn(`lifecycle: DRIFT detected (${result.drift.length} unreconciled artifacts):`);
6468
+ for (const item of result.drift) {
6469
+ stderrFn(
6470
+ ` DRIFT: ${item.id} status=${item.actual_status ?? "missing"} in ${item.in_archive ? "archive" : "pending-sync"}, expected ${item.expected_status} (commit ${item.commit_shas[0] ?? "unknown"})`
6471
+ );
6472
+ stderrFn(
6473
+ ` Remediation: git mv .cleargate/delivery/pending-sync/${item.file_path?.replace("pending-sync/", "") ?? item.id + "_*.md"} .cleargate/delivery/archive/ && update status: ${item.expected_status}`
6474
+ );
6475
+ }
6476
+ if (!opts.parents) {
6477
+ return exitFn(1);
6478
+ }
6378
6479
  }
6379
- return exitFn(1);
6380
6480
  } catch (err) {
6381
6481
  stderrFn(`lifecycle reconciliation error: ${err instanceof Error ? err.message : String(err)}`);
6382
- return exitFn(1);
6482
+ if (!opts.parents) {
6483
+ return exitFn(1);
6484
+ }
6485
+ }
6486
+ if (opts.parents) {
6487
+ const archiveRoot = path31.join(deliveryRoot, "archive");
6488
+ walkActiveParents({ deliveryRoot, archiveRoot }).then((results) => {
6489
+ stdoutFn("Parent rollup audit (--parents):");
6490
+ for (const r of results) {
6491
+ if (r.verdict === "auto-flip") {
6492
+ stdoutFn(
6493
+ ` ${r.parent_id} \u2713 proposed: Completed (${r.terminal_children.length}/${r.terminal_children.length} children Completed)`
6494
+ );
6495
+ } else if (r.verdict === "halt-partial" || r.verdict === "halt-zero-children") {
6496
+ stdoutFn(` ${r.parent_id} \u2717 ${r.verdict}: ${r.halt_reason ?? "no details"}`);
6497
+ } else if (r.verdict === "no-op") {
6498
+ } else {
6499
+ stdoutFn(` ${r.parent_id} ~ ${r.verdict}`);
6500
+ }
6501
+ }
6502
+ exitFn(0);
6503
+ }).catch((err) => {
6504
+ stderrFn(`--parents audit error: ${err instanceof Error ? err.message : String(err)}`);
6505
+ exitFn(0);
6506
+ });
6507
+ return;
6383
6508
  }
6509
+ return exitFn(0);
6384
6510
  }
6385
6511
  function parseFileFrontmatter(raw) {
6386
6512
  const lines = raw.split("\n");
@@ -7037,6 +7163,10 @@ function refreshScopedGateCaches(sprintId, cwd, execFn) {
7037
7163
  if (!absPath) {
7038
7164
  continue;
7039
7165
  }
7166
+ if (absPath.includes(`${path31.sep}archive${path31.sep}`)) {
7167
+ result.skipped.push(id);
7168
+ continue;
7169
+ }
7040
7170
  let status = "";
7041
7171
  try {
7042
7172
  const raw = fs30.readFileSync(absPath, "utf8");
@@ -10244,6 +10374,7 @@ function getItemId3(fm) {
10244
10374
  }
10245
10375
 
10246
10376
  // src/commands/push.ts
10377
+ import * as fs40 from "fs";
10247
10378
  import * as fsPromises11 from "fs/promises";
10248
10379
  import * as path49 from "path";
10249
10380
  async function pushHandler(fileOrId, opts = {}) {
@@ -10253,6 +10384,13 @@ async function pushHandler(fileOrId, opts = {}) {
10253
10384
  const stderr = opts.stderr ?? ((s) => process.stderr.write(s));
10254
10385
  const exit = opts.exit ?? ((c) => process.exit(c));
10255
10386
  const nowFn = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
10387
+ const migrationLockPath = path49.join(projectRoot, ".cleargate", ".migration-lock");
10388
+ if (fs40.existsSync(migrationLockPath)) {
10389
+ stderr(`Error: CR-067 migration in progress (.migration-lock held); retry in 30s
10390
+ `);
10391
+ exit(75);
10392
+ return;
10393
+ }
10256
10394
  const identity = resolveIdentity(projectRoot);
10257
10395
  const sprintRoot = resolveActiveSprintDir(projectRoot);
10258
10396
  async function resolveMcp() {
@@ -10320,6 +10458,19 @@ async function pushHandler(fileOrId, opts = {}) {
10320
10458
  async function handlePush(filePath, ctx) {
10321
10459
  const { projectRoot, identity, sprintRoot, nowFn, resolveMcp, stdout, stderr, exit } = ctx;
10322
10460
  const resolvedPath = path49.isAbsolute(filePath) ? filePath : path49.resolve(projectRoot, filePath);
10461
+ if (SPRINT_RUNS_PATH_REGEX.test(resolvedPath)) {
10462
+ if (!SPRINT_REPORT_PATH_REGEX.test(resolvedPath)) {
10463
+ stderr(
10464
+ `Error: path not in allowlist. Only sprint report files are accepted from sprint-runs/.
10465
+ Allowed: .cleargate/sprint-runs/SPRINT-NN/REPORT.md
10466
+ .cleargate/sprint-runs/SPRINT-NN/SPRINT-NN_REPORT.md
10467
+ Got: "${resolvedPath}"
10468
+ `
10469
+ );
10470
+ exit(2);
10471
+ return;
10472
+ }
10473
+ }
10323
10474
  let rawContent;
10324
10475
  try {
10325
10476
  rawContent = await fsPromises11.readFile(resolvedPath, "utf8");
@@ -10349,7 +10500,7 @@ async function handlePush(filePath, ctx) {
10349
10500
  return;
10350
10501
  }
10351
10502
  const itemId = getItemId4(fm);
10352
- const type = getItemType2(fm);
10503
+ const type = getItemTypeWithPathOverride(resolvedPath, fm);
10353
10504
  if (!type) {
10354
10505
  stderr(`Error: cannot determine item type from frontmatter in "${resolvedPath}".
10355
10506
  `);
@@ -10362,6 +10513,9 @@ async function handlePush(filePath, ctx) {
10362
10513
  if (h1) payloadForPush["title"] = h1;
10363
10514
  }
10364
10515
  payloadForPush["body"] = body;
10516
+ if (payloadForPush["origin"] === void 0) {
10517
+ payloadForPush["origin"] = "cleargate-cli";
10518
+ }
10365
10519
  const mcp2 = await resolveMcp();
10366
10520
  let result;
10367
10521
  try {
@@ -10481,17 +10635,20 @@ async function writeAtomic8(filePath, content) {
10481
10635
  await fsPromises11.rename(tmpPath, filePath);
10482
10636
  }
10483
10637
  function getItemId4(fm) {
10484
- for (const key of ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id"]) {
10638
+ for (const key of ["story_id", "epic_id", "proposal_id", "sprint_id", "cr_id", "bug_id"]) {
10485
10639
  const val = fm[key];
10486
10640
  if (typeof val === "string" && val) return val;
10487
10641
  }
10488
10642
  return "unknown";
10489
10643
  }
10644
+ var SPRINT_RUNS_PATH_REGEX = /\.cleargate[\\/]sprint-runs[\\/]/;
10645
+ var SPRINT_REPORT_PATH_REGEX = /\.cleargate[\\/]sprint-runs[\\/]SPRINT-\d{2,}[\\/](REPORT|SPRINT-\d{2,}_REPORT)\.md$/;
10490
10646
  function getItemType2(fm) {
10491
10647
  const typeMap = {
10492
10648
  story_id: "story",
10493
10649
  epic_id: "epic",
10494
10650
  proposal_id: "proposal",
10651
+ sprint_id: "sprint",
10495
10652
  cr_id: "cr",
10496
10653
  bug_id: "bug"
10497
10654
  };
@@ -10500,6 +10657,10 @@ function getItemType2(fm) {
10500
10657
  }
10501
10658
  return null;
10502
10659
  }
10660
+ function getItemTypeWithPathOverride(localPath, fm) {
10661
+ if (SPRINT_REPORT_PATH_REGEX.test(localPath)) return "sprint_report";
10662
+ return getItemType2(fm);
10663
+ }
10503
10664
 
10504
10665
  // src/commands/conflicts.ts
10505
10666
  import * as fsPromises12 from "fs/promises";
@@ -10625,7 +10786,7 @@ function formatEntry(entry) {
10625
10786
  }
10626
10787
 
10627
10788
  // src/commands/admin-login.ts
10628
- import * as fs40 from "fs";
10789
+ import * as fs41 from "fs";
10629
10790
  import * as path51 from "path";
10630
10791
  import * as os7 from "os";
10631
10792
  var DEFAULT_MCP_URL = "http://localhost:3000";
@@ -10639,10 +10800,10 @@ function resolveAuthFilePath(opts) {
10639
10800
  }
10640
10801
  function writeAdminAuth(filePath, token) {
10641
10802
  const dir = path51.dirname(filePath);
10642
- fs40.mkdirSync(dir, { recursive: true });
10803
+ fs41.mkdirSync(dir, { recursive: true });
10643
10804
  const payload = JSON.stringify({ version: 1, token }, null, 2);
10644
- fs40.writeFileSync(filePath, payload, { encoding: "utf8", mode: 384 });
10645
- fs40.chmodSync(filePath, 384);
10805
+ fs41.writeFileSync(filePath, payload, { encoding: "utf8", mode: 384 });
10806
+ fs41.chmodSync(filePath, 384);
10646
10807
  }
10647
10808
  async function adminLoginHandler(opts = {}) {
10648
10809
  const fetchFn = opts.fetch ?? globalThis.fetch;
@@ -10751,7 +10912,7 @@ async function adminLoginHandler(opts = {}) {
10751
10912
  }
10752
10913
 
10753
10914
  // src/commands/hotfix.ts
10754
- import * as fs41 from "fs";
10915
+ import * as fs42 from "fs";
10755
10916
  import * as path52 from "path";
10756
10917
  function defaultExit4(code) {
10757
10918
  return process.exit(code);
@@ -10762,7 +10923,7 @@ function maxHotfixId(pendingDir) {
10762
10923
  let max = 0;
10763
10924
  let entries;
10764
10925
  try {
10765
- entries = fs41.readdirSync(pendingDir);
10926
+ entries = fs42.readdirSync(pendingDir);
10766
10927
  } catch {
10767
10928
  return 0;
10768
10929
  }
@@ -10782,7 +10943,7 @@ function countActiveHotfixes(repoRoot) {
10782
10943
  let count = 0;
10783
10944
  let pendingEntries = [];
10784
10945
  try {
10785
- pendingEntries = fs41.readdirSync(pendingDir);
10946
+ pendingEntries = fs42.readdirSync(pendingDir);
10786
10947
  } catch {
10787
10948
  }
10788
10949
  for (const entry of pendingEntries) {
@@ -10790,13 +10951,13 @@ function countActiveHotfixes(repoRoot) {
10790
10951
  }
10791
10952
  let archiveEntries = [];
10792
10953
  try {
10793
- archiveEntries = fs41.readdirSync(archiveDir);
10954
+ archiveEntries = fs42.readdirSync(archiveDir);
10794
10955
  } catch {
10795
10956
  }
10796
10957
  for (const entry of archiveEntries) {
10797
10958
  if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) {
10798
10959
  try {
10799
- const stat = fs41.statSync(path52.join(archiveDir, entry));
10960
+ const stat = fs42.statSync(path52.join(archiveDir, entry));
10800
10961
  if (stat.mtimeMs >= sevenDaysAgo) count++;
10801
10962
  } catch {
10802
10963
  }
@@ -10831,7 +10992,7 @@ function hotfixNewHandler(opts, cli) {
10831
10992
  const templatePath = resolveTemplatePath(repoRoot);
10832
10993
  let templateContent;
10833
10994
  try {
10834
- templateContent = fs41.readFileSync(templatePath, "utf8");
10995
+ templateContent = fs42.readFileSync(templatePath, "utf8");
10835
10996
  } catch {
10836
10997
  stderrFn(`[cleargate hotfix new] template not found: ${templatePath}`);
10837
10998
  return exitFn(2);
@@ -10841,8 +11002,8 @@ function hotfixNewHandler(opts, cli) {
10841
11002
  const fileName = `${idStr}_${fileSlug}.md`;
10842
11003
  const outPath = path52.join(pendingDir, fileName);
10843
11004
  try {
10844
- fs41.mkdirSync(pendingDir, { recursive: true });
10845
- fs41.writeFileSync(outPath, content, "utf8");
11005
+ fs42.mkdirSync(pendingDir, { recursive: true });
11006
+ fs42.writeFileSync(outPath, content, "utf8");
10846
11007
  } catch (err) {
10847
11008
  const msg = err instanceof Error ? err.message : String(err);
10848
11009
  stderrFn(`[cleargate hotfix new] write failed: ${msg}`);
@@ -10926,8 +11087,25 @@ var AuthFetcher = class {
10926
11087
  }
10927
11088
  };
10928
11089
 
11090
+ // src/auth/service-token-fetcher.ts
11091
+ var ServiceTokenFetcher = class {
11092
+ constructor(token) {
11093
+ this.token = token;
11094
+ }
11095
+ token;
11096
+ async getAccessToken() {
11097
+ return this.token;
11098
+ }
11099
+ };
11100
+
10929
11101
  // src/commands/mcp-serve.ts
10930
11102
  var DEFAULT_BASE_URL = "https://cleargate-mcp.soula.ge";
11103
+ var ServiceToken401Error = class extends Error {
11104
+ constructor() {
11105
+ super("service-token-401");
11106
+ this.name = "ServiceToken401Error";
11107
+ }
11108
+ };
10931
11109
  async function mcpServeHandler(opts) {
10932
11110
  const fetchFn = opts.fetch ?? globalThis.fetch;
10933
11111
  const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
@@ -10937,32 +11115,44 @@ async function mcpServeHandler(opts) {
10937
11115
  flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag }
10938
11116
  });
10939
11117
  const baseUrl = cfg.mcpUrl ?? DEFAULT_BASE_URL;
10940
- const store = await (opts.createStore ?? createTokenStore)({
10941
- ...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
10942
- ...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
10943
- });
10944
- const fetcher = new AuthFetcher({
10945
- baseUrl,
10946
- loadRefresh: () => store.load(opts.profile),
10947
- saveRefresh: (t) => store.save(opts.profile, t),
10948
- ...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
10949
- ...opts.now !== void 0 ? { now: opts.now } : {}
10950
- });
10951
- try {
10952
- await fetcher.getAccessToken();
10953
- } catch (err) {
10954
- if (err instanceof RefreshError) {
10955
- stderr(
10956
- `cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
11118
+ const serviceToken = process.env["CLEARGATE_SERVICE_TOKEN"] ?? "";
11119
+ let fetcher;
11120
+ let isServiceTokenMode;
11121
+ if (serviceToken.length > 0) {
11122
+ isServiceTokenMode = true;
11123
+ fetcher = new ServiceTokenFetcher(serviceToken);
11124
+ stderr("cleargate mcp serve: auth mode = service-token\n");
11125
+ } else {
11126
+ isServiceTokenMode = false;
11127
+ const store = await (opts.createStore ?? createTokenStore)({
11128
+ ...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
11129
+ ...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
11130
+ });
11131
+ const authFetcher = new AuthFetcher({
11132
+ baseUrl,
11133
+ loadRefresh: () => store.load(opts.profile),
11134
+ saveRefresh: (t) => store.save(opts.profile, t),
11135
+ ...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
11136
+ ...opts.now !== void 0 ? { now: opts.now } : {}
11137
+ });
11138
+ fetcher = authFetcher;
11139
+ stderr("cleargate mcp serve: auth mode = keychain-refresh\n");
11140
+ try {
11141
+ await authFetcher.getAccessToken();
11142
+ } catch (err) {
11143
+ if (err instanceof RefreshError) {
11144
+ stderr(
11145
+ `cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
10957
11146
  `
10958
- );
10959
- } else {
10960
- stderr(
10961
- `cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
11147
+ );
11148
+ } else {
11149
+ stderr(
11150
+ `cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
10962
11151
  `
10963
- );
11152
+ );
11153
+ }
11154
+ return exit(1);
10964
11155
  }
10965
- return exit(1);
10966
11156
  }
10967
11157
  const inputStream = opts.stdin ?? process.stdin;
10968
11158
  const rl = readline5.createInterface({
@@ -10973,8 +11163,23 @@ async function mcpServeHandler(opts) {
10973
11163
  for await (const line of rl) {
10974
11164
  if (!line.trim()) continue;
10975
11165
  try {
10976
- await proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr);
11166
+ await proxyOne(
11167
+ line,
11168
+ baseUrl,
11169
+ fetcher,
11170
+ isServiceTokenMode,
11171
+ fetchFn,
11172
+ stdout,
11173
+ stderr
11174
+ );
10977
11175
  } catch (err) {
11176
+ if (err instanceof ServiceToken401Error) {
11177
+ stderr(
11178
+ `cleargate mcp serve: CLEARGATE_SERVICE_TOKEN rejected by /mcp (401). Issue a new token in the admin console: Tokens \u2192 Issue \u2192 copy snippet.
11179
+ `
11180
+ );
11181
+ return exit(1);
11182
+ }
10978
11183
  const errMsg = err instanceof Error ? err.message : String(err);
10979
11184
  stderr(`cleargate mcp serve: proxy error: ${errMsg}
10980
11185
  `);
@@ -10991,7 +11196,7 @@ async function mcpServeHandler(opts) {
10991
11196
  }
10992
11197
  }
10993
11198
  }
10994
- async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
11199
+ async function proxyOne(line, baseUrl, fetcher, isServiceTokenMode, fetchFn, stdout, stderr) {
10995
11200
  let parsed;
10996
11201
  try {
10997
11202
  parsed = JSON.parse(line);
@@ -11004,6 +11209,9 @@ async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
11004
11209
  let access = await fetcher.getAccessToken();
11005
11210
  let res = await postFrame(baseUrl, line, access, fetchFn);
11006
11211
  if (res.status === 401) {
11212
+ if (isServiceTokenMode) {
11213
+ throw new ServiceToken401Error();
11214
+ }
11007
11215
  fetcher.invalidate();
11008
11216
  access = await fetcher.getAccessToken();
11009
11217
  res = await postFrame(baseUrl, line, access, fetchFn);
@@ -11222,8 +11430,8 @@ sprint.command("close <sprint-id>").description("close a sprint \u2014 validates
11222
11430
  }
11223
11431
  sprintCloseHandler(handlerOpts);
11224
11432
  });
11225
- sprint.command("reconcile-lifecycle <sprint-id>").description("CR-017: check lifecycle status of artifacts referenced in this sprint's commits (exits 1 on drift)").option("--since <iso-date>", "start of git log range (default: sprint start_date or 90 days ago)").option("--until <iso-date>", "end of git log range (default: now)").action((sprintId, opts) => {
11226
- reconcileLifecycleCliHandler({ sprintId, since: opts.since, until: opts.until });
11433
+ sprint.command("reconcile-lifecycle <sprint-id>").description("CR-017: check lifecycle status of artifacts referenced in this sprint's commits (exits 1 on drift)").option("--since <iso-date>", "start of git log range (default: sprint start_date or 90 days ago)").option("--until <iso-date>", "end of git log range (default: now)").option("--parents", "audit parent (Epic/Sprint) rollup statuses; read-only (CR-066)").action((sprintId, opts) => {
11434
+ reconcileLifecycleCliHandler({ sprintId, since: opts.since, until: opts.until, parents: opts.parents });
11227
11435
  });
11228
11436
  sprint.command("archive <sprint-id>").description("archive a completed sprint \u2014 move pending-sync files, clear .active, merge + delete sprint branch").option("--dry-run", "print the archive plan without making any changes").option("--allow-wiki-lint-debt", "CR-022 M5: waive wiki-lint findings during archive (mirrors --allow-drift pattern)").action(async (sprintId, opts) => {
11229
11437
  const handlerOpts = { sprintId };