@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.js CHANGED
@@ -75,6 +75,7 @@ __export(index_exports, {
75
75
  ForbiddenImportCollector: () => ForbiddenImportCollector,
76
76
  GateConfigSchema: () => GateConfigSchema,
77
77
  GateResultSchema: () => GateResultSchema,
78
+ GitHubIssuesSyncAdapter: () => GitHubIssuesSyncAdapter,
78
79
  HandoffSchema: () => HandoffSchema,
79
80
  HarnessStateSchema: () => HarnessStateSchema,
80
81
  InteractionTypeSchema: () => InteractionTypeSchema,
@@ -96,6 +97,7 @@ __export(index_exports, {
96
97
  RuleRegistry: () => RuleRegistry,
97
98
  SECURITY_DESCRIPTOR: () => SECURITY_DESCRIPTOR,
98
99
  STALENESS_WARNING_DAYS: () => STALENESS_WARNING_DAYS,
100
+ STATUS_RANK: () => STATUS_RANK,
99
101
  SecurityConfigSchema: () => SecurityConfigSchema,
100
102
  SecurityScanner: () => SecurityScanner,
101
103
  SharableBoundaryConfigSchema: () => SharableBoundaryConfigSchema,
@@ -129,6 +131,7 @@ __export(index_exports, {
129
131
  archiveLearnings: () => archiveLearnings,
130
132
  archiveSession: () => archiveSession,
131
133
  archiveStream: () => archiveStream,
134
+ assignFeature: () => assignFeature,
132
135
  buildDependencyGraph: () => buildDependencyGraph,
133
136
  buildExclusionSet: () => buildExclusionSet,
134
137
  buildSnapshot: () => buildSnapshot,
@@ -193,6 +196,7 @@ __export(index_exports, {
193
196
  formatGitHubSummary: () => formatGitHubSummary,
194
197
  formatOutline: () => formatOutline,
195
198
  formatTerminalOutput: () => formatTerminalOutput,
199
+ fullSync: () => fullSync,
196
200
  generateAgentsMap: () => generateAgentsMap,
197
201
  generateSuggestions: () => generateSuggestions,
198
202
  getActionEmitter: () => getActionEmitter,
@@ -210,6 +214,7 @@ __export(index_exports, {
210
214
  injectionRules: () => injectionRules,
211
215
  insecureDefaultsRules: () => insecureDefaultsRules,
212
216
  isDuplicateFinding: () => isDuplicateFinding,
217
+ isRegression: () => isRegression,
213
218
  isSmallSuggestion: () => isSmallSuggestion,
214
219
  isUpdateCheckEnabled: () => isUpdateCheckEnabled,
215
220
  listActiveSessions: () => listActiveSessions,
@@ -263,6 +268,7 @@ __export(index_exports, {
263
268
  resetParserCache: () => resetParserCache,
264
269
  resolveFileToLayer: () => resolveFileToLayer,
265
270
  resolveModelTier: () => resolveModelTier,
271
+ resolveReverseStatus: () => resolveReverseStatus,
266
272
  resolveRuleSeverity: () => resolveRuleSeverity,
267
273
  resolveSessionDir: () => resolveSessionDir,
268
274
  resolveStreamPath: () => resolveStreamPath,
@@ -283,6 +289,7 @@ __export(index_exports, {
283
289
  saveStreamIndex: () => saveStreamIndex,
284
290
  scanForInjection: () => scanForInjection,
285
291
  scopeContext: () => scopeContext,
292
+ scoreRoadmapCandidates: () => scoreRoadmapCandidates,
286
293
  searchSymbols: () => searchSymbols,
287
294
  secretRules: () => secretRules,
288
295
  serializeRoadmap: () => serializeRoadmap,
@@ -291,7 +298,9 @@ __export(index_exports, {
291
298
  shouldRunCheck: () => shouldRunCheck,
292
299
  spawnBackgroundCheck: () => spawnBackgroundCheck,
293
300
  syncConstraintNodes: () => syncConstraintNodes,
301
+ syncFromExternal: () => syncFromExternal,
294
302
  syncRoadmap: () => syncRoadmap,
303
+ syncToExternal: () => syncToExternal,
295
304
  tagUncitedFindings: () => tagUncitedFindings,
296
305
  touchStream: () => touchStream,
297
306
  trackAction: () => trackAction,
@@ -12158,6 +12167,7 @@ var VALID_STATUSES = /* @__PURE__ */ new Set([
12158
12167
  "blocked"
12159
12168
  ]);
12160
12169
  var EM_DASH = "\u2014";
12170
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
12161
12171
  function parseRoadmap(markdown) {
12162
12172
  const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
12163
12173
  if (!fmMatch) {
@@ -12168,9 +12178,12 @@ function parseRoadmap(markdown) {
12168
12178
  const body = markdown.slice(fmMatch[0].length);
12169
12179
  const milestonesResult = parseMilestones(body);
12170
12180
  if (!milestonesResult.ok) return milestonesResult;
12181
+ const historyResult = parseAssignmentHistory(body);
12182
+ if (!historyResult.ok) return historyResult;
12171
12183
  return (0, import_types19.Ok)({
12172
12184
  frontmatter: fmResult.value,
12173
- milestones: milestonesResult.value
12185
+ milestones: milestonesResult.value,
12186
+ assignmentHistory: historyResult.value
12174
12187
  });
12175
12188
  }
12176
12189
  function parseFrontmatter2(raw) {
@@ -12210,12 +12223,17 @@ function parseMilestones(body) {
12210
12223
  const h2Pattern = /^## (.+)$/gm;
12211
12224
  const h2Matches = [];
12212
12225
  let match;
12226
+ let bodyEnd = body.length;
12213
12227
  while ((match = h2Pattern.exec(body)) !== null) {
12228
+ if (match[1] === "Assignment History") {
12229
+ bodyEnd = match.index;
12230
+ break;
12231
+ }
12214
12232
  h2Matches.push({ heading: match[1], startIndex: match.index, fullMatch: match[0] });
12215
12233
  }
12216
12234
  for (let i = 0; i < h2Matches.length; i++) {
12217
12235
  const h2 = h2Matches[i];
12218
- const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : body.length;
12236
+ const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : bodyEnd;
12219
12237
  const sectionBody = body.slice(h2.startIndex + h2.fullMatch.length, nextStart);
12220
12238
  const isBacklog = h2.heading === "Backlog";
12221
12239
  const milestoneName = isBacklog ? "Backlog" : h2.heading.replace(/^Milestone:\s*/, "");
@@ -12281,15 +12299,60 @@ function parseFeatureFields(name, body) {
12281
12299
  const specRaw = fieldMap.get("Spec") ?? EM_DASH;
12282
12300
  const plans = parseListField(fieldMap, "Plans", "Plan");
12283
12301
  const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
12302
+ const assigneeRaw = fieldMap.get("Assignee") ?? EM_DASH;
12303
+ const priorityRaw = fieldMap.get("Priority") ?? EM_DASH;
12304
+ const externalIdRaw = fieldMap.get("External-ID") ?? EM_DASH;
12305
+ if (priorityRaw !== EM_DASH && !VALID_PRIORITIES.has(priorityRaw)) {
12306
+ return (0, import_types19.Err)(
12307
+ new Error(
12308
+ `Feature "${name}" has invalid priority: "${priorityRaw}". Valid priorities: ${[...VALID_PRIORITIES].join(", ")}`
12309
+ )
12310
+ );
12311
+ }
12284
12312
  return (0, import_types19.Ok)({
12285
12313
  name,
12286
12314
  status: statusRaw,
12287
12315
  spec: specRaw === EM_DASH ? null : specRaw,
12288
12316
  plans,
12289
12317
  blockedBy,
12290
- summary: fieldMap.get("Summary") ?? ""
12318
+ summary: fieldMap.get("Summary") ?? "",
12319
+ assignee: assigneeRaw === EM_DASH ? null : assigneeRaw,
12320
+ priority: priorityRaw === EM_DASH ? null : priorityRaw,
12321
+ externalId: externalIdRaw === EM_DASH ? null : externalIdRaw
12291
12322
  });
12292
12323
  }
12324
+ function parseAssignmentHistory(body) {
12325
+ const historyMatch = body.match(/^## Assignment History\s*\n/m);
12326
+ if (!historyMatch || historyMatch.index === void 0) return (0, import_types19.Ok)([]);
12327
+ const historyStart = historyMatch.index + historyMatch[0].length;
12328
+ const rawHistoryBody = body.slice(historyStart);
12329
+ const nextH2 = rawHistoryBody.search(/^## /m);
12330
+ const historyBody = nextH2 === -1 ? rawHistoryBody : rawHistoryBody.slice(0, nextH2);
12331
+ const records = [];
12332
+ const lines = historyBody.split("\n");
12333
+ let pastHeader = false;
12334
+ for (const line of lines) {
12335
+ const trimmed = line.trim();
12336
+ if (!trimmed.startsWith("|")) continue;
12337
+ if (!pastHeader) {
12338
+ if (trimmed.match(/^\|[-\s|]+\|$/)) {
12339
+ pastHeader = true;
12340
+ }
12341
+ continue;
12342
+ }
12343
+ const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
12344
+ if (cells.length < 4) continue;
12345
+ const action = cells[2];
12346
+ if (!["assigned", "completed", "unassigned"].includes(action)) continue;
12347
+ records.push({
12348
+ feature: cells[0],
12349
+ assignee: cells[1],
12350
+ action,
12351
+ date: cells[3]
12352
+ });
12353
+ }
12354
+ return (0, import_types19.Ok)(records);
12355
+ }
12293
12356
 
12294
12357
  // src/roadmap/serialize.ts
12295
12358
  var EM_DASH2 = "\u2014";
@@ -12317,6 +12380,10 @@ function serializeRoadmap(roadmap) {
12317
12380
  lines.push(...serializeFeature(feature));
12318
12381
  }
12319
12382
  }
12383
+ if (roadmap.assignmentHistory && roadmap.assignmentHistory.length > 0) {
12384
+ lines.push("");
12385
+ lines.push(...serializeAssignmentHistory(roadmap.assignmentHistory));
12386
+ }
12320
12387
  lines.push("");
12321
12388
  return lines.join("\n");
12322
12389
  }
@@ -12327,7 +12394,7 @@ function serializeFeature(feature) {
12327
12394
  const spec = feature.spec ?? EM_DASH2;
12328
12395
  const plans = feature.plans.length > 0 ? feature.plans.join(", ") : EM_DASH2;
12329
12396
  const blockedBy = feature.blockedBy.length > 0 ? feature.blockedBy.join(", ") : EM_DASH2;
12330
- return [
12397
+ const lines = [
12331
12398
  `### ${feature.name}`,
12332
12399
  "",
12333
12400
  `- **Status:** ${feature.status}`,
@@ -12336,12 +12403,45 @@ function serializeFeature(feature) {
12336
12403
  `- **Blockers:** ${blockedBy}`,
12337
12404
  `- **Plan:** ${plans}`
12338
12405
  ];
12406
+ const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
12407
+ if (hasExtended) {
12408
+ lines.push(`- **Assignee:** ${feature.assignee ?? EM_DASH2}`);
12409
+ lines.push(`- **Priority:** ${feature.priority ?? EM_DASH2}`);
12410
+ lines.push(`- **External-ID:** ${feature.externalId ?? EM_DASH2}`);
12411
+ }
12412
+ return lines;
12413
+ }
12414
+ function serializeAssignmentHistory(records) {
12415
+ const lines = [
12416
+ "## Assignment History",
12417
+ "| Feature | Assignee | Action | Date |",
12418
+ "|---------|----------|--------|------|"
12419
+ ];
12420
+ for (const record of records) {
12421
+ lines.push(`| ${record.feature} | ${record.assignee} | ${record.action} | ${record.date} |`);
12422
+ }
12423
+ return lines;
12339
12424
  }
12340
12425
 
12341
12426
  // src/roadmap/sync.ts
12342
12427
  var fs19 = __toESM(require("fs"));
12343
12428
  var path19 = __toESM(require("path"));
12344
12429
  var import_types20 = require("@harness-engineering/types");
12430
+
12431
+ // src/roadmap/status-rank.ts
12432
+ var STATUS_RANK = {
12433
+ backlog: 0,
12434
+ planned: 1,
12435
+ blocked: 1,
12436
+ // lateral to planned — sync can move to/from blocked freely
12437
+ "in-progress": 2,
12438
+ done: 3
12439
+ };
12440
+ function isRegression(from, to) {
12441
+ return STATUS_RANK[to] < STATUS_RANK[from];
12442
+ }
12443
+
12444
+ // src/roadmap/sync.ts
12345
12445
  function inferStatus(feature, projectPath, allFeatures) {
12346
12446
  if (feature.blockedBy.length > 0) {
12347
12447
  const blockerNotDone = feature.blockedBy.some((blockerName) => {
@@ -12408,17 +12508,6 @@ function inferStatus(feature, projectPath, allFeatures) {
12408
12508
  if (anyStarted) return "in-progress";
12409
12509
  return null;
12410
12510
  }
12411
- var STATUS_RANK = {
12412
- backlog: 0,
12413
- planned: 1,
12414
- blocked: 1,
12415
- // lateral to planned — sync can move to/from blocked freely
12416
- "in-progress": 2,
12417
- done: 3
12418
- };
12419
- function isRegression(from, to) {
12420
- return STATUS_RANK[to] < STATUS_RANK[from];
12421
- }
12422
12511
  function syncRoadmap(options) {
12423
12512
  const { projectPath, roadmap, forceSync } = options;
12424
12513
  const allFeatures = roadmap.milestones.flatMap((m) => m.features);
@@ -12449,6 +12538,487 @@ function applySyncChanges(roadmap, changes) {
12449
12538
  roadmap.frontmatter.lastSynced = (/* @__PURE__ */ new Date()).toISOString();
12450
12539
  }
12451
12540
 
12541
+ // src/roadmap/tracker-sync.ts
12542
+ function resolveReverseStatus(externalStatus, labels, config) {
12543
+ const reverseMap = config.reverseStatusMap;
12544
+ if (!reverseMap) return null;
12545
+ if (reverseMap[externalStatus]) {
12546
+ return reverseMap[externalStatus];
12547
+ }
12548
+ const statusLabels = ["in-progress", "blocked", "planned"];
12549
+ const matchingLabels = labels.filter((l) => statusLabels.includes(l));
12550
+ if (matchingLabels.length === 1) {
12551
+ const compoundKey = `${externalStatus}:${matchingLabels[0]}`;
12552
+ if (reverseMap[compoundKey]) {
12553
+ return reverseMap[compoundKey];
12554
+ }
12555
+ }
12556
+ return null;
12557
+ }
12558
+
12559
+ // src/roadmap/adapters/github-issues.ts
12560
+ var import_types21 = require("@harness-engineering/types");
12561
+ function parseExternalId(externalId) {
12562
+ const match = externalId.match(/^github:([^/]+)\/([^#]+)#(\d+)$/);
12563
+ if (!match) return null;
12564
+ return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
12565
+ }
12566
+ function buildExternalId(owner, repo, number) {
12567
+ return `github:${owner}/${repo}#${number}`;
12568
+ }
12569
+ function labelsForStatus(status, config) {
12570
+ const base = config.labels ?? [];
12571
+ const externalStatus = config.statusMap[status];
12572
+ if (externalStatus === "open" && status !== "backlog") {
12573
+ return [...base, status];
12574
+ }
12575
+ return [...base];
12576
+ }
12577
+ var RETRY_DEFAULTS = { maxRetries: 5, baseDelayMs: 1e3 };
12578
+ function sleep(ms) {
12579
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
12580
+ }
12581
+ async function fetchWithRetry(fetchFn, input, init, opts = RETRY_DEFAULTS) {
12582
+ let lastResponse;
12583
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
12584
+ const response = await fetchFn(input, init);
12585
+ if (response.status !== 403 && response.status !== 429) return response;
12586
+ lastResponse = response;
12587
+ if (attempt === opts.maxRetries) break;
12588
+ const retryAfter = response.headers.get("Retry-After");
12589
+ let delayMs;
12590
+ if (retryAfter) {
12591
+ const seconds = parseInt(retryAfter, 10);
12592
+ delayMs = isNaN(seconds) ? opts.baseDelayMs : seconds * 1e3;
12593
+ } else {
12594
+ delayMs = opts.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500;
12595
+ }
12596
+ await sleep(delayMs);
12597
+ }
12598
+ return lastResponse;
12599
+ }
12600
+ var GitHubIssuesSyncAdapter = class {
12601
+ token;
12602
+ config;
12603
+ fetchFn;
12604
+ apiBase;
12605
+ owner;
12606
+ repo;
12607
+ retryOpts;
12608
+ constructor(options) {
12609
+ this.token = options.token;
12610
+ this.config = options.config;
12611
+ this.fetchFn = options.fetchFn ?? globalThis.fetch;
12612
+ this.apiBase = options.apiBase ?? "https://api.github.com";
12613
+ this.retryOpts = {
12614
+ maxRetries: options.maxRetries ?? RETRY_DEFAULTS.maxRetries,
12615
+ baseDelayMs: options.baseDelayMs ?? RETRY_DEFAULTS.baseDelayMs
12616
+ };
12617
+ const repoParts = (options.config.repo ?? "").split("/");
12618
+ if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
12619
+ throw new Error(`Invalid repo format: "${options.config.repo}". Expected "owner/repo".`);
12620
+ }
12621
+ this.owner = repoParts[0];
12622
+ this.repo = repoParts[1];
12623
+ }
12624
+ headers() {
12625
+ return {
12626
+ Authorization: `Bearer ${this.token}`,
12627
+ Accept: "application/vnd.github+json",
12628
+ "Content-Type": "application/json",
12629
+ "X-GitHub-Api-Version": "2022-11-28"
12630
+ };
12631
+ }
12632
+ /**
12633
+ * Close an issue if the feature status maps to 'closed'.
12634
+ * GitHub Issues API doesn't accept state on POST — requires a follow-up PATCH.
12635
+ */
12636
+ async closeIfDone(issueNumber, featureStatus) {
12637
+ const externalStatus = this.config.statusMap[featureStatus];
12638
+ if (externalStatus !== "closed") return;
12639
+ await fetchWithRetry(
12640
+ this.fetchFn,
12641
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues/${issueNumber}`,
12642
+ { method: "PATCH", headers: this.headers(), body: JSON.stringify({ state: "closed" }) },
12643
+ this.retryOpts
12644
+ );
12645
+ }
12646
+ async createTicket(feature, milestone) {
12647
+ try {
12648
+ const labels = labelsForStatus(feature.status, this.config);
12649
+ const body = [
12650
+ feature.summary,
12651
+ "",
12652
+ `**Milestone:** ${milestone}`,
12653
+ feature.spec ? `**Spec:** ${feature.spec}` : ""
12654
+ ].filter(Boolean).join("\n");
12655
+ const response = await fetchWithRetry(
12656
+ this.fetchFn,
12657
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues`,
12658
+ {
12659
+ method: "POST",
12660
+ headers: this.headers(),
12661
+ body: JSON.stringify({ title: feature.name, body, labels })
12662
+ },
12663
+ this.retryOpts
12664
+ );
12665
+ if (!response.ok) {
12666
+ const text = await response.text();
12667
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12668
+ }
12669
+ const data = await response.json();
12670
+ const externalId = buildExternalId(this.owner, this.repo, data.number);
12671
+ await this.closeIfDone(data.number, feature.status);
12672
+ return (0, import_types21.Ok)({ externalId, url: data.html_url });
12673
+ } catch (error) {
12674
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12675
+ }
12676
+ }
12677
+ async updateTicket(externalId, changes) {
12678
+ try {
12679
+ const parsed = parseExternalId(externalId);
12680
+ if (!parsed) return (0, import_types21.Err)(new Error(`Invalid externalId format: "${externalId}"`));
12681
+ const patch = {};
12682
+ if (changes.name !== void 0) patch.title = changes.name;
12683
+ if (changes.summary !== void 0) {
12684
+ const body = [changes.summary, "", changes.spec ? `**Spec:** ${changes.spec}` : ""].filter(Boolean).join("\n");
12685
+ patch.body = body;
12686
+ }
12687
+ if (changes.status !== void 0) {
12688
+ const externalStatus = this.config.statusMap[changes.status];
12689
+ patch.state = externalStatus;
12690
+ patch.labels = labelsForStatus(changes.status, this.config);
12691
+ }
12692
+ const response = await fetchWithRetry(
12693
+ this.fetchFn,
12694
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
12695
+ {
12696
+ method: "PATCH",
12697
+ headers: this.headers(),
12698
+ body: JSON.stringify(patch)
12699
+ },
12700
+ this.retryOpts
12701
+ );
12702
+ if (!response.ok) {
12703
+ const text = await response.text();
12704
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12705
+ }
12706
+ const data = await response.json();
12707
+ return (0, import_types21.Ok)({ externalId, url: data.html_url });
12708
+ } catch (error) {
12709
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12710
+ }
12711
+ }
12712
+ async fetchTicketState(externalId) {
12713
+ try {
12714
+ const parsed = parseExternalId(externalId);
12715
+ if (!parsed) return (0, import_types21.Err)(new Error(`Invalid externalId format: "${externalId}"`));
12716
+ const response = await fetchWithRetry(
12717
+ this.fetchFn,
12718
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
12719
+ {
12720
+ method: "GET",
12721
+ headers: this.headers()
12722
+ },
12723
+ this.retryOpts
12724
+ );
12725
+ if (!response.ok) {
12726
+ const text = await response.text();
12727
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12728
+ }
12729
+ const data = await response.json();
12730
+ return (0, import_types21.Ok)({
12731
+ externalId,
12732
+ status: data.state,
12733
+ labels: data.labels.map((l) => l.name),
12734
+ assignee: data.assignee ? `@${data.assignee.login}` : null
12735
+ });
12736
+ } catch (error) {
12737
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12738
+ }
12739
+ }
12740
+ async fetchAllTickets() {
12741
+ try {
12742
+ const filterLabels = this.config.labels ?? [];
12743
+ const labelsParam = filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
12744
+ const tickets = [];
12745
+ let page = 1;
12746
+ const perPage = 100;
12747
+ while (true) {
12748
+ const response = await fetchWithRetry(
12749
+ this.fetchFn,
12750
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
12751
+ {
12752
+ method: "GET",
12753
+ headers: this.headers()
12754
+ },
12755
+ this.retryOpts
12756
+ );
12757
+ if (!response.ok) {
12758
+ const text = await response.text();
12759
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12760
+ }
12761
+ const data = await response.json();
12762
+ const issues = data.filter((d) => !d.pull_request);
12763
+ for (const issue of issues) {
12764
+ tickets.push({
12765
+ externalId: buildExternalId(this.owner, this.repo, issue.number),
12766
+ status: issue.state,
12767
+ labels: issue.labels.map((l) => l.name),
12768
+ assignee: issue.assignee ? `@${issue.assignee.login}` : null
12769
+ });
12770
+ }
12771
+ if (data.length < perPage) break;
12772
+ page++;
12773
+ }
12774
+ return (0, import_types21.Ok)(tickets);
12775
+ } catch (error) {
12776
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12777
+ }
12778
+ }
12779
+ async assignTicket(externalId, assignee) {
12780
+ try {
12781
+ const parsed = parseExternalId(externalId);
12782
+ if (!parsed) return (0, import_types21.Err)(new Error(`Invalid externalId format: "${externalId}"`));
12783
+ const login = assignee.startsWith("@") ? assignee.slice(1) : assignee;
12784
+ const response = await fetchWithRetry(
12785
+ this.fetchFn,
12786
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/assignees`,
12787
+ {
12788
+ method: "POST",
12789
+ headers: this.headers(),
12790
+ body: JSON.stringify({ assignees: [login] })
12791
+ },
12792
+ this.retryOpts
12793
+ );
12794
+ if (!response.ok) {
12795
+ const text = await response.text();
12796
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12797
+ }
12798
+ return (0, import_types21.Ok)(void 0);
12799
+ } catch (error) {
12800
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12801
+ }
12802
+ }
12803
+ };
12804
+
12805
+ // src/roadmap/sync-engine.ts
12806
+ var fs20 = __toESM(require("fs"));
12807
+ function emptySyncResult() {
12808
+ return { created: [], updated: [], assignmentChanges: [], errors: [] };
12809
+ }
12810
+ async function syncToExternal(roadmap, adapter, _config) {
12811
+ const result = emptySyncResult();
12812
+ for (const milestone of roadmap.milestones) {
12813
+ for (const feature of milestone.features) {
12814
+ if (!feature.externalId) {
12815
+ const createResult = await adapter.createTicket(feature, milestone.name);
12816
+ if (createResult.ok) {
12817
+ feature.externalId = createResult.value.externalId;
12818
+ result.created.push(createResult.value);
12819
+ } else {
12820
+ result.errors.push({ featureOrId: feature.name, error: createResult.error });
12821
+ }
12822
+ } else {
12823
+ const updateResult = await adapter.updateTicket(feature.externalId, feature);
12824
+ if (updateResult.ok) {
12825
+ result.updated.push(feature.externalId);
12826
+ } else {
12827
+ result.errors.push({ featureOrId: feature.externalId, error: updateResult.error });
12828
+ }
12829
+ }
12830
+ }
12831
+ }
12832
+ return result;
12833
+ }
12834
+ async function syncFromExternal(roadmap, adapter, config, options) {
12835
+ const result = emptySyncResult();
12836
+ const forceSync = options?.forceSync ?? false;
12837
+ const featureByExternalId = /* @__PURE__ */ new Map();
12838
+ for (const milestone of roadmap.milestones) {
12839
+ for (const feature of milestone.features) {
12840
+ if (feature.externalId) {
12841
+ featureByExternalId.set(feature.externalId, feature);
12842
+ }
12843
+ }
12844
+ }
12845
+ if (featureByExternalId.size === 0) return result;
12846
+ const fetchResult = await adapter.fetchAllTickets();
12847
+ if (!fetchResult.ok) {
12848
+ result.errors.push({ featureOrId: "*", error: fetchResult.error });
12849
+ return result;
12850
+ }
12851
+ for (const ticketState of fetchResult.value) {
12852
+ const feature = featureByExternalId.get(ticketState.externalId);
12853
+ if (!feature) continue;
12854
+ if (ticketState.assignee !== feature.assignee) {
12855
+ result.assignmentChanges.push({
12856
+ feature: feature.name,
12857
+ from: feature.assignee,
12858
+ to: ticketState.assignee
12859
+ });
12860
+ feature.assignee = ticketState.assignee;
12861
+ }
12862
+ const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
12863
+ if (resolvedStatus && resolvedStatus !== feature.status) {
12864
+ const newStatus = resolvedStatus;
12865
+ if (!forceSync && isRegression(feature.status, newStatus)) {
12866
+ continue;
12867
+ }
12868
+ feature.status = newStatus;
12869
+ }
12870
+ }
12871
+ return result;
12872
+ }
12873
+ var syncMutex = Promise.resolve();
12874
+ async function fullSync(roadmapPath, adapter, config, options) {
12875
+ const previousSync = syncMutex;
12876
+ let releaseMutex;
12877
+ syncMutex = new Promise((resolve7) => {
12878
+ releaseMutex = resolve7;
12879
+ });
12880
+ await previousSync;
12881
+ try {
12882
+ const raw = fs20.readFileSync(roadmapPath, "utf-8");
12883
+ const parseResult = parseRoadmap(raw);
12884
+ if (!parseResult.ok) {
12885
+ return {
12886
+ ...emptySyncResult(),
12887
+ errors: [{ featureOrId: "*", error: parseResult.error }]
12888
+ };
12889
+ }
12890
+ const roadmap = parseResult.value;
12891
+ const pushResult = await syncToExternal(roadmap, adapter, config);
12892
+ const pullResult = await syncFromExternal(roadmap, adapter, config, options);
12893
+ fs20.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
12894
+ return {
12895
+ created: pushResult.created,
12896
+ updated: pushResult.updated,
12897
+ assignmentChanges: pullResult.assignmentChanges,
12898
+ errors: [...pushResult.errors, ...pullResult.errors]
12899
+ };
12900
+ } finally {
12901
+ releaseMutex();
12902
+ }
12903
+ }
12904
+
12905
+ // src/roadmap/pilot-scoring.ts
12906
+ var PRIORITY_RANK = {
12907
+ P0: 0,
12908
+ P1: 1,
12909
+ P2: 2,
12910
+ P3: 3
12911
+ };
12912
+ var POSITION_WEIGHT = 0.5;
12913
+ var DEPENDENTS_WEIGHT = 0.3;
12914
+ var AFFINITY_WEIGHT = 0.2;
12915
+ function scoreRoadmapCandidates(roadmap, options) {
12916
+ const allFeatures = roadmap.milestones.flatMap((m) => m.features);
12917
+ const allFeatureNames = new Set(allFeatures.map((f) => f.name.toLowerCase()));
12918
+ const doneFeatures = new Set(
12919
+ allFeatures.filter((f) => f.status === "done").map((f) => f.name.toLowerCase())
12920
+ );
12921
+ const dependentsCount = /* @__PURE__ */ new Map();
12922
+ for (const feature of allFeatures) {
12923
+ for (const blocker of feature.blockedBy) {
12924
+ const key = blocker.toLowerCase();
12925
+ dependentsCount.set(key, (dependentsCount.get(key) ?? 0) + 1);
12926
+ }
12927
+ }
12928
+ const maxDependents = Math.max(1, ...dependentsCount.values());
12929
+ const milestoneMap = /* @__PURE__ */ new Map();
12930
+ for (const ms of roadmap.milestones) {
12931
+ milestoneMap.set(
12932
+ ms.name,
12933
+ ms.features.map((f) => f.name.toLowerCase())
12934
+ );
12935
+ }
12936
+ const userCompletedFeatures = /* @__PURE__ */ new Set();
12937
+ if (options?.currentUser) {
12938
+ const user = options.currentUser.toLowerCase();
12939
+ for (const record of roadmap.assignmentHistory) {
12940
+ if (record.action === "completed" && record.assignee.toLowerCase() === user) {
12941
+ userCompletedFeatures.add(record.feature.toLowerCase());
12942
+ }
12943
+ }
12944
+ }
12945
+ let totalPositions = 0;
12946
+ for (const ms of roadmap.milestones) {
12947
+ totalPositions += ms.features.length;
12948
+ }
12949
+ totalPositions = Math.max(1, totalPositions);
12950
+ const candidates = [];
12951
+ let globalPosition = 0;
12952
+ for (const ms of roadmap.milestones) {
12953
+ for (let featureIdx = 0; featureIdx < ms.features.length; featureIdx++) {
12954
+ const feature = ms.features[featureIdx];
12955
+ globalPosition++;
12956
+ if (feature.status !== "planned" && feature.status !== "backlog") continue;
12957
+ const isBlocked = feature.blockedBy.some((blocker) => {
12958
+ const key = blocker.toLowerCase();
12959
+ return allFeatureNames.has(key) && !doneFeatures.has(key);
12960
+ });
12961
+ if (isBlocked) continue;
12962
+ const positionScore = 1 - (globalPosition - 1) / totalPositions;
12963
+ const deps = dependentsCount.get(feature.name.toLowerCase()) ?? 0;
12964
+ const dependentsScore = deps / maxDependents;
12965
+ let affinityScore = 0;
12966
+ if (userCompletedFeatures.size > 0) {
12967
+ const completedBlockers = feature.blockedBy.filter(
12968
+ (b) => userCompletedFeatures.has(b.toLowerCase())
12969
+ );
12970
+ if (completedBlockers.length > 0) {
12971
+ affinityScore = 1;
12972
+ } else {
12973
+ const siblings = milestoneMap.get(ms.name) ?? [];
12974
+ const completedSiblings = siblings.filter((s) => userCompletedFeatures.has(s));
12975
+ if (completedSiblings.length > 0) {
12976
+ affinityScore = 0.5;
12977
+ }
12978
+ }
12979
+ }
12980
+ const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
12981
+ const priorityTier = feature.priority ? PRIORITY_RANK[feature.priority] : null;
12982
+ candidates.push({
12983
+ feature,
12984
+ milestone: ms.name,
12985
+ positionScore,
12986
+ dependentsScore,
12987
+ affinityScore,
12988
+ weightedScore,
12989
+ priorityTier
12990
+ });
12991
+ }
12992
+ }
12993
+ candidates.sort((a, b) => {
12994
+ if (a.priorityTier !== null && b.priorityTier === null) return -1;
12995
+ if (a.priorityTier === null && b.priorityTier !== null) return 1;
12996
+ if (a.priorityTier !== null && b.priorityTier !== null) {
12997
+ if (a.priorityTier !== b.priorityTier) return a.priorityTier - b.priorityTier;
12998
+ }
12999
+ return b.weightedScore - a.weightedScore;
13000
+ });
13001
+ return candidates;
13002
+ }
13003
+ function assignFeature(roadmap, feature, assignee, date) {
13004
+ if (feature.assignee === assignee) return;
13005
+ if (feature.assignee !== null) {
13006
+ roadmap.assignmentHistory.push({
13007
+ feature: feature.name,
13008
+ assignee: feature.assignee,
13009
+ action: "unassigned",
13010
+ date
13011
+ });
13012
+ }
13013
+ feature.assignee = assignee;
13014
+ roadmap.assignmentHistory.push({
13015
+ feature: feature.name,
13016
+ assignee,
13017
+ action: "assigned",
13018
+ date
13019
+ });
13020
+ }
13021
+
12452
13022
  // src/interaction/types.ts
12453
13023
  var import_zod8 = require("zod");
12454
13024
  var InteractionTypeSchema = import_zod8.z.enum(["question", "confirmation", "transition"]);
@@ -12479,17 +13049,18 @@ var EmitInteractionInputSchema = import_zod8.z.object({
12479
13049
  });
12480
13050
 
12481
13051
  // src/blueprint/scanner.ts
12482
- var fs20 = __toESM(require("fs/promises"));
13052
+ var fs21 = __toESM(require("fs/promises"));
12483
13053
  var path20 = __toESM(require("path"));
12484
13054
  var ProjectScanner = class {
12485
13055
  constructor(rootDir) {
12486
13056
  this.rootDir = rootDir;
12487
13057
  }
13058
+ rootDir;
12488
13059
  async scan() {
12489
13060
  let projectName = path20.basename(this.rootDir);
12490
13061
  try {
12491
13062
  const pkgPath = path20.join(this.rootDir, "package.json");
12492
- const pkgRaw = await fs20.readFile(pkgPath, "utf-8");
13063
+ const pkgRaw = await fs21.readFile(pkgPath, "utf-8");
12493
13064
  const pkg = JSON.parse(pkgRaw);
12494
13065
  if (pkg.name) projectName = pkg.name;
12495
13066
  } catch {
@@ -12530,7 +13101,7 @@ var ProjectScanner = class {
12530
13101
  };
12531
13102
 
12532
13103
  // src/blueprint/generator.ts
12533
- var fs21 = __toESM(require("fs/promises"));
13104
+ var fs22 = __toESM(require("fs/promises"));
12534
13105
  var path21 = __toESM(require("path"));
12535
13106
  var ejs = __toESM(require("ejs"));
12536
13107
 
@@ -12615,13 +13186,13 @@ var BlueprintGenerator = class {
12615
13186
  styles: STYLES,
12616
13187
  scripts: SCRIPTS
12617
13188
  });
12618
- await fs21.mkdir(options.outputDir, { recursive: true });
12619
- await fs21.writeFile(path21.join(options.outputDir, "index.html"), html);
13189
+ await fs22.mkdir(options.outputDir, { recursive: true });
13190
+ await fs22.writeFile(path21.join(options.outputDir, "index.html"), html);
12620
13191
  }
12621
13192
  };
12622
13193
 
12623
13194
  // src/update-checker.ts
12624
- var fs22 = __toESM(require("fs"));
13195
+ var fs23 = __toESM(require("fs"));
12625
13196
  var path22 = __toESM(require("path"));
12626
13197
  var os = __toESM(require("os"));
12627
13198
  var import_child_process3 = require("child_process");
@@ -12640,7 +13211,7 @@ function shouldRunCheck(state, intervalMs) {
12640
13211
  }
12641
13212
  function readCheckState() {
12642
13213
  try {
12643
- const raw = fs22.readFileSync(getStatePath(), "utf-8");
13214
+ const raw = fs23.readFileSync(getStatePath(), "utf-8");
12644
13215
  const parsed = JSON.parse(raw);
12645
13216
  if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
12646
13217
  const state = parsed;
@@ -13153,7 +13724,7 @@ function getModelPrice(model, dataset) {
13153
13724
  }
13154
13725
 
13155
13726
  // src/pricing/cache.ts
13156
- var fs23 = __toESM(require("fs/promises"));
13727
+ var fs24 = __toESM(require("fs/promises"));
13157
13728
  var path23 = __toESM(require("path"));
13158
13729
 
13159
13730
  // src/pricing/fallback.json
@@ -13214,7 +13785,7 @@ function getStalenessMarkerPath(projectRoot) {
13214
13785
  }
13215
13786
  async function readDiskCache(projectRoot) {
13216
13787
  try {
13217
- const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
13788
+ const raw = await fs24.readFile(getCachePath(projectRoot), "utf-8");
13218
13789
  return JSON.parse(raw);
13219
13790
  } catch {
13220
13791
  return null;
@@ -13222,8 +13793,8 @@ async function readDiskCache(projectRoot) {
13222
13793
  }
13223
13794
  async function writeDiskCache(projectRoot, data) {
13224
13795
  const cachePath = getCachePath(projectRoot);
13225
- await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
13226
- await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
13796
+ await fs24.mkdir(path23.dirname(cachePath), { recursive: true });
13797
+ await fs24.writeFile(cachePath, JSON.stringify(data, null, 2));
13227
13798
  }
13228
13799
  async function fetchFromNetwork() {
13229
13800
  try {
@@ -13250,7 +13821,7 @@ function loadFallbackDataset() {
13250
13821
  async function checkAndWarnStaleness(projectRoot) {
13251
13822
  const markerPath = getStalenessMarkerPath(projectRoot);
13252
13823
  try {
13253
- const raw = await fs23.readFile(markerPath, "utf-8");
13824
+ const raw = await fs24.readFile(markerPath, "utf-8");
13254
13825
  const marker = JSON.parse(raw);
13255
13826
  const firstUse = new Date(marker.firstFallbackUse).getTime();
13256
13827
  const now = Date.now();
@@ -13262,8 +13833,8 @@ async function checkAndWarnStaleness(projectRoot) {
13262
13833
  }
13263
13834
  } catch {
13264
13835
  try {
13265
- await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
13266
- await fs23.writeFile(
13836
+ await fs24.mkdir(path23.dirname(markerPath), { recursive: true });
13837
+ await fs24.writeFile(
13267
13838
  markerPath,
13268
13839
  JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
13269
13840
  );
@@ -13273,7 +13844,7 @@ async function checkAndWarnStaleness(projectRoot) {
13273
13844
  }
13274
13845
  async function clearStalenessMarker(projectRoot) {
13275
13846
  try {
13276
- await fs23.unlink(getStalenessMarkerPath(projectRoot));
13847
+ await fs24.unlink(getStalenessMarkerPath(projectRoot));
13277
13848
  } catch {
13278
13849
  }
13279
13850
  }
@@ -13443,7 +14014,7 @@ function aggregateByDay(records) {
13443
14014
  }
13444
14015
 
13445
14016
  // src/usage/jsonl-reader.ts
13446
- var fs24 = __toESM(require("fs"));
14017
+ var fs25 = __toESM(require("fs"));
13447
14018
  var path24 = __toESM(require("path"));
13448
14019
  function parseLine(line, lineNumber) {
13449
14020
  let entry;
@@ -13486,7 +14057,7 @@ function readCostRecords(projectRoot) {
13486
14057
  const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
13487
14058
  let raw;
13488
14059
  try {
13489
- raw = fs24.readFileSync(costsFile, "utf-8");
14060
+ raw = fs25.readFileSync(costsFile, "utf-8");
13490
14061
  } catch {
13491
14062
  return [];
13492
14063
  }
@@ -13504,7 +14075,7 @@ function readCostRecords(projectRoot) {
13504
14075
  }
13505
14076
 
13506
14077
  // src/usage/cc-parser.ts
13507
- var fs25 = __toESM(require("fs"));
14078
+ var fs26 = __toESM(require("fs"));
13508
14079
  var path25 = __toESM(require("path"));
13509
14080
  var os2 = __toESM(require("os"));
13510
14081
  function extractUsage(entry) {
@@ -13552,7 +14123,7 @@ function parseCCLine(line, filePath, lineNumber) {
13552
14123
  function readCCFile(filePath) {
13553
14124
  let raw;
13554
14125
  try {
13555
- raw = fs25.readFileSync(filePath, "utf-8");
14126
+ raw = fs26.readFileSync(filePath, "utf-8");
13556
14127
  } catch {
13557
14128
  return [];
13558
14129
  }
@@ -13577,7 +14148,7 @@ function parseCCRecords() {
13577
14148
  const projectsDir = path25.join(homeDir, ".claude", "projects");
13578
14149
  let projectDirs;
13579
14150
  try {
13580
- projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
14151
+ projectDirs = fs26.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
13581
14152
  } catch {
13582
14153
  return [];
13583
14154
  }
@@ -13585,7 +14156,7 @@ function parseCCRecords() {
13585
14156
  for (const dir of projectDirs) {
13586
14157
  let files;
13587
14158
  try {
13588
- files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
14159
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
13589
14160
  } catch {
13590
14161
  continue;
13591
14162
  }
@@ -13644,6 +14215,7 @@ var VERSION = "0.15.0";
13644
14215
  ForbiddenImportCollector,
13645
14216
  GateConfigSchema,
13646
14217
  GateResultSchema,
14218
+ GitHubIssuesSyncAdapter,
13647
14219
  HandoffSchema,
13648
14220
  HarnessStateSchema,
13649
14221
  InteractionTypeSchema,
@@ -13665,6 +14237,7 @@ var VERSION = "0.15.0";
13665
14237
  RuleRegistry,
13666
14238
  SECURITY_DESCRIPTOR,
13667
14239
  STALENESS_WARNING_DAYS,
14240
+ STATUS_RANK,
13668
14241
  SecurityConfigSchema,
13669
14242
  SecurityScanner,
13670
14243
  SharableBoundaryConfigSchema,
@@ -13698,6 +14271,7 @@ var VERSION = "0.15.0";
13698
14271
  archiveLearnings,
13699
14272
  archiveSession,
13700
14273
  archiveStream,
14274
+ assignFeature,
13701
14275
  buildDependencyGraph,
13702
14276
  buildExclusionSet,
13703
14277
  buildSnapshot,
@@ -13762,6 +14336,7 @@ var VERSION = "0.15.0";
13762
14336
  formatGitHubSummary,
13763
14337
  formatOutline,
13764
14338
  formatTerminalOutput,
14339
+ fullSync,
13765
14340
  generateAgentsMap,
13766
14341
  generateSuggestions,
13767
14342
  getActionEmitter,
@@ -13779,6 +14354,7 @@ var VERSION = "0.15.0";
13779
14354
  injectionRules,
13780
14355
  insecureDefaultsRules,
13781
14356
  isDuplicateFinding,
14357
+ isRegression,
13782
14358
  isSmallSuggestion,
13783
14359
  isUpdateCheckEnabled,
13784
14360
  listActiveSessions,
@@ -13832,6 +14408,7 @@ var VERSION = "0.15.0";
13832
14408
  resetParserCache,
13833
14409
  resolveFileToLayer,
13834
14410
  resolveModelTier,
14411
+ resolveReverseStatus,
13835
14412
  resolveRuleSeverity,
13836
14413
  resolveSessionDir,
13837
14414
  resolveStreamPath,
@@ -13852,6 +14429,7 @@ var VERSION = "0.15.0";
13852
14429
  saveStreamIndex,
13853
14430
  scanForInjection,
13854
14431
  scopeContext,
14432
+ scoreRoadmapCandidates,
13855
14433
  searchSymbols,
13856
14434
  secretRules,
13857
14435
  serializeRoadmap,
@@ -13860,7 +14438,9 @@ var VERSION = "0.15.0";
13860
14438
  shouldRunCheck,
13861
14439
  spawnBackgroundCheck,
13862
14440
  syncConstraintNodes,
14441
+ syncFromExternal,
13863
14442
  syncRoadmap,
14443
+ syncToExternal,
13864
14444
  tagUncitedFindings,
13865
14445
  touchStream,
13866
14446
  trackAction,