cleargate 0.11.5 → 0.12.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.
- package/dist/MANIFEST.json +5 -5
- package/dist/admin-api/index.cjs +3 -1
- package/dist/admin-api/index.cjs.map +1 -1
- package/dist/admin-api/index.d.cts +1 -0
- package/dist/admin-api/index.d.ts +1 -0
- package/dist/admin-api/index.js +3 -1
- package/dist/admin-api/index.js.map +1 -1
- package/dist/cli.cjs +245 -72
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +244 -72
- package/dist/cli.js.map +1 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +6 -4
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +138 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +89 -0
- package/dist/templates/cleargate-planning/CLAUDE.md +2 -0
- package/dist/templates/cleargate-planning/MANIFEST.json +5 -5
- package/package.json +1 -1
- package/templates/cleargate-planning/.claude/agents/architect.md +6 -4
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +138 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +89 -0
- package/templates/cleargate-planning/CLAUDE.md +2 -0
- package/templates/cleargate-planning/MANIFEST.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { Command } from "commander";
|
|
|
22
22
|
// package.json
|
|
23
23
|
var package_default = {
|
|
24
24
|
name: "cleargate",
|
|
25
|
-
version: "0.
|
|
25
|
+
version: "0.12.0",
|
|
26
26
|
private: false,
|
|
27
27
|
type: "module",
|
|
28
28
|
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.",
|
|
@@ -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
|
-
|
|
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
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
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
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
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
|
-
|
|
3165
|
-
|
|
3190
|
+
exit(0);
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3166
3193
|
}
|
|
3167
3194
|
const filename = path17.basename(absRawPath);
|
|
3168
3195
|
let bucketInfo;
|
|
3169
3196
|
try {
|
|
3170
|
-
|
|
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
|
-
|
|
3214
|
-
|
|
3244
|
+
let existingPage;
|
|
3245
|
+
if (pageExists) {
|
|
3215
3246
|
try {
|
|
3216
3247
|
const existingPageContent = fs17.readFileSync(pagePath, "utf8");
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = {};
|
|
@@ -7037,6 +7135,10 @@ function refreshScopedGateCaches(sprintId, cwd, execFn) {
|
|
|
7037
7135
|
if (!absPath) {
|
|
7038
7136
|
continue;
|
|
7039
7137
|
}
|
|
7138
|
+
if (absPath.includes(`${path31.sep}archive${path31.sep}`)) {
|
|
7139
|
+
result.skipped.push(id);
|
|
7140
|
+
continue;
|
|
7141
|
+
}
|
|
7040
7142
|
let status = "";
|
|
7041
7143
|
try {
|
|
7042
7144
|
const raw = fs30.readFileSync(absPath, "utf8");
|
|
@@ -10320,6 +10422,19 @@ async function pushHandler(fileOrId, opts = {}) {
|
|
|
10320
10422
|
async function handlePush(filePath, ctx) {
|
|
10321
10423
|
const { projectRoot, identity, sprintRoot, nowFn, resolveMcp, stdout, stderr, exit } = ctx;
|
|
10322
10424
|
const resolvedPath = path49.isAbsolute(filePath) ? filePath : path49.resolve(projectRoot, filePath);
|
|
10425
|
+
if (SPRINT_RUNS_PATH_REGEX.test(resolvedPath)) {
|
|
10426
|
+
if (!SPRINT_REPORT_PATH_REGEX.test(resolvedPath)) {
|
|
10427
|
+
stderr(
|
|
10428
|
+
`Error: path not in allowlist. Only sprint report files are accepted from sprint-runs/.
|
|
10429
|
+
Allowed: .cleargate/sprint-runs/SPRINT-NN/REPORT.md
|
|
10430
|
+
.cleargate/sprint-runs/SPRINT-NN/SPRINT-NN_REPORT.md
|
|
10431
|
+
Got: "${resolvedPath}"
|
|
10432
|
+
`
|
|
10433
|
+
);
|
|
10434
|
+
exit(2);
|
|
10435
|
+
return;
|
|
10436
|
+
}
|
|
10437
|
+
}
|
|
10323
10438
|
let rawContent;
|
|
10324
10439
|
try {
|
|
10325
10440
|
rawContent = await fsPromises11.readFile(resolvedPath, "utf8");
|
|
@@ -10349,7 +10464,7 @@ async function handlePush(filePath, ctx) {
|
|
|
10349
10464
|
return;
|
|
10350
10465
|
}
|
|
10351
10466
|
const itemId = getItemId4(fm);
|
|
10352
|
-
const type =
|
|
10467
|
+
const type = getItemTypeWithPathOverride(resolvedPath, fm);
|
|
10353
10468
|
if (!type) {
|
|
10354
10469
|
stderr(`Error: cannot determine item type from frontmatter in "${resolvedPath}".
|
|
10355
10470
|
`);
|
|
@@ -10362,6 +10477,9 @@ async function handlePush(filePath, ctx) {
|
|
|
10362
10477
|
if (h1) payloadForPush["title"] = h1;
|
|
10363
10478
|
}
|
|
10364
10479
|
payloadForPush["body"] = body;
|
|
10480
|
+
if (payloadForPush["origin"] === void 0) {
|
|
10481
|
+
payloadForPush["origin"] = "cleargate-cli";
|
|
10482
|
+
}
|
|
10365
10483
|
const mcp2 = await resolveMcp();
|
|
10366
10484
|
let result;
|
|
10367
10485
|
try {
|
|
@@ -10481,17 +10599,20 @@ async function writeAtomic8(filePath, content) {
|
|
|
10481
10599
|
await fsPromises11.rename(tmpPath, filePath);
|
|
10482
10600
|
}
|
|
10483
10601
|
function getItemId4(fm) {
|
|
10484
|
-
for (const key of ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id"]) {
|
|
10602
|
+
for (const key of ["story_id", "epic_id", "proposal_id", "sprint_id", "cr_id", "bug_id"]) {
|
|
10485
10603
|
const val = fm[key];
|
|
10486
10604
|
if (typeof val === "string" && val) return val;
|
|
10487
10605
|
}
|
|
10488
10606
|
return "unknown";
|
|
10489
10607
|
}
|
|
10608
|
+
var SPRINT_RUNS_PATH_REGEX = /\.cleargate[\\/]sprint-runs[\\/]/;
|
|
10609
|
+
var SPRINT_REPORT_PATH_REGEX = /\.cleargate[\\/]sprint-runs[\\/]SPRINT-\d{2,}[\\/](REPORT|SPRINT-\d{2,}_REPORT)\.md$/;
|
|
10490
10610
|
function getItemType2(fm) {
|
|
10491
10611
|
const typeMap = {
|
|
10492
10612
|
story_id: "story",
|
|
10493
10613
|
epic_id: "epic",
|
|
10494
10614
|
proposal_id: "proposal",
|
|
10615
|
+
sprint_id: "sprint",
|
|
10495
10616
|
cr_id: "cr",
|
|
10496
10617
|
bug_id: "bug"
|
|
10497
10618
|
};
|
|
@@ -10500,6 +10621,10 @@ function getItemType2(fm) {
|
|
|
10500
10621
|
}
|
|
10501
10622
|
return null;
|
|
10502
10623
|
}
|
|
10624
|
+
function getItemTypeWithPathOverride(localPath, fm) {
|
|
10625
|
+
if (SPRINT_REPORT_PATH_REGEX.test(localPath)) return "sprint_report";
|
|
10626
|
+
return getItemType2(fm);
|
|
10627
|
+
}
|
|
10503
10628
|
|
|
10504
10629
|
// src/commands/conflicts.ts
|
|
10505
10630
|
import * as fsPromises12 from "fs/promises";
|
|
@@ -10926,8 +11051,25 @@ var AuthFetcher = class {
|
|
|
10926
11051
|
}
|
|
10927
11052
|
};
|
|
10928
11053
|
|
|
11054
|
+
// src/auth/service-token-fetcher.ts
|
|
11055
|
+
var ServiceTokenFetcher = class {
|
|
11056
|
+
constructor(token) {
|
|
11057
|
+
this.token = token;
|
|
11058
|
+
}
|
|
11059
|
+
token;
|
|
11060
|
+
async getAccessToken() {
|
|
11061
|
+
return this.token;
|
|
11062
|
+
}
|
|
11063
|
+
};
|
|
11064
|
+
|
|
10929
11065
|
// src/commands/mcp-serve.ts
|
|
10930
11066
|
var DEFAULT_BASE_URL = "https://cleargate-mcp.soula.ge";
|
|
11067
|
+
var ServiceToken401Error = class extends Error {
|
|
11068
|
+
constructor() {
|
|
11069
|
+
super("service-token-401");
|
|
11070
|
+
this.name = "ServiceToken401Error";
|
|
11071
|
+
}
|
|
11072
|
+
};
|
|
10931
11073
|
async function mcpServeHandler(opts) {
|
|
10932
11074
|
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
10933
11075
|
const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
|
|
@@ -10937,32 +11079,44 @@ async function mcpServeHandler(opts) {
|
|
|
10937
11079
|
flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag }
|
|
10938
11080
|
});
|
|
10939
11081
|
const baseUrl = cfg.mcpUrl ?? DEFAULT_BASE_URL;
|
|
10940
|
-
const
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
11082
|
+
const serviceToken = process.env["CLEARGATE_SERVICE_TOKEN"] ?? "";
|
|
11083
|
+
let fetcher;
|
|
11084
|
+
let isServiceTokenMode;
|
|
11085
|
+
if (serviceToken.length > 0) {
|
|
11086
|
+
isServiceTokenMode = true;
|
|
11087
|
+
fetcher = new ServiceTokenFetcher(serviceToken);
|
|
11088
|
+
stderr("cleargate mcp serve: auth mode = service-token\n");
|
|
11089
|
+
} else {
|
|
11090
|
+
isServiceTokenMode = false;
|
|
11091
|
+
const store = await (opts.createStore ?? createTokenStore)({
|
|
11092
|
+
...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
|
|
11093
|
+
...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
|
|
11094
|
+
});
|
|
11095
|
+
const authFetcher = new AuthFetcher({
|
|
11096
|
+
baseUrl,
|
|
11097
|
+
loadRefresh: () => store.load(opts.profile),
|
|
11098
|
+
saveRefresh: (t) => store.save(opts.profile, t),
|
|
11099
|
+
...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
|
|
11100
|
+
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
11101
|
+
});
|
|
11102
|
+
fetcher = authFetcher;
|
|
11103
|
+
stderr("cleargate mcp serve: auth mode = keychain-refresh\n");
|
|
11104
|
+
try {
|
|
11105
|
+
await authFetcher.getAccessToken();
|
|
11106
|
+
} catch (err) {
|
|
11107
|
+
if (err instanceof RefreshError) {
|
|
11108
|
+
stderr(
|
|
11109
|
+
`cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
|
|
10957
11110
|
`
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
11111
|
+
);
|
|
11112
|
+
} else {
|
|
11113
|
+
stderr(
|
|
11114
|
+
`cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
|
|
10962
11115
|
`
|
|
10963
|
-
|
|
11116
|
+
);
|
|
11117
|
+
}
|
|
11118
|
+
return exit(1);
|
|
10964
11119
|
}
|
|
10965
|
-
return exit(1);
|
|
10966
11120
|
}
|
|
10967
11121
|
const inputStream = opts.stdin ?? process.stdin;
|
|
10968
11122
|
const rl = readline5.createInterface({
|
|
@@ -10973,8 +11127,23 @@ async function mcpServeHandler(opts) {
|
|
|
10973
11127
|
for await (const line of rl) {
|
|
10974
11128
|
if (!line.trim()) continue;
|
|
10975
11129
|
try {
|
|
10976
|
-
await proxyOne(
|
|
11130
|
+
await proxyOne(
|
|
11131
|
+
line,
|
|
11132
|
+
baseUrl,
|
|
11133
|
+
fetcher,
|
|
11134
|
+
isServiceTokenMode,
|
|
11135
|
+
fetchFn,
|
|
11136
|
+
stdout,
|
|
11137
|
+
stderr
|
|
11138
|
+
);
|
|
10977
11139
|
} catch (err) {
|
|
11140
|
+
if (err instanceof ServiceToken401Error) {
|
|
11141
|
+
stderr(
|
|
11142
|
+
`cleargate mcp serve: CLEARGATE_SERVICE_TOKEN rejected by /mcp (401). Issue a new token in the admin console: Tokens \u2192 Issue \u2192 copy snippet.
|
|
11143
|
+
`
|
|
11144
|
+
);
|
|
11145
|
+
return exit(1);
|
|
11146
|
+
}
|
|
10978
11147
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
10979
11148
|
stderr(`cleargate mcp serve: proxy error: ${errMsg}
|
|
10980
11149
|
`);
|
|
@@ -10991,7 +11160,7 @@ async function mcpServeHandler(opts) {
|
|
|
10991
11160
|
}
|
|
10992
11161
|
}
|
|
10993
11162
|
}
|
|
10994
|
-
async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
|
|
11163
|
+
async function proxyOne(line, baseUrl, fetcher, isServiceTokenMode, fetchFn, stdout, stderr) {
|
|
10995
11164
|
let parsed;
|
|
10996
11165
|
try {
|
|
10997
11166
|
parsed = JSON.parse(line);
|
|
@@ -11004,6 +11173,9 @@ async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
|
|
|
11004
11173
|
let access = await fetcher.getAccessToken();
|
|
11005
11174
|
let res = await postFrame(baseUrl, line, access, fetchFn);
|
|
11006
11175
|
if (res.status === 401) {
|
|
11176
|
+
if (isServiceTokenMode) {
|
|
11177
|
+
throw new ServiceToken401Error();
|
|
11178
|
+
}
|
|
11007
11179
|
fetcher.invalidate();
|
|
11008
11180
|
access = await fetcher.getAccessToken();
|
|
11009
11181
|
res = await postFrame(baseUrl, line, access, fetchFn);
|