@harness-engineering/core 0.16.0 → 0.17.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/index.mjs CHANGED
@@ -10058,6 +10058,7 @@ var VALID_STATUSES = /* @__PURE__ */ new Set([
10058
10058
  "blocked"
10059
10059
  ]);
10060
10060
  var EM_DASH = "\u2014";
10061
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
10061
10062
  function parseRoadmap(markdown) {
10062
10063
  const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
10063
10064
  if (!fmMatch) {
@@ -10068,9 +10069,12 @@ function parseRoadmap(markdown) {
10068
10069
  const body = markdown.slice(fmMatch[0].length);
10069
10070
  const milestonesResult = parseMilestones(body);
10070
10071
  if (!milestonesResult.ok) return milestonesResult;
10072
+ const historyResult = parseAssignmentHistory(body);
10073
+ if (!historyResult.ok) return historyResult;
10071
10074
  return Ok2({
10072
10075
  frontmatter: fmResult.value,
10073
- milestones: milestonesResult.value
10076
+ milestones: milestonesResult.value,
10077
+ assignmentHistory: historyResult.value
10074
10078
  });
10075
10079
  }
10076
10080
  function parseFrontmatter2(raw) {
@@ -10110,12 +10114,17 @@ function parseMilestones(body) {
10110
10114
  const h2Pattern = /^## (.+)$/gm;
10111
10115
  const h2Matches = [];
10112
10116
  let match;
10117
+ let bodyEnd = body.length;
10113
10118
  while ((match = h2Pattern.exec(body)) !== null) {
10119
+ if (match[1] === "Assignment History") {
10120
+ bodyEnd = match.index;
10121
+ break;
10122
+ }
10114
10123
  h2Matches.push({ heading: match[1], startIndex: match.index, fullMatch: match[0] });
10115
10124
  }
10116
10125
  for (let i = 0; i < h2Matches.length; i++) {
10117
10126
  const h2 = h2Matches[i];
10118
- const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : body.length;
10127
+ const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : bodyEnd;
10119
10128
  const sectionBody = body.slice(h2.startIndex + h2.fullMatch.length, nextStart);
10120
10129
  const isBacklog = h2.heading === "Backlog";
10121
10130
  const milestoneName = isBacklog ? "Backlog" : h2.heading.replace(/^Milestone:\s*/, "");
@@ -10181,15 +10190,60 @@ function parseFeatureFields(name, body) {
10181
10190
  const specRaw = fieldMap.get("Spec") ?? EM_DASH;
10182
10191
  const plans = parseListField(fieldMap, "Plans", "Plan");
10183
10192
  const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
10193
+ const assigneeRaw = fieldMap.get("Assignee") ?? EM_DASH;
10194
+ const priorityRaw = fieldMap.get("Priority") ?? EM_DASH;
10195
+ const externalIdRaw = fieldMap.get("External-ID") ?? EM_DASH;
10196
+ if (priorityRaw !== EM_DASH && !VALID_PRIORITIES.has(priorityRaw)) {
10197
+ return Err2(
10198
+ new Error(
10199
+ `Feature "${name}" has invalid priority: "${priorityRaw}". Valid priorities: ${[...VALID_PRIORITIES].join(", ")}`
10200
+ )
10201
+ );
10202
+ }
10184
10203
  return Ok2({
10185
10204
  name,
10186
10205
  status: statusRaw,
10187
10206
  spec: specRaw === EM_DASH ? null : specRaw,
10188
10207
  plans,
10189
10208
  blockedBy,
10190
- summary: fieldMap.get("Summary") ?? ""
10209
+ summary: fieldMap.get("Summary") ?? "",
10210
+ assignee: assigneeRaw === EM_DASH ? null : assigneeRaw,
10211
+ priority: priorityRaw === EM_DASH ? null : priorityRaw,
10212
+ externalId: externalIdRaw === EM_DASH ? null : externalIdRaw
10191
10213
  });
10192
10214
  }
10215
+ function parseAssignmentHistory(body) {
10216
+ const historyMatch = body.match(/^## Assignment History\s*\n/m);
10217
+ if (!historyMatch || historyMatch.index === void 0) return Ok2([]);
10218
+ const historyStart = historyMatch.index + historyMatch[0].length;
10219
+ const rawHistoryBody = body.slice(historyStart);
10220
+ const nextH2 = rawHistoryBody.search(/^## /m);
10221
+ const historyBody = nextH2 === -1 ? rawHistoryBody : rawHistoryBody.slice(0, nextH2);
10222
+ const records = [];
10223
+ const lines = historyBody.split("\n");
10224
+ let pastHeader = false;
10225
+ for (const line of lines) {
10226
+ const trimmed = line.trim();
10227
+ if (!trimmed.startsWith("|")) continue;
10228
+ if (!pastHeader) {
10229
+ if (trimmed.match(/^\|[-\s|]+\|$/)) {
10230
+ pastHeader = true;
10231
+ }
10232
+ continue;
10233
+ }
10234
+ const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
10235
+ if (cells.length < 4) continue;
10236
+ const action = cells[2];
10237
+ if (!["assigned", "completed", "unassigned"].includes(action)) continue;
10238
+ records.push({
10239
+ feature: cells[0],
10240
+ assignee: cells[1],
10241
+ action,
10242
+ date: cells[3]
10243
+ });
10244
+ }
10245
+ return Ok2(records);
10246
+ }
10193
10247
 
10194
10248
  // src/roadmap/serialize.ts
10195
10249
  var EM_DASH2 = "\u2014";
@@ -10217,6 +10271,10 @@ function serializeRoadmap(roadmap) {
10217
10271
  lines.push(...serializeFeature(feature));
10218
10272
  }
10219
10273
  }
10274
+ if (roadmap.assignmentHistory && roadmap.assignmentHistory.length > 0) {
10275
+ lines.push("");
10276
+ lines.push(...serializeAssignmentHistory(roadmap.assignmentHistory));
10277
+ }
10220
10278
  lines.push("");
10221
10279
  return lines.join("\n");
10222
10280
  }
@@ -10227,7 +10285,7 @@ function serializeFeature(feature) {
10227
10285
  const spec = feature.spec ?? EM_DASH2;
10228
10286
  const plans = feature.plans.length > 0 ? feature.plans.join(", ") : EM_DASH2;
10229
10287
  const blockedBy = feature.blockedBy.length > 0 ? feature.blockedBy.join(", ") : EM_DASH2;
10230
- return [
10288
+ const lines = [
10231
10289
  `### ${feature.name}`,
10232
10290
  "",
10233
10291
  `- **Status:** ${feature.status}`,
@@ -10236,12 +10294,45 @@ function serializeFeature(feature) {
10236
10294
  `- **Blockers:** ${blockedBy}`,
10237
10295
  `- **Plan:** ${plans}`
10238
10296
  ];
10297
+ const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
10298
+ if (hasExtended) {
10299
+ lines.push(`- **Assignee:** ${feature.assignee ?? EM_DASH2}`);
10300
+ lines.push(`- **Priority:** ${feature.priority ?? EM_DASH2}`);
10301
+ lines.push(`- **External-ID:** ${feature.externalId ?? EM_DASH2}`);
10302
+ }
10303
+ return lines;
10304
+ }
10305
+ function serializeAssignmentHistory(records) {
10306
+ const lines = [
10307
+ "## Assignment History",
10308
+ "| Feature | Assignee | Action | Date |",
10309
+ "|---------|----------|--------|------|"
10310
+ ];
10311
+ for (const record of records) {
10312
+ lines.push(`| ${record.feature} | ${record.assignee} | ${record.action} | ${record.date} |`);
10313
+ }
10314
+ return lines;
10239
10315
  }
10240
10316
 
10241
10317
  // src/roadmap/sync.ts
10242
10318
  import * as fs19 from "fs";
10243
10319
  import * as path19 from "path";
10244
10320
  import { Ok as Ok3 } from "@harness-engineering/types";
10321
+
10322
+ // src/roadmap/status-rank.ts
10323
+ var STATUS_RANK = {
10324
+ backlog: 0,
10325
+ planned: 1,
10326
+ blocked: 1,
10327
+ // lateral to planned — sync can move to/from blocked freely
10328
+ "in-progress": 2,
10329
+ done: 3
10330
+ };
10331
+ function isRegression(from, to) {
10332
+ return STATUS_RANK[to] < STATUS_RANK[from];
10333
+ }
10334
+
10335
+ // src/roadmap/sync.ts
10245
10336
  function inferStatus(feature, projectPath, allFeatures) {
10246
10337
  if (feature.blockedBy.length > 0) {
10247
10338
  const blockerNotDone = feature.blockedBy.some((blockerName) => {
@@ -10308,17 +10399,6 @@ function inferStatus(feature, projectPath, allFeatures) {
10308
10399
  if (anyStarted) return "in-progress";
10309
10400
  return null;
10310
10401
  }
10311
- var STATUS_RANK = {
10312
- backlog: 0,
10313
- planned: 1,
10314
- blocked: 1,
10315
- // lateral to planned — sync can move to/from blocked freely
10316
- "in-progress": 2,
10317
- done: 3
10318
- };
10319
- function isRegression(from, to) {
10320
- return STATUS_RANK[to] < STATUS_RANK[from];
10321
- }
10322
10402
  function syncRoadmap(options) {
10323
10403
  const { projectPath, roadmap, forceSync } = options;
10324
10404
  const allFeatures = roadmap.milestones.flatMap((m) => m.features);
@@ -10349,6 +10429,487 @@ function applySyncChanges(roadmap, changes) {
10349
10429
  roadmap.frontmatter.lastSynced = (/* @__PURE__ */ new Date()).toISOString();
10350
10430
  }
10351
10431
 
10432
+ // src/roadmap/tracker-sync.ts
10433
+ function resolveReverseStatus(externalStatus, labels, config) {
10434
+ const reverseMap = config.reverseStatusMap;
10435
+ if (!reverseMap) return null;
10436
+ if (reverseMap[externalStatus]) {
10437
+ return reverseMap[externalStatus];
10438
+ }
10439
+ const statusLabels = ["in-progress", "blocked", "planned"];
10440
+ const matchingLabels = labels.filter((l) => statusLabels.includes(l));
10441
+ if (matchingLabels.length === 1) {
10442
+ const compoundKey = `${externalStatus}:${matchingLabels[0]}`;
10443
+ if (reverseMap[compoundKey]) {
10444
+ return reverseMap[compoundKey];
10445
+ }
10446
+ }
10447
+ return null;
10448
+ }
10449
+
10450
+ // src/roadmap/adapters/github-issues.ts
10451
+ import { Ok as Ok4, Err as Err3 } from "@harness-engineering/types";
10452
+ function parseExternalId(externalId) {
10453
+ const match = externalId.match(/^github:([^/]+)\/([^#]+)#(\d+)$/);
10454
+ if (!match) return null;
10455
+ return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
10456
+ }
10457
+ function buildExternalId(owner, repo, number) {
10458
+ return `github:${owner}/${repo}#${number}`;
10459
+ }
10460
+ function labelsForStatus(status, config) {
10461
+ const base = config.labels ?? [];
10462
+ const externalStatus = config.statusMap[status];
10463
+ if (externalStatus === "open" && status !== "backlog") {
10464
+ return [...base, status];
10465
+ }
10466
+ return [...base];
10467
+ }
10468
+ var RETRY_DEFAULTS = { maxRetries: 5, baseDelayMs: 1e3 };
10469
+ function sleep(ms) {
10470
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
10471
+ }
10472
+ async function fetchWithRetry(fetchFn, input, init, opts = RETRY_DEFAULTS) {
10473
+ let lastResponse;
10474
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
10475
+ const response = await fetchFn(input, init);
10476
+ if (response.status !== 403 && response.status !== 429) return response;
10477
+ lastResponse = response;
10478
+ if (attempt === opts.maxRetries) break;
10479
+ const retryAfter = response.headers.get("Retry-After");
10480
+ let delayMs;
10481
+ if (retryAfter) {
10482
+ const seconds = parseInt(retryAfter, 10);
10483
+ delayMs = isNaN(seconds) ? opts.baseDelayMs : seconds * 1e3;
10484
+ } else {
10485
+ delayMs = opts.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500;
10486
+ }
10487
+ await sleep(delayMs);
10488
+ }
10489
+ return lastResponse;
10490
+ }
10491
+ var GitHubIssuesSyncAdapter = class {
10492
+ token;
10493
+ config;
10494
+ fetchFn;
10495
+ apiBase;
10496
+ owner;
10497
+ repo;
10498
+ retryOpts;
10499
+ constructor(options) {
10500
+ this.token = options.token;
10501
+ this.config = options.config;
10502
+ this.fetchFn = options.fetchFn ?? globalThis.fetch;
10503
+ this.apiBase = options.apiBase ?? "https://api.github.com";
10504
+ this.retryOpts = {
10505
+ maxRetries: options.maxRetries ?? RETRY_DEFAULTS.maxRetries,
10506
+ baseDelayMs: options.baseDelayMs ?? RETRY_DEFAULTS.baseDelayMs
10507
+ };
10508
+ const repoParts = (options.config.repo ?? "").split("/");
10509
+ if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
10510
+ throw new Error(`Invalid repo format: "${options.config.repo}". Expected "owner/repo".`);
10511
+ }
10512
+ this.owner = repoParts[0];
10513
+ this.repo = repoParts[1];
10514
+ }
10515
+ headers() {
10516
+ return {
10517
+ Authorization: `Bearer ${this.token}`,
10518
+ Accept: "application/vnd.github+json",
10519
+ "Content-Type": "application/json",
10520
+ "X-GitHub-Api-Version": "2022-11-28"
10521
+ };
10522
+ }
10523
+ /**
10524
+ * Close an issue if the feature status maps to 'closed'.
10525
+ * GitHub Issues API doesn't accept state on POST — requires a follow-up PATCH.
10526
+ */
10527
+ async closeIfDone(issueNumber, featureStatus) {
10528
+ const externalStatus = this.config.statusMap[featureStatus];
10529
+ if (externalStatus !== "closed") return;
10530
+ await fetchWithRetry(
10531
+ this.fetchFn,
10532
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues/${issueNumber}`,
10533
+ { method: "PATCH", headers: this.headers(), body: JSON.stringify({ state: "closed" }) },
10534
+ this.retryOpts
10535
+ );
10536
+ }
10537
+ async createTicket(feature, milestone) {
10538
+ try {
10539
+ const labels = labelsForStatus(feature.status, this.config);
10540
+ const body = [
10541
+ feature.summary,
10542
+ "",
10543
+ `**Milestone:** ${milestone}`,
10544
+ feature.spec ? `**Spec:** ${feature.spec}` : ""
10545
+ ].filter(Boolean).join("\n");
10546
+ const response = await fetchWithRetry(
10547
+ this.fetchFn,
10548
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues`,
10549
+ {
10550
+ method: "POST",
10551
+ headers: this.headers(),
10552
+ body: JSON.stringify({ title: feature.name, body, labels })
10553
+ },
10554
+ this.retryOpts
10555
+ );
10556
+ if (!response.ok) {
10557
+ const text = await response.text();
10558
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10559
+ }
10560
+ const data = await response.json();
10561
+ const externalId = buildExternalId(this.owner, this.repo, data.number);
10562
+ await this.closeIfDone(data.number, feature.status);
10563
+ return Ok4({ externalId, url: data.html_url });
10564
+ } catch (error) {
10565
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10566
+ }
10567
+ }
10568
+ async updateTicket(externalId, changes) {
10569
+ try {
10570
+ const parsed = parseExternalId(externalId);
10571
+ if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
10572
+ const patch = {};
10573
+ if (changes.name !== void 0) patch.title = changes.name;
10574
+ if (changes.summary !== void 0) {
10575
+ const body = [changes.summary, "", changes.spec ? `**Spec:** ${changes.spec}` : ""].filter(Boolean).join("\n");
10576
+ patch.body = body;
10577
+ }
10578
+ if (changes.status !== void 0) {
10579
+ const externalStatus = this.config.statusMap[changes.status];
10580
+ patch.state = externalStatus;
10581
+ patch.labels = labelsForStatus(changes.status, this.config);
10582
+ }
10583
+ const response = await fetchWithRetry(
10584
+ this.fetchFn,
10585
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
10586
+ {
10587
+ method: "PATCH",
10588
+ headers: this.headers(),
10589
+ body: JSON.stringify(patch)
10590
+ },
10591
+ this.retryOpts
10592
+ );
10593
+ if (!response.ok) {
10594
+ const text = await response.text();
10595
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10596
+ }
10597
+ const data = await response.json();
10598
+ return Ok4({ externalId, url: data.html_url });
10599
+ } catch (error) {
10600
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10601
+ }
10602
+ }
10603
+ async fetchTicketState(externalId) {
10604
+ try {
10605
+ const parsed = parseExternalId(externalId);
10606
+ if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
10607
+ const response = await fetchWithRetry(
10608
+ this.fetchFn,
10609
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
10610
+ {
10611
+ method: "GET",
10612
+ headers: this.headers()
10613
+ },
10614
+ this.retryOpts
10615
+ );
10616
+ if (!response.ok) {
10617
+ const text = await response.text();
10618
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10619
+ }
10620
+ const data = await response.json();
10621
+ return Ok4({
10622
+ externalId,
10623
+ status: data.state,
10624
+ labels: data.labels.map((l) => l.name),
10625
+ assignee: data.assignee ? `@${data.assignee.login}` : null
10626
+ });
10627
+ } catch (error) {
10628
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10629
+ }
10630
+ }
10631
+ async fetchAllTickets() {
10632
+ try {
10633
+ const filterLabels = this.config.labels ?? [];
10634
+ const labelsParam = filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
10635
+ const tickets = [];
10636
+ let page = 1;
10637
+ const perPage = 100;
10638
+ while (true) {
10639
+ const response = await fetchWithRetry(
10640
+ this.fetchFn,
10641
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
10642
+ {
10643
+ method: "GET",
10644
+ headers: this.headers()
10645
+ },
10646
+ this.retryOpts
10647
+ );
10648
+ if (!response.ok) {
10649
+ const text = await response.text();
10650
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10651
+ }
10652
+ const data = await response.json();
10653
+ const issues = data.filter((d) => !d.pull_request);
10654
+ for (const issue of issues) {
10655
+ tickets.push({
10656
+ externalId: buildExternalId(this.owner, this.repo, issue.number),
10657
+ status: issue.state,
10658
+ labels: issue.labels.map((l) => l.name),
10659
+ assignee: issue.assignee ? `@${issue.assignee.login}` : null
10660
+ });
10661
+ }
10662
+ if (data.length < perPage) break;
10663
+ page++;
10664
+ }
10665
+ return Ok4(tickets);
10666
+ } catch (error) {
10667
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10668
+ }
10669
+ }
10670
+ async assignTicket(externalId, assignee) {
10671
+ try {
10672
+ const parsed = parseExternalId(externalId);
10673
+ if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
10674
+ const login = assignee.startsWith("@") ? assignee.slice(1) : assignee;
10675
+ const response = await fetchWithRetry(
10676
+ this.fetchFn,
10677
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/assignees`,
10678
+ {
10679
+ method: "POST",
10680
+ headers: this.headers(),
10681
+ body: JSON.stringify({ assignees: [login] })
10682
+ },
10683
+ this.retryOpts
10684
+ );
10685
+ if (!response.ok) {
10686
+ const text = await response.text();
10687
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10688
+ }
10689
+ return Ok4(void 0);
10690
+ } catch (error) {
10691
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10692
+ }
10693
+ }
10694
+ };
10695
+
10696
+ // src/roadmap/sync-engine.ts
10697
+ import * as fs20 from "fs";
10698
+ function emptySyncResult() {
10699
+ return { created: [], updated: [], assignmentChanges: [], errors: [] };
10700
+ }
10701
+ async function syncToExternal(roadmap, adapter, _config) {
10702
+ const result = emptySyncResult();
10703
+ for (const milestone of roadmap.milestones) {
10704
+ for (const feature of milestone.features) {
10705
+ if (!feature.externalId) {
10706
+ const createResult = await adapter.createTicket(feature, milestone.name);
10707
+ if (createResult.ok) {
10708
+ feature.externalId = createResult.value.externalId;
10709
+ result.created.push(createResult.value);
10710
+ } else {
10711
+ result.errors.push({ featureOrId: feature.name, error: createResult.error });
10712
+ }
10713
+ } else {
10714
+ const updateResult = await adapter.updateTicket(feature.externalId, feature);
10715
+ if (updateResult.ok) {
10716
+ result.updated.push(feature.externalId);
10717
+ } else {
10718
+ result.errors.push({ featureOrId: feature.externalId, error: updateResult.error });
10719
+ }
10720
+ }
10721
+ }
10722
+ }
10723
+ return result;
10724
+ }
10725
+ async function syncFromExternal(roadmap, adapter, config, options) {
10726
+ const result = emptySyncResult();
10727
+ const forceSync = options?.forceSync ?? false;
10728
+ const featureByExternalId = /* @__PURE__ */ new Map();
10729
+ for (const milestone of roadmap.milestones) {
10730
+ for (const feature of milestone.features) {
10731
+ if (feature.externalId) {
10732
+ featureByExternalId.set(feature.externalId, feature);
10733
+ }
10734
+ }
10735
+ }
10736
+ if (featureByExternalId.size === 0) return result;
10737
+ const fetchResult = await adapter.fetchAllTickets();
10738
+ if (!fetchResult.ok) {
10739
+ result.errors.push({ featureOrId: "*", error: fetchResult.error });
10740
+ return result;
10741
+ }
10742
+ for (const ticketState of fetchResult.value) {
10743
+ const feature = featureByExternalId.get(ticketState.externalId);
10744
+ if (!feature) continue;
10745
+ if (ticketState.assignee !== feature.assignee) {
10746
+ result.assignmentChanges.push({
10747
+ feature: feature.name,
10748
+ from: feature.assignee,
10749
+ to: ticketState.assignee
10750
+ });
10751
+ feature.assignee = ticketState.assignee;
10752
+ }
10753
+ const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
10754
+ if (resolvedStatus && resolvedStatus !== feature.status) {
10755
+ const newStatus = resolvedStatus;
10756
+ if (!forceSync && isRegression(feature.status, newStatus)) {
10757
+ continue;
10758
+ }
10759
+ feature.status = newStatus;
10760
+ }
10761
+ }
10762
+ return result;
10763
+ }
10764
+ var syncMutex = Promise.resolve();
10765
+ async function fullSync(roadmapPath, adapter, config, options) {
10766
+ const previousSync = syncMutex;
10767
+ let releaseMutex;
10768
+ syncMutex = new Promise((resolve5) => {
10769
+ releaseMutex = resolve5;
10770
+ });
10771
+ await previousSync;
10772
+ try {
10773
+ const raw = fs20.readFileSync(roadmapPath, "utf-8");
10774
+ const parseResult = parseRoadmap(raw);
10775
+ if (!parseResult.ok) {
10776
+ return {
10777
+ ...emptySyncResult(),
10778
+ errors: [{ featureOrId: "*", error: parseResult.error }]
10779
+ };
10780
+ }
10781
+ const roadmap = parseResult.value;
10782
+ const pushResult = await syncToExternal(roadmap, adapter, config);
10783
+ const pullResult = await syncFromExternal(roadmap, adapter, config, options);
10784
+ fs20.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
10785
+ return {
10786
+ created: pushResult.created,
10787
+ updated: pushResult.updated,
10788
+ assignmentChanges: pullResult.assignmentChanges,
10789
+ errors: [...pushResult.errors, ...pullResult.errors]
10790
+ };
10791
+ } finally {
10792
+ releaseMutex();
10793
+ }
10794
+ }
10795
+
10796
+ // src/roadmap/pilot-scoring.ts
10797
+ var PRIORITY_RANK = {
10798
+ P0: 0,
10799
+ P1: 1,
10800
+ P2: 2,
10801
+ P3: 3
10802
+ };
10803
+ var POSITION_WEIGHT = 0.5;
10804
+ var DEPENDENTS_WEIGHT = 0.3;
10805
+ var AFFINITY_WEIGHT = 0.2;
10806
+ function scoreRoadmapCandidates(roadmap, options) {
10807
+ const allFeatures = roadmap.milestones.flatMap((m) => m.features);
10808
+ const allFeatureNames = new Set(allFeatures.map((f) => f.name.toLowerCase()));
10809
+ const doneFeatures = new Set(
10810
+ allFeatures.filter((f) => f.status === "done").map((f) => f.name.toLowerCase())
10811
+ );
10812
+ const dependentsCount = /* @__PURE__ */ new Map();
10813
+ for (const feature of allFeatures) {
10814
+ for (const blocker of feature.blockedBy) {
10815
+ const key = blocker.toLowerCase();
10816
+ dependentsCount.set(key, (dependentsCount.get(key) ?? 0) + 1);
10817
+ }
10818
+ }
10819
+ const maxDependents = Math.max(1, ...dependentsCount.values());
10820
+ const milestoneMap = /* @__PURE__ */ new Map();
10821
+ for (const ms of roadmap.milestones) {
10822
+ milestoneMap.set(
10823
+ ms.name,
10824
+ ms.features.map((f) => f.name.toLowerCase())
10825
+ );
10826
+ }
10827
+ const userCompletedFeatures = /* @__PURE__ */ new Set();
10828
+ if (options?.currentUser) {
10829
+ const user = options.currentUser.toLowerCase();
10830
+ for (const record of roadmap.assignmentHistory) {
10831
+ if (record.action === "completed" && record.assignee.toLowerCase() === user) {
10832
+ userCompletedFeatures.add(record.feature.toLowerCase());
10833
+ }
10834
+ }
10835
+ }
10836
+ let totalPositions = 0;
10837
+ for (const ms of roadmap.milestones) {
10838
+ totalPositions += ms.features.length;
10839
+ }
10840
+ totalPositions = Math.max(1, totalPositions);
10841
+ const candidates = [];
10842
+ let globalPosition = 0;
10843
+ for (const ms of roadmap.milestones) {
10844
+ for (let featureIdx = 0; featureIdx < ms.features.length; featureIdx++) {
10845
+ const feature = ms.features[featureIdx];
10846
+ globalPosition++;
10847
+ if (feature.status !== "planned" && feature.status !== "backlog") continue;
10848
+ const isBlocked = feature.blockedBy.some((blocker) => {
10849
+ const key = blocker.toLowerCase();
10850
+ return allFeatureNames.has(key) && !doneFeatures.has(key);
10851
+ });
10852
+ if (isBlocked) continue;
10853
+ const positionScore = 1 - (globalPosition - 1) / totalPositions;
10854
+ const deps = dependentsCount.get(feature.name.toLowerCase()) ?? 0;
10855
+ const dependentsScore = deps / maxDependents;
10856
+ let affinityScore = 0;
10857
+ if (userCompletedFeatures.size > 0) {
10858
+ const completedBlockers = feature.blockedBy.filter(
10859
+ (b) => userCompletedFeatures.has(b.toLowerCase())
10860
+ );
10861
+ if (completedBlockers.length > 0) {
10862
+ affinityScore = 1;
10863
+ } else {
10864
+ const siblings = milestoneMap.get(ms.name) ?? [];
10865
+ const completedSiblings = siblings.filter((s) => userCompletedFeatures.has(s));
10866
+ if (completedSiblings.length > 0) {
10867
+ affinityScore = 0.5;
10868
+ }
10869
+ }
10870
+ }
10871
+ const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
10872
+ const priorityTier = feature.priority ? PRIORITY_RANK[feature.priority] : null;
10873
+ candidates.push({
10874
+ feature,
10875
+ milestone: ms.name,
10876
+ positionScore,
10877
+ dependentsScore,
10878
+ affinityScore,
10879
+ weightedScore,
10880
+ priorityTier
10881
+ });
10882
+ }
10883
+ }
10884
+ candidates.sort((a, b) => {
10885
+ if (a.priorityTier !== null && b.priorityTier === null) return -1;
10886
+ if (a.priorityTier === null && b.priorityTier !== null) return 1;
10887
+ if (a.priorityTier !== null && b.priorityTier !== null) {
10888
+ if (a.priorityTier !== b.priorityTier) return a.priorityTier - b.priorityTier;
10889
+ }
10890
+ return b.weightedScore - a.weightedScore;
10891
+ });
10892
+ return candidates;
10893
+ }
10894
+ function assignFeature(roadmap, feature, assignee, date) {
10895
+ if (feature.assignee === assignee) return;
10896
+ if (feature.assignee !== null) {
10897
+ roadmap.assignmentHistory.push({
10898
+ feature: feature.name,
10899
+ assignee: feature.assignee,
10900
+ action: "unassigned",
10901
+ date
10902
+ });
10903
+ }
10904
+ feature.assignee = assignee;
10905
+ roadmap.assignmentHistory.push({
10906
+ feature: feature.name,
10907
+ assignee,
10908
+ action: "assigned",
10909
+ date
10910
+ });
10911
+ }
10912
+
10352
10913
  // src/interaction/types.ts
10353
10914
  import { z as z7 } from "zod";
10354
10915
  var InteractionTypeSchema = z7.enum(["question", "confirmation", "transition"]);
@@ -10379,17 +10940,18 @@ var EmitInteractionInputSchema = z7.object({
10379
10940
  });
10380
10941
 
10381
10942
  // src/blueprint/scanner.ts
10382
- import * as fs20 from "fs/promises";
10943
+ import * as fs21 from "fs/promises";
10383
10944
  import * as path20 from "path";
10384
10945
  var ProjectScanner = class {
10385
10946
  constructor(rootDir) {
10386
10947
  this.rootDir = rootDir;
10387
10948
  }
10949
+ rootDir;
10388
10950
  async scan() {
10389
10951
  let projectName = path20.basename(this.rootDir);
10390
10952
  try {
10391
10953
  const pkgPath = path20.join(this.rootDir, "package.json");
10392
- const pkgRaw = await fs20.readFile(pkgPath, "utf-8");
10954
+ const pkgRaw = await fs21.readFile(pkgPath, "utf-8");
10393
10955
  const pkg = JSON.parse(pkgRaw);
10394
10956
  if (pkg.name) projectName = pkg.name;
10395
10957
  } catch {
@@ -10430,7 +10992,7 @@ var ProjectScanner = class {
10430
10992
  };
10431
10993
 
10432
10994
  // src/blueprint/generator.ts
10433
- import * as fs21 from "fs/promises";
10995
+ import * as fs22 from "fs/promises";
10434
10996
  import * as path21 from "path";
10435
10997
  import * as ejs from "ejs";
10436
10998
 
@@ -10515,13 +11077,13 @@ var BlueprintGenerator = class {
10515
11077
  styles: STYLES,
10516
11078
  scripts: SCRIPTS
10517
11079
  });
10518
- await fs21.mkdir(options.outputDir, { recursive: true });
10519
- await fs21.writeFile(path21.join(options.outputDir, "index.html"), html);
11080
+ await fs22.mkdir(options.outputDir, { recursive: true });
11081
+ await fs22.writeFile(path21.join(options.outputDir, "index.html"), html);
10520
11082
  }
10521
11083
  };
10522
11084
 
10523
11085
  // src/update-checker.ts
10524
- import * as fs22 from "fs";
11086
+ import * as fs23 from "fs";
10525
11087
  import * as path22 from "path";
10526
11088
  import * as os from "os";
10527
11089
  import { spawn } from "child_process";
@@ -10540,7 +11102,7 @@ function shouldRunCheck(state, intervalMs) {
10540
11102
  }
10541
11103
  function readCheckState() {
10542
11104
  try {
10543
- const raw = fs22.readFileSync(getStatePath(), "utf-8");
11105
+ const raw = fs23.readFileSync(getStatePath(), "utf-8");
10544
11106
  const parsed = JSON.parse(raw);
10545
11107
  if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
10546
11108
  const state = parsed;
@@ -11052,7 +11614,7 @@ function getModelPrice(model, dataset) {
11052
11614
  }
11053
11615
 
11054
11616
  // src/pricing/cache.ts
11055
- import * as fs23 from "fs/promises";
11617
+ import * as fs24 from "fs/promises";
11056
11618
  import * as path23 from "path";
11057
11619
 
11058
11620
  // src/pricing/fallback.json
@@ -11113,7 +11675,7 @@ function getStalenessMarkerPath(projectRoot) {
11113
11675
  }
11114
11676
  async function readDiskCache(projectRoot) {
11115
11677
  try {
11116
- const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
11678
+ const raw = await fs24.readFile(getCachePath(projectRoot), "utf-8");
11117
11679
  return JSON.parse(raw);
11118
11680
  } catch {
11119
11681
  return null;
@@ -11121,8 +11683,8 @@ async function readDiskCache(projectRoot) {
11121
11683
  }
11122
11684
  async function writeDiskCache(projectRoot, data) {
11123
11685
  const cachePath = getCachePath(projectRoot);
11124
- await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
11125
- await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
11686
+ await fs24.mkdir(path23.dirname(cachePath), { recursive: true });
11687
+ await fs24.writeFile(cachePath, JSON.stringify(data, null, 2));
11126
11688
  }
11127
11689
  async function fetchFromNetwork() {
11128
11690
  try {
@@ -11149,7 +11711,7 @@ function loadFallbackDataset() {
11149
11711
  async function checkAndWarnStaleness(projectRoot) {
11150
11712
  const markerPath = getStalenessMarkerPath(projectRoot);
11151
11713
  try {
11152
- const raw = await fs23.readFile(markerPath, "utf-8");
11714
+ const raw = await fs24.readFile(markerPath, "utf-8");
11153
11715
  const marker = JSON.parse(raw);
11154
11716
  const firstUse = new Date(marker.firstFallbackUse).getTime();
11155
11717
  const now = Date.now();
@@ -11161,8 +11723,8 @@ async function checkAndWarnStaleness(projectRoot) {
11161
11723
  }
11162
11724
  } catch {
11163
11725
  try {
11164
- await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
11165
- await fs23.writeFile(
11726
+ await fs24.mkdir(path23.dirname(markerPath), { recursive: true });
11727
+ await fs24.writeFile(
11166
11728
  markerPath,
11167
11729
  JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
11168
11730
  );
@@ -11172,7 +11734,7 @@ async function checkAndWarnStaleness(projectRoot) {
11172
11734
  }
11173
11735
  async function clearStalenessMarker(projectRoot) {
11174
11736
  try {
11175
- await fs23.unlink(getStalenessMarkerPath(projectRoot));
11737
+ await fs24.unlink(getStalenessMarkerPath(projectRoot));
11176
11738
  } catch {
11177
11739
  }
11178
11740
  }
@@ -11342,7 +11904,7 @@ function aggregateByDay(records) {
11342
11904
  }
11343
11905
 
11344
11906
  // src/usage/jsonl-reader.ts
11345
- import * as fs24 from "fs";
11907
+ import * as fs25 from "fs";
11346
11908
  import * as path24 from "path";
11347
11909
  function parseLine(line, lineNumber) {
11348
11910
  let entry;
@@ -11385,7 +11947,7 @@ function readCostRecords(projectRoot) {
11385
11947
  const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
11386
11948
  let raw;
11387
11949
  try {
11388
- raw = fs24.readFileSync(costsFile, "utf-8");
11950
+ raw = fs25.readFileSync(costsFile, "utf-8");
11389
11951
  } catch {
11390
11952
  return [];
11391
11953
  }
@@ -11403,7 +11965,7 @@ function readCostRecords(projectRoot) {
11403
11965
  }
11404
11966
 
11405
11967
  // src/usage/cc-parser.ts
11406
- import * as fs25 from "fs";
11968
+ import * as fs26 from "fs";
11407
11969
  import * as path25 from "path";
11408
11970
  import * as os2 from "os";
11409
11971
  function extractUsage(entry) {
@@ -11451,7 +12013,7 @@ function parseCCLine(line, filePath, lineNumber) {
11451
12013
  function readCCFile(filePath) {
11452
12014
  let raw;
11453
12015
  try {
11454
- raw = fs25.readFileSync(filePath, "utf-8");
12016
+ raw = fs26.readFileSync(filePath, "utf-8");
11455
12017
  } catch {
11456
12018
  return [];
11457
12019
  }
@@ -11476,7 +12038,7 @@ function parseCCRecords() {
11476
12038
  const projectsDir = path25.join(homeDir, ".claude", "projects");
11477
12039
  let projectDirs;
11478
12040
  try {
11479
- projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
12041
+ projectDirs = fs26.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
11480
12042
  } catch {
11481
12043
  return [];
11482
12044
  }
@@ -11484,7 +12046,7 @@ function parseCCRecords() {
11484
12046
  for (const dir of projectDirs) {
11485
12047
  let files;
11486
12048
  try {
11487
- files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
12049
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
11488
12050
  } catch {
11489
12051
  continue;
11490
12052
  }
@@ -11542,6 +12104,7 @@ export {
11542
12104
  ForbiddenImportCollector,
11543
12105
  GateConfigSchema,
11544
12106
  GateResultSchema,
12107
+ GitHubIssuesSyncAdapter,
11545
12108
  HandoffSchema,
11546
12109
  HarnessStateSchema,
11547
12110
  InteractionTypeSchema,
@@ -11563,6 +12126,7 @@ export {
11563
12126
  RuleRegistry,
11564
12127
  SECURITY_DESCRIPTOR,
11565
12128
  STALENESS_WARNING_DAYS,
12129
+ STATUS_RANK,
11566
12130
  SecurityConfigSchema,
11567
12131
  SecurityScanner,
11568
12132
  SharableBoundaryConfigSchema,
@@ -11596,6 +12160,7 @@ export {
11596
12160
  archiveLearnings,
11597
12161
  archiveSession,
11598
12162
  archiveStream,
12163
+ assignFeature,
11599
12164
  buildDependencyGraph,
11600
12165
  buildExclusionSet,
11601
12166
  buildSnapshot,
@@ -11660,6 +12225,7 @@ export {
11660
12225
  formatGitHubSummary,
11661
12226
  formatOutline,
11662
12227
  formatTerminalOutput,
12228
+ fullSync,
11663
12229
  generateAgentsMap,
11664
12230
  generateSuggestions,
11665
12231
  getActionEmitter,
@@ -11677,6 +12243,7 @@ export {
11677
12243
  injectionRules,
11678
12244
  insecureDefaultsRules,
11679
12245
  isDuplicateFinding,
12246
+ isRegression,
11680
12247
  isSmallSuggestion,
11681
12248
  isUpdateCheckEnabled,
11682
12249
  listActiveSessions,
@@ -11730,6 +12297,7 @@ export {
11730
12297
  resetParserCache,
11731
12298
  resolveFileToLayer,
11732
12299
  resolveModelTier,
12300
+ resolveReverseStatus,
11733
12301
  resolveRuleSeverity,
11734
12302
  resolveSessionDir,
11735
12303
  resolveStreamPath,
@@ -11750,6 +12318,7 @@ export {
11750
12318
  saveStreamIndex,
11751
12319
  scanForInjection,
11752
12320
  scopeContext,
12321
+ scoreRoadmapCandidates,
11753
12322
  searchSymbols,
11754
12323
  secretRules,
11755
12324
  serializeRoadmap,
@@ -11758,7 +12327,9 @@ export {
11758
12327
  shouldRunCheck,
11759
12328
  spawnBackgroundCheck,
11760
12329
  syncConstraintNodes,
12330
+ syncFromExternal,
11761
12331
  syncRoadmap,
12332
+ syncToExternal,
11762
12333
  tagUncitedFindings,
11763
12334
  touchStream,
11764
12335
  trackAction,