@harness-engineering/core 0.16.0 → 0.17.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/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,438 @@ 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 GitHubIssuesSyncAdapter = class {
10469
+ token;
10470
+ config;
10471
+ fetchFn;
10472
+ apiBase;
10473
+ owner;
10474
+ repo;
10475
+ constructor(options) {
10476
+ this.token = options.token;
10477
+ this.config = options.config;
10478
+ this.fetchFn = options.fetchFn ?? globalThis.fetch;
10479
+ this.apiBase = options.apiBase ?? "https://api.github.com";
10480
+ const repoParts = (options.config.repo ?? "").split("/");
10481
+ if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
10482
+ throw new Error(`Invalid repo format: "${options.config.repo}". Expected "owner/repo".`);
10483
+ }
10484
+ this.owner = repoParts[0];
10485
+ this.repo = repoParts[1];
10486
+ }
10487
+ headers() {
10488
+ return {
10489
+ Authorization: `Bearer ${this.token}`,
10490
+ Accept: "application/vnd.github+json",
10491
+ "Content-Type": "application/json",
10492
+ "X-GitHub-Api-Version": "2022-11-28"
10493
+ };
10494
+ }
10495
+ async createTicket(feature, milestone) {
10496
+ try {
10497
+ const labels = labelsForStatus(feature.status, this.config);
10498
+ const body = [
10499
+ feature.summary,
10500
+ "",
10501
+ `**Milestone:** ${milestone}`,
10502
+ feature.spec ? `**Spec:** ${feature.spec}` : ""
10503
+ ].filter(Boolean).join("\n");
10504
+ const response = await this.fetchFn(
10505
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues`,
10506
+ {
10507
+ method: "POST",
10508
+ headers: this.headers(),
10509
+ body: JSON.stringify({
10510
+ title: feature.name,
10511
+ body,
10512
+ labels
10513
+ })
10514
+ }
10515
+ );
10516
+ if (!response.ok) {
10517
+ const text = await response.text();
10518
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10519
+ }
10520
+ const data = await response.json();
10521
+ const externalId = buildExternalId(this.owner, this.repo, data.number);
10522
+ return Ok4({ externalId, url: data.html_url });
10523
+ } catch (error) {
10524
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10525
+ }
10526
+ }
10527
+ async updateTicket(externalId, changes) {
10528
+ try {
10529
+ const parsed = parseExternalId(externalId);
10530
+ if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
10531
+ const patch = {};
10532
+ if (changes.name !== void 0) patch.title = changes.name;
10533
+ if (changes.summary !== void 0) {
10534
+ const body = [changes.summary, "", changes.spec ? `**Spec:** ${changes.spec}` : ""].filter(Boolean).join("\n");
10535
+ patch.body = body;
10536
+ }
10537
+ if (changes.status !== void 0) {
10538
+ const externalStatus = this.config.statusMap[changes.status];
10539
+ patch.state = externalStatus;
10540
+ patch.labels = labelsForStatus(changes.status, this.config);
10541
+ }
10542
+ const response = await this.fetchFn(
10543
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
10544
+ {
10545
+ method: "PATCH",
10546
+ headers: this.headers(),
10547
+ body: JSON.stringify(patch)
10548
+ }
10549
+ );
10550
+ if (!response.ok) {
10551
+ const text = await response.text();
10552
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10553
+ }
10554
+ const data = await response.json();
10555
+ return Ok4({ externalId, url: data.html_url });
10556
+ } catch (error) {
10557
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10558
+ }
10559
+ }
10560
+ async fetchTicketState(externalId) {
10561
+ try {
10562
+ const parsed = parseExternalId(externalId);
10563
+ if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
10564
+ const response = await this.fetchFn(
10565
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
10566
+ {
10567
+ method: "GET",
10568
+ headers: this.headers()
10569
+ }
10570
+ );
10571
+ if (!response.ok) {
10572
+ const text = await response.text();
10573
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10574
+ }
10575
+ const data = await response.json();
10576
+ return Ok4({
10577
+ externalId,
10578
+ status: data.state,
10579
+ labels: data.labels.map((l) => l.name),
10580
+ assignee: data.assignee ? `@${data.assignee.login}` : null
10581
+ });
10582
+ } catch (error) {
10583
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10584
+ }
10585
+ }
10586
+ async fetchAllTickets() {
10587
+ try {
10588
+ const filterLabels = this.config.labels ?? [];
10589
+ const labelsParam = filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
10590
+ const tickets = [];
10591
+ let page = 1;
10592
+ const perPage = 100;
10593
+ while (true) {
10594
+ const response = await this.fetchFn(
10595
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
10596
+ {
10597
+ method: "GET",
10598
+ headers: this.headers()
10599
+ }
10600
+ );
10601
+ if (!response.ok) {
10602
+ const text = await response.text();
10603
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10604
+ }
10605
+ const data = await response.json();
10606
+ const issues = data.filter((d) => !d.pull_request);
10607
+ for (const issue of issues) {
10608
+ tickets.push({
10609
+ externalId: buildExternalId(this.owner, this.repo, issue.number),
10610
+ status: issue.state,
10611
+ labels: issue.labels.map((l) => l.name),
10612
+ assignee: issue.assignee ? `@${issue.assignee.login}` : null
10613
+ });
10614
+ }
10615
+ if (data.length < perPage) break;
10616
+ page++;
10617
+ }
10618
+ return Ok4(tickets);
10619
+ } catch (error) {
10620
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10621
+ }
10622
+ }
10623
+ async assignTicket(externalId, assignee) {
10624
+ try {
10625
+ const parsed = parseExternalId(externalId);
10626
+ if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
10627
+ const login = assignee.startsWith("@") ? assignee.slice(1) : assignee;
10628
+ const response = await this.fetchFn(
10629
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/assignees`,
10630
+ {
10631
+ method: "POST",
10632
+ headers: this.headers(),
10633
+ body: JSON.stringify({ assignees: [login] })
10634
+ }
10635
+ );
10636
+ if (!response.ok) {
10637
+ const text = await response.text();
10638
+ return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
10639
+ }
10640
+ return Ok4(void 0);
10641
+ } catch (error) {
10642
+ return Err3(error instanceof Error ? error : new Error(String(error)));
10643
+ }
10644
+ }
10645
+ };
10646
+
10647
+ // src/roadmap/sync-engine.ts
10648
+ import * as fs20 from "fs";
10649
+ function emptySyncResult() {
10650
+ return { created: [], updated: [], assignmentChanges: [], errors: [] };
10651
+ }
10652
+ async function syncToExternal(roadmap, adapter, _config) {
10653
+ const result = emptySyncResult();
10654
+ for (const milestone of roadmap.milestones) {
10655
+ for (const feature of milestone.features) {
10656
+ if (!feature.externalId) {
10657
+ const createResult = await adapter.createTicket(feature, milestone.name);
10658
+ if (createResult.ok) {
10659
+ feature.externalId = createResult.value.externalId;
10660
+ result.created.push(createResult.value);
10661
+ } else {
10662
+ result.errors.push({ featureOrId: feature.name, error: createResult.error });
10663
+ }
10664
+ } else {
10665
+ const updateResult = await adapter.updateTicket(feature.externalId, feature);
10666
+ if (updateResult.ok) {
10667
+ result.updated.push(feature.externalId);
10668
+ } else {
10669
+ result.errors.push({ featureOrId: feature.externalId, error: updateResult.error });
10670
+ }
10671
+ }
10672
+ }
10673
+ }
10674
+ return result;
10675
+ }
10676
+ async function syncFromExternal(roadmap, adapter, config, options) {
10677
+ const result = emptySyncResult();
10678
+ const forceSync = options?.forceSync ?? false;
10679
+ const featureByExternalId = /* @__PURE__ */ new Map();
10680
+ for (const milestone of roadmap.milestones) {
10681
+ for (const feature of milestone.features) {
10682
+ if (feature.externalId) {
10683
+ featureByExternalId.set(feature.externalId, feature);
10684
+ }
10685
+ }
10686
+ }
10687
+ if (featureByExternalId.size === 0) return result;
10688
+ const fetchResult = await adapter.fetchAllTickets();
10689
+ if (!fetchResult.ok) {
10690
+ result.errors.push({ featureOrId: "*", error: fetchResult.error });
10691
+ return result;
10692
+ }
10693
+ for (const ticketState of fetchResult.value) {
10694
+ const feature = featureByExternalId.get(ticketState.externalId);
10695
+ if (!feature) continue;
10696
+ if (ticketState.assignee !== feature.assignee) {
10697
+ result.assignmentChanges.push({
10698
+ feature: feature.name,
10699
+ from: feature.assignee,
10700
+ to: ticketState.assignee
10701
+ });
10702
+ feature.assignee = ticketState.assignee;
10703
+ }
10704
+ const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
10705
+ if (resolvedStatus && resolvedStatus !== feature.status) {
10706
+ const newStatus = resolvedStatus;
10707
+ if (!forceSync && isRegression(feature.status, newStatus)) {
10708
+ continue;
10709
+ }
10710
+ feature.status = newStatus;
10711
+ }
10712
+ }
10713
+ return result;
10714
+ }
10715
+ var syncMutex = Promise.resolve();
10716
+ async function fullSync(roadmapPath, adapter, config, options) {
10717
+ const previousSync = syncMutex;
10718
+ let releaseMutex;
10719
+ syncMutex = new Promise((resolve5) => {
10720
+ releaseMutex = resolve5;
10721
+ });
10722
+ await previousSync;
10723
+ try {
10724
+ const raw = fs20.readFileSync(roadmapPath, "utf-8");
10725
+ const parseResult = parseRoadmap(raw);
10726
+ if (!parseResult.ok) {
10727
+ return {
10728
+ ...emptySyncResult(),
10729
+ errors: [{ featureOrId: "*", error: parseResult.error }]
10730
+ };
10731
+ }
10732
+ const roadmap = parseResult.value;
10733
+ const pushResult = await syncToExternal(roadmap, adapter, config);
10734
+ const pullResult = await syncFromExternal(roadmap, adapter, config, options);
10735
+ fs20.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
10736
+ return {
10737
+ created: pushResult.created,
10738
+ updated: pushResult.updated,
10739
+ assignmentChanges: pullResult.assignmentChanges,
10740
+ errors: [...pushResult.errors, ...pullResult.errors]
10741
+ };
10742
+ } finally {
10743
+ releaseMutex();
10744
+ }
10745
+ }
10746
+
10747
+ // src/roadmap/pilot-scoring.ts
10748
+ var PRIORITY_RANK = {
10749
+ P0: 0,
10750
+ P1: 1,
10751
+ P2: 2,
10752
+ P3: 3
10753
+ };
10754
+ var POSITION_WEIGHT = 0.5;
10755
+ var DEPENDENTS_WEIGHT = 0.3;
10756
+ var AFFINITY_WEIGHT = 0.2;
10757
+ function scoreRoadmapCandidates(roadmap, options) {
10758
+ const allFeatures = roadmap.milestones.flatMap((m) => m.features);
10759
+ const allFeatureNames = new Set(allFeatures.map((f) => f.name.toLowerCase()));
10760
+ const doneFeatures = new Set(
10761
+ allFeatures.filter((f) => f.status === "done").map((f) => f.name.toLowerCase())
10762
+ );
10763
+ const dependentsCount = /* @__PURE__ */ new Map();
10764
+ for (const feature of allFeatures) {
10765
+ for (const blocker of feature.blockedBy) {
10766
+ const key = blocker.toLowerCase();
10767
+ dependentsCount.set(key, (dependentsCount.get(key) ?? 0) + 1);
10768
+ }
10769
+ }
10770
+ const maxDependents = Math.max(1, ...dependentsCount.values());
10771
+ const milestoneMap = /* @__PURE__ */ new Map();
10772
+ for (const ms of roadmap.milestones) {
10773
+ milestoneMap.set(
10774
+ ms.name,
10775
+ ms.features.map((f) => f.name.toLowerCase())
10776
+ );
10777
+ }
10778
+ const userCompletedFeatures = /* @__PURE__ */ new Set();
10779
+ if (options?.currentUser) {
10780
+ const user = options.currentUser.toLowerCase();
10781
+ for (const record of roadmap.assignmentHistory) {
10782
+ if (record.action === "completed" && record.assignee.toLowerCase() === user) {
10783
+ userCompletedFeatures.add(record.feature.toLowerCase());
10784
+ }
10785
+ }
10786
+ }
10787
+ let totalPositions = 0;
10788
+ for (const ms of roadmap.milestones) {
10789
+ totalPositions += ms.features.length;
10790
+ }
10791
+ totalPositions = Math.max(1, totalPositions);
10792
+ const candidates = [];
10793
+ let globalPosition = 0;
10794
+ for (const ms of roadmap.milestones) {
10795
+ for (let featureIdx = 0; featureIdx < ms.features.length; featureIdx++) {
10796
+ const feature = ms.features[featureIdx];
10797
+ globalPosition++;
10798
+ if (feature.status !== "planned" && feature.status !== "backlog") continue;
10799
+ const isBlocked = feature.blockedBy.some((blocker) => {
10800
+ const key = blocker.toLowerCase();
10801
+ return allFeatureNames.has(key) && !doneFeatures.has(key);
10802
+ });
10803
+ if (isBlocked) continue;
10804
+ const positionScore = 1 - (globalPosition - 1) / totalPositions;
10805
+ const deps = dependentsCount.get(feature.name.toLowerCase()) ?? 0;
10806
+ const dependentsScore = deps / maxDependents;
10807
+ let affinityScore = 0;
10808
+ if (userCompletedFeatures.size > 0) {
10809
+ const completedBlockers = feature.blockedBy.filter(
10810
+ (b) => userCompletedFeatures.has(b.toLowerCase())
10811
+ );
10812
+ if (completedBlockers.length > 0) {
10813
+ affinityScore = 1;
10814
+ } else {
10815
+ const siblings = milestoneMap.get(ms.name) ?? [];
10816
+ const completedSiblings = siblings.filter((s) => userCompletedFeatures.has(s));
10817
+ if (completedSiblings.length > 0) {
10818
+ affinityScore = 0.5;
10819
+ }
10820
+ }
10821
+ }
10822
+ const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
10823
+ const priorityTier = feature.priority ? PRIORITY_RANK[feature.priority] : null;
10824
+ candidates.push({
10825
+ feature,
10826
+ milestone: ms.name,
10827
+ positionScore,
10828
+ dependentsScore,
10829
+ affinityScore,
10830
+ weightedScore,
10831
+ priorityTier
10832
+ });
10833
+ }
10834
+ }
10835
+ candidates.sort((a, b) => {
10836
+ if (a.priorityTier !== null && b.priorityTier === null) return -1;
10837
+ if (a.priorityTier === null && b.priorityTier !== null) return 1;
10838
+ if (a.priorityTier !== null && b.priorityTier !== null) {
10839
+ if (a.priorityTier !== b.priorityTier) return a.priorityTier - b.priorityTier;
10840
+ }
10841
+ return b.weightedScore - a.weightedScore;
10842
+ });
10843
+ return candidates;
10844
+ }
10845
+ function assignFeature(roadmap, feature, assignee, date) {
10846
+ if (feature.assignee === assignee) return;
10847
+ if (feature.assignee !== null) {
10848
+ roadmap.assignmentHistory.push({
10849
+ feature: feature.name,
10850
+ assignee: feature.assignee,
10851
+ action: "unassigned",
10852
+ date
10853
+ });
10854
+ }
10855
+ feature.assignee = assignee;
10856
+ roadmap.assignmentHistory.push({
10857
+ feature: feature.name,
10858
+ assignee,
10859
+ action: "assigned",
10860
+ date
10861
+ });
10862
+ }
10863
+
10352
10864
  // src/interaction/types.ts
10353
10865
  import { z as z7 } from "zod";
10354
10866
  var InteractionTypeSchema = z7.enum(["question", "confirmation", "transition"]);
@@ -10379,17 +10891,18 @@ var EmitInteractionInputSchema = z7.object({
10379
10891
  });
10380
10892
 
10381
10893
  // src/blueprint/scanner.ts
10382
- import * as fs20 from "fs/promises";
10894
+ import * as fs21 from "fs/promises";
10383
10895
  import * as path20 from "path";
10384
10896
  var ProjectScanner = class {
10385
10897
  constructor(rootDir) {
10386
10898
  this.rootDir = rootDir;
10387
10899
  }
10900
+ rootDir;
10388
10901
  async scan() {
10389
10902
  let projectName = path20.basename(this.rootDir);
10390
10903
  try {
10391
10904
  const pkgPath = path20.join(this.rootDir, "package.json");
10392
- const pkgRaw = await fs20.readFile(pkgPath, "utf-8");
10905
+ const pkgRaw = await fs21.readFile(pkgPath, "utf-8");
10393
10906
  const pkg = JSON.parse(pkgRaw);
10394
10907
  if (pkg.name) projectName = pkg.name;
10395
10908
  } catch {
@@ -10430,7 +10943,7 @@ var ProjectScanner = class {
10430
10943
  };
10431
10944
 
10432
10945
  // src/blueprint/generator.ts
10433
- import * as fs21 from "fs/promises";
10946
+ import * as fs22 from "fs/promises";
10434
10947
  import * as path21 from "path";
10435
10948
  import * as ejs from "ejs";
10436
10949
 
@@ -10515,13 +11028,13 @@ var BlueprintGenerator = class {
10515
11028
  styles: STYLES,
10516
11029
  scripts: SCRIPTS
10517
11030
  });
10518
- await fs21.mkdir(options.outputDir, { recursive: true });
10519
- await fs21.writeFile(path21.join(options.outputDir, "index.html"), html);
11031
+ await fs22.mkdir(options.outputDir, { recursive: true });
11032
+ await fs22.writeFile(path21.join(options.outputDir, "index.html"), html);
10520
11033
  }
10521
11034
  };
10522
11035
 
10523
11036
  // src/update-checker.ts
10524
- import * as fs22 from "fs";
11037
+ import * as fs23 from "fs";
10525
11038
  import * as path22 from "path";
10526
11039
  import * as os from "os";
10527
11040
  import { spawn } from "child_process";
@@ -10540,7 +11053,7 @@ function shouldRunCheck(state, intervalMs) {
10540
11053
  }
10541
11054
  function readCheckState() {
10542
11055
  try {
10543
- const raw = fs22.readFileSync(getStatePath(), "utf-8");
11056
+ const raw = fs23.readFileSync(getStatePath(), "utf-8");
10544
11057
  const parsed = JSON.parse(raw);
10545
11058
  if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
10546
11059
  const state = parsed;
@@ -11052,7 +11565,7 @@ function getModelPrice(model, dataset) {
11052
11565
  }
11053
11566
 
11054
11567
  // src/pricing/cache.ts
11055
- import * as fs23 from "fs/promises";
11568
+ import * as fs24 from "fs/promises";
11056
11569
  import * as path23 from "path";
11057
11570
 
11058
11571
  // src/pricing/fallback.json
@@ -11113,7 +11626,7 @@ function getStalenessMarkerPath(projectRoot) {
11113
11626
  }
11114
11627
  async function readDiskCache(projectRoot) {
11115
11628
  try {
11116
- const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
11629
+ const raw = await fs24.readFile(getCachePath(projectRoot), "utf-8");
11117
11630
  return JSON.parse(raw);
11118
11631
  } catch {
11119
11632
  return null;
@@ -11121,8 +11634,8 @@ async function readDiskCache(projectRoot) {
11121
11634
  }
11122
11635
  async function writeDiskCache(projectRoot, data) {
11123
11636
  const cachePath = getCachePath(projectRoot);
11124
- await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
11125
- await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
11637
+ await fs24.mkdir(path23.dirname(cachePath), { recursive: true });
11638
+ await fs24.writeFile(cachePath, JSON.stringify(data, null, 2));
11126
11639
  }
11127
11640
  async function fetchFromNetwork() {
11128
11641
  try {
@@ -11149,7 +11662,7 @@ function loadFallbackDataset() {
11149
11662
  async function checkAndWarnStaleness(projectRoot) {
11150
11663
  const markerPath = getStalenessMarkerPath(projectRoot);
11151
11664
  try {
11152
- const raw = await fs23.readFile(markerPath, "utf-8");
11665
+ const raw = await fs24.readFile(markerPath, "utf-8");
11153
11666
  const marker = JSON.parse(raw);
11154
11667
  const firstUse = new Date(marker.firstFallbackUse).getTime();
11155
11668
  const now = Date.now();
@@ -11161,8 +11674,8 @@ async function checkAndWarnStaleness(projectRoot) {
11161
11674
  }
11162
11675
  } catch {
11163
11676
  try {
11164
- await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
11165
- await fs23.writeFile(
11677
+ await fs24.mkdir(path23.dirname(markerPath), { recursive: true });
11678
+ await fs24.writeFile(
11166
11679
  markerPath,
11167
11680
  JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
11168
11681
  );
@@ -11172,7 +11685,7 @@ async function checkAndWarnStaleness(projectRoot) {
11172
11685
  }
11173
11686
  async function clearStalenessMarker(projectRoot) {
11174
11687
  try {
11175
- await fs23.unlink(getStalenessMarkerPath(projectRoot));
11688
+ await fs24.unlink(getStalenessMarkerPath(projectRoot));
11176
11689
  } catch {
11177
11690
  }
11178
11691
  }
@@ -11342,7 +11855,7 @@ function aggregateByDay(records) {
11342
11855
  }
11343
11856
 
11344
11857
  // src/usage/jsonl-reader.ts
11345
- import * as fs24 from "fs";
11858
+ import * as fs25 from "fs";
11346
11859
  import * as path24 from "path";
11347
11860
  function parseLine(line, lineNumber) {
11348
11861
  let entry;
@@ -11385,7 +11898,7 @@ function readCostRecords(projectRoot) {
11385
11898
  const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
11386
11899
  let raw;
11387
11900
  try {
11388
- raw = fs24.readFileSync(costsFile, "utf-8");
11901
+ raw = fs25.readFileSync(costsFile, "utf-8");
11389
11902
  } catch {
11390
11903
  return [];
11391
11904
  }
@@ -11403,7 +11916,7 @@ function readCostRecords(projectRoot) {
11403
11916
  }
11404
11917
 
11405
11918
  // src/usage/cc-parser.ts
11406
- import * as fs25 from "fs";
11919
+ import * as fs26 from "fs";
11407
11920
  import * as path25 from "path";
11408
11921
  import * as os2 from "os";
11409
11922
  function extractUsage(entry) {
@@ -11451,7 +11964,7 @@ function parseCCLine(line, filePath, lineNumber) {
11451
11964
  function readCCFile(filePath) {
11452
11965
  let raw;
11453
11966
  try {
11454
- raw = fs25.readFileSync(filePath, "utf-8");
11967
+ raw = fs26.readFileSync(filePath, "utf-8");
11455
11968
  } catch {
11456
11969
  return [];
11457
11970
  }
@@ -11476,7 +11989,7 @@ function parseCCRecords() {
11476
11989
  const projectsDir = path25.join(homeDir, ".claude", "projects");
11477
11990
  let projectDirs;
11478
11991
  try {
11479
- projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
11992
+ projectDirs = fs26.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
11480
11993
  } catch {
11481
11994
  return [];
11482
11995
  }
@@ -11484,7 +11997,7 @@ function parseCCRecords() {
11484
11997
  for (const dir of projectDirs) {
11485
11998
  let files;
11486
11999
  try {
11487
- files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
12000
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
11488
12001
  } catch {
11489
12002
  continue;
11490
12003
  }
@@ -11542,6 +12055,7 @@ export {
11542
12055
  ForbiddenImportCollector,
11543
12056
  GateConfigSchema,
11544
12057
  GateResultSchema,
12058
+ GitHubIssuesSyncAdapter,
11545
12059
  HandoffSchema,
11546
12060
  HarnessStateSchema,
11547
12061
  InteractionTypeSchema,
@@ -11563,6 +12077,7 @@ export {
11563
12077
  RuleRegistry,
11564
12078
  SECURITY_DESCRIPTOR,
11565
12079
  STALENESS_WARNING_DAYS,
12080
+ STATUS_RANK,
11566
12081
  SecurityConfigSchema,
11567
12082
  SecurityScanner,
11568
12083
  SharableBoundaryConfigSchema,
@@ -11596,6 +12111,7 @@ export {
11596
12111
  archiveLearnings,
11597
12112
  archiveSession,
11598
12113
  archiveStream,
12114
+ assignFeature,
11599
12115
  buildDependencyGraph,
11600
12116
  buildExclusionSet,
11601
12117
  buildSnapshot,
@@ -11660,6 +12176,7 @@ export {
11660
12176
  formatGitHubSummary,
11661
12177
  formatOutline,
11662
12178
  formatTerminalOutput,
12179
+ fullSync,
11663
12180
  generateAgentsMap,
11664
12181
  generateSuggestions,
11665
12182
  getActionEmitter,
@@ -11677,6 +12194,7 @@ export {
11677
12194
  injectionRules,
11678
12195
  insecureDefaultsRules,
11679
12196
  isDuplicateFinding,
12197
+ isRegression,
11680
12198
  isSmallSuggestion,
11681
12199
  isUpdateCheckEnabled,
11682
12200
  listActiveSessions,
@@ -11730,6 +12248,7 @@ export {
11730
12248
  resetParserCache,
11731
12249
  resolveFileToLayer,
11732
12250
  resolveModelTier,
12251
+ resolveReverseStatus,
11733
12252
  resolveRuleSeverity,
11734
12253
  resolveSessionDir,
11735
12254
  resolveStreamPath,
@@ -11750,6 +12269,7 @@ export {
11750
12269
  saveStreamIndex,
11751
12270
  scanForInjection,
11752
12271
  scopeContext,
12272
+ scoreRoadmapCandidates,
11753
12273
  searchSymbols,
11754
12274
  secretRules,
11755
12275
  serializeRoadmap,
@@ -11758,7 +12278,9 @@ export {
11758
12278
  shouldRunCheck,
11759
12279
  spawnBackgroundCheck,
11760
12280
  syncConstraintNodes,
12281
+ syncFromExternal,
11761
12282
  syncRoadmap,
12283
+ syncToExternal,
11762
12284
  tagUncitedFindings,
11763
12285
  touchStream,
11764
12286
  trackAction,