@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.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,438 @@ 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 GitHubIssuesSyncAdapter = class {
12578
+ token;
12579
+ config;
12580
+ fetchFn;
12581
+ apiBase;
12582
+ owner;
12583
+ repo;
12584
+ constructor(options) {
12585
+ this.token = options.token;
12586
+ this.config = options.config;
12587
+ this.fetchFn = options.fetchFn ?? globalThis.fetch;
12588
+ this.apiBase = options.apiBase ?? "https://api.github.com";
12589
+ const repoParts = (options.config.repo ?? "").split("/");
12590
+ if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
12591
+ throw new Error(`Invalid repo format: "${options.config.repo}". Expected "owner/repo".`);
12592
+ }
12593
+ this.owner = repoParts[0];
12594
+ this.repo = repoParts[1];
12595
+ }
12596
+ headers() {
12597
+ return {
12598
+ Authorization: `Bearer ${this.token}`,
12599
+ Accept: "application/vnd.github+json",
12600
+ "Content-Type": "application/json",
12601
+ "X-GitHub-Api-Version": "2022-11-28"
12602
+ };
12603
+ }
12604
+ async createTicket(feature, milestone) {
12605
+ try {
12606
+ const labels = labelsForStatus(feature.status, this.config);
12607
+ const body = [
12608
+ feature.summary,
12609
+ "",
12610
+ `**Milestone:** ${milestone}`,
12611
+ feature.spec ? `**Spec:** ${feature.spec}` : ""
12612
+ ].filter(Boolean).join("\n");
12613
+ const response = await this.fetchFn(
12614
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues`,
12615
+ {
12616
+ method: "POST",
12617
+ headers: this.headers(),
12618
+ body: JSON.stringify({
12619
+ title: feature.name,
12620
+ body,
12621
+ labels
12622
+ })
12623
+ }
12624
+ );
12625
+ if (!response.ok) {
12626
+ const text = await response.text();
12627
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12628
+ }
12629
+ const data = await response.json();
12630
+ const externalId = buildExternalId(this.owner, this.repo, data.number);
12631
+ return (0, import_types21.Ok)({ externalId, url: data.html_url });
12632
+ } catch (error) {
12633
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12634
+ }
12635
+ }
12636
+ async updateTicket(externalId, changes) {
12637
+ try {
12638
+ const parsed = parseExternalId(externalId);
12639
+ if (!parsed) return (0, import_types21.Err)(new Error(`Invalid externalId format: "${externalId}"`));
12640
+ const patch = {};
12641
+ if (changes.name !== void 0) patch.title = changes.name;
12642
+ if (changes.summary !== void 0) {
12643
+ const body = [changes.summary, "", changes.spec ? `**Spec:** ${changes.spec}` : ""].filter(Boolean).join("\n");
12644
+ patch.body = body;
12645
+ }
12646
+ if (changes.status !== void 0) {
12647
+ const externalStatus = this.config.statusMap[changes.status];
12648
+ patch.state = externalStatus;
12649
+ patch.labels = labelsForStatus(changes.status, this.config);
12650
+ }
12651
+ const response = await this.fetchFn(
12652
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
12653
+ {
12654
+ method: "PATCH",
12655
+ headers: this.headers(),
12656
+ body: JSON.stringify(patch)
12657
+ }
12658
+ );
12659
+ if (!response.ok) {
12660
+ const text = await response.text();
12661
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12662
+ }
12663
+ const data = await response.json();
12664
+ return (0, import_types21.Ok)({ externalId, url: data.html_url });
12665
+ } catch (error) {
12666
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12667
+ }
12668
+ }
12669
+ async fetchTicketState(externalId) {
12670
+ try {
12671
+ const parsed = parseExternalId(externalId);
12672
+ if (!parsed) return (0, import_types21.Err)(new Error(`Invalid externalId format: "${externalId}"`));
12673
+ const response = await this.fetchFn(
12674
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
12675
+ {
12676
+ method: "GET",
12677
+ headers: this.headers()
12678
+ }
12679
+ );
12680
+ if (!response.ok) {
12681
+ const text = await response.text();
12682
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12683
+ }
12684
+ const data = await response.json();
12685
+ return (0, import_types21.Ok)({
12686
+ externalId,
12687
+ status: data.state,
12688
+ labels: data.labels.map((l) => l.name),
12689
+ assignee: data.assignee ? `@${data.assignee.login}` : null
12690
+ });
12691
+ } catch (error) {
12692
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12693
+ }
12694
+ }
12695
+ async fetchAllTickets() {
12696
+ try {
12697
+ const filterLabels = this.config.labels ?? [];
12698
+ const labelsParam = filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
12699
+ const tickets = [];
12700
+ let page = 1;
12701
+ const perPage = 100;
12702
+ while (true) {
12703
+ const response = await this.fetchFn(
12704
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
12705
+ {
12706
+ method: "GET",
12707
+ headers: this.headers()
12708
+ }
12709
+ );
12710
+ if (!response.ok) {
12711
+ const text = await response.text();
12712
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12713
+ }
12714
+ const data = await response.json();
12715
+ const issues = data.filter((d) => !d.pull_request);
12716
+ for (const issue of issues) {
12717
+ tickets.push({
12718
+ externalId: buildExternalId(this.owner, this.repo, issue.number),
12719
+ status: issue.state,
12720
+ labels: issue.labels.map((l) => l.name),
12721
+ assignee: issue.assignee ? `@${issue.assignee.login}` : null
12722
+ });
12723
+ }
12724
+ if (data.length < perPage) break;
12725
+ page++;
12726
+ }
12727
+ return (0, import_types21.Ok)(tickets);
12728
+ } catch (error) {
12729
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12730
+ }
12731
+ }
12732
+ async assignTicket(externalId, assignee) {
12733
+ try {
12734
+ const parsed = parseExternalId(externalId);
12735
+ if (!parsed) return (0, import_types21.Err)(new Error(`Invalid externalId format: "${externalId}"`));
12736
+ const login = assignee.startsWith("@") ? assignee.slice(1) : assignee;
12737
+ const response = await this.fetchFn(
12738
+ `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/assignees`,
12739
+ {
12740
+ method: "POST",
12741
+ headers: this.headers(),
12742
+ body: JSON.stringify({ assignees: [login] })
12743
+ }
12744
+ );
12745
+ if (!response.ok) {
12746
+ const text = await response.text();
12747
+ return (0, import_types21.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
12748
+ }
12749
+ return (0, import_types21.Ok)(void 0);
12750
+ } catch (error) {
12751
+ return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
12752
+ }
12753
+ }
12754
+ };
12755
+
12756
+ // src/roadmap/sync-engine.ts
12757
+ var fs20 = __toESM(require("fs"));
12758
+ function emptySyncResult() {
12759
+ return { created: [], updated: [], assignmentChanges: [], errors: [] };
12760
+ }
12761
+ async function syncToExternal(roadmap, adapter, _config) {
12762
+ const result = emptySyncResult();
12763
+ for (const milestone of roadmap.milestones) {
12764
+ for (const feature of milestone.features) {
12765
+ if (!feature.externalId) {
12766
+ const createResult = await adapter.createTicket(feature, milestone.name);
12767
+ if (createResult.ok) {
12768
+ feature.externalId = createResult.value.externalId;
12769
+ result.created.push(createResult.value);
12770
+ } else {
12771
+ result.errors.push({ featureOrId: feature.name, error: createResult.error });
12772
+ }
12773
+ } else {
12774
+ const updateResult = await adapter.updateTicket(feature.externalId, feature);
12775
+ if (updateResult.ok) {
12776
+ result.updated.push(feature.externalId);
12777
+ } else {
12778
+ result.errors.push({ featureOrId: feature.externalId, error: updateResult.error });
12779
+ }
12780
+ }
12781
+ }
12782
+ }
12783
+ return result;
12784
+ }
12785
+ async function syncFromExternal(roadmap, adapter, config, options) {
12786
+ const result = emptySyncResult();
12787
+ const forceSync = options?.forceSync ?? false;
12788
+ const featureByExternalId = /* @__PURE__ */ new Map();
12789
+ for (const milestone of roadmap.milestones) {
12790
+ for (const feature of milestone.features) {
12791
+ if (feature.externalId) {
12792
+ featureByExternalId.set(feature.externalId, feature);
12793
+ }
12794
+ }
12795
+ }
12796
+ if (featureByExternalId.size === 0) return result;
12797
+ const fetchResult = await adapter.fetchAllTickets();
12798
+ if (!fetchResult.ok) {
12799
+ result.errors.push({ featureOrId: "*", error: fetchResult.error });
12800
+ return result;
12801
+ }
12802
+ for (const ticketState of fetchResult.value) {
12803
+ const feature = featureByExternalId.get(ticketState.externalId);
12804
+ if (!feature) continue;
12805
+ if (ticketState.assignee !== feature.assignee) {
12806
+ result.assignmentChanges.push({
12807
+ feature: feature.name,
12808
+ from: feature.assignee,
12809
+ to: ticketState.assignee
12810
+ });
12811
+ feature.assignee = ticketState.assignee;
12812
+ }
12813
+ const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
12814
+ if (resolvedStatus && resolvedStatus !== feature.status) {
12815
+ const newStatus = resolvedStatus;
12816
+ if (!forceSync && isRegression(feature.status, newStatus)) {
12817
+ continue;
12818
+ }
12819
+ feature.status = newStatus;
12820
+ }
12821
+ }
12822
+ return result;
12823
+ }
12824
+ var syncMutex = Promise.resolve();
12825
+ async function fullSync(roadmapPath, adapter, config, options) {
12826
+ const previousSync = syncMutex;
12827
+ let releaseMutex;
12828
+ syncMutex = new Promise((resolve7) => {
12829
+ releaseMutex = resolve7;
12830
+ });
12831
+ await previousSync;
12832
+ try {
12833
+ const raw = fs20.readFileSync(roadmapPath, "utf-8");
12834
+ const parseResult = parseRoadmap(raw);
12835
+ if (!parseResult.ok) {
12836
+ return {
12837
+ ...emptySyncResult(),
12838
+ errors: [{ featureOrId: "*", error: parseResult.error }]
12839
+ };
12840
+ }
12841
+ const roadmap = parseResult.value;
12842
+ const pushResult = await syncToExternal(roadmap, adapter, config);
12843
+ const pullResult = await syncFromExternal(roadmap, adapter, config, options);
12844
+ fs20.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
12845
+ return {
12846
+ created: pushResult.created,
12847
+ updated: pushResult.updated,
12848
+ assignmentChanges: pullResult.assignmentChanges,
12849
+ errors: [...pushResult.errors, ...pullResult.errors]
12850
+ };
12851
+ } finally {
12852
+ releaseMutex();
12853
+ }
12854
+ }
12855
+
12856
+ // src/roadmap/pilot-scoring.ts
12857
+ var PRIORITY_RANK = {
12858
+ P0: 0,
12859
+ P1: 1,
12860
+ P2: 2,
12861
+ P3: 3
12862
+ };
12863
+ var POSITION_WEIGHT = 0.5;
12864
+ var DEPENDENTS_WEIGHT = 0.3;
12865
+ var AFFINITY_WEIGHT = 0.2;
12866
+ function scoreRoadmapCandidates(roadmap, options) {
12867
+ const allFeatures = roadmap.milestones.flatMap((m) => m.features);
12868
+ const allFeatureNames = new Set(allFeatures.map((f) => f.name.toLowerCase()));
12869
+ const doneFeatures = new Set(
12870
+ allFeatures.filter((f) => f.status === "done").map((f) => f.name.toLowerCase())
12871
+ );
12872
+ const dependentsCount = /* @__PURE__ */ new Map();
12873
+ for (const feature of allFeatures) {
12874
+ for (const blocker of feature.blockedBy) {
12875
+ const key = blocker.toLowerCase();
12876
+ dependentsCount.set(key, (dependentsCount.get(key) ?? 0) + 1);
12877
+ }
12878
+ }
12879
+ const maxDependents = Math.max(1, ...dependentsCount.values());
12880
+ const milestoneMap = /* @__PURE__ */ new Map();
12881
+ for (const ms of roadmap.milestones) {
12882
+ milestoneMap.set(
12883
+ ms.name,
12884
+ ms.features.map((f) => f.name.toLowerCase())
12885
+ );
12886
+ }
12887
+ const userCompletedFeatures = /* @__PURE__ */ new Set();
12888
+ if (options?.currentUser) {
12889
+ const user = options.currentUser.toLowerCase();
12890
+ for (const record of roadmap.assignmentHistory) {
12891
+ if (record.action === "completed" && record.assignee.toLowerCase() === user) {
12892
+ userCompletedFeatures.add(record.feature.toLowerCase());
12893
+ }
12894
+ }
12895
+ }
12896
+ let totalPositions = 0;
12897
+ for (const ms of roadmap.milestones) {
12898
+ totalPositions += ms.features.length;
12899
+ }
12900
+ totalPositions = Math.max(1, totalPositions);
12901
+ const candidates = [];
12902
+ let globalPosition = 0;
12903
+ for (const ms of roadmap.milestones) {
12904
+ for (let featureIdx = 0; featureIdx < ms.features.length; featureIdx++) {
12905
+ const feature = ms.features[featureIdx];
12906
+ globalPosition++;
12907
+ if (feature.status !== "planned" && feature.status !== "backlog") continue;
12908
+ const isBlocked = feature.blockedBy.some((blocker) => {
12909
+ const key = blocker.toLowerCase();
12910
+ return allFeatureNames.has(key) && !doneFeatures.has(key);
12911
+ });
12912
+ if (isBlocked) continue;
12913
+ const positionScore = 1 - (globalPosition - 1) / totalPositions;
12914
+ const deps = dependentsCount.get(feature.name.toLowerCase()) ?? 0;
12915
+ const dependentsScore = deps / maxDependents;
12916
+ let affinityScore = 0;
12917
+ if (userCompletedFeatures.size > 0) {
12918
+ const completedBlockers = feature.blockedBy.filter(
12919
+ (b) => userCompletedFeatures.has(b.toLowerCase())
12920
+ );
12921
+ if (completedBlockers.length > 0) {
12922
+ affinityScore = 1;
12923
+ } else {
12924
+ const siblings = milestoneMap.get(ms.name) ?? [];
12925
+ const completedSiblings = siblings.filter((s) => userCompletedFeatures.has(s));
12926
+ if (completedSiblings.length > 0) {
12927
+ affinityScore = 0.5;
12928
+ }
12929
+ }
12930
+ }
12931
+ const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
12932
+ const priorityTier = feature.priority ? PRIORITY_RANK[feature.priority] : null;
12933
+ candidates.push({
12934
+ feature,
12935
+ milestone: ms.name,
12936
+ positionScore,
12937
+ dependentsScore,
12938
+ affinityScore,
12939
+ weightedScore,
12940
+ priorityTier
12941
+ });
12942
+ }
12943
+ }
12944
+ candidates.sort((a, b) => {
12945
+ if (a.priorityTier !== null && b.priorityTier === null) return -1;
12946
+ if (a.priorityTier === null && b.priorityTier !== null) return 1;
12947
+ if (a.priorityTier !== null && b.priorityTier !== null) {
12948
+ if (a.priorityTier !== b.priorityTier) return a.priorityTier - b.priorityTier;
12949
+ }
12950
+ return b.weightedScore - a.weightedScore;
12951
+ });
12952
+ return candidates;
12953
+ }
12954
+ function assignFeature(roadmap, feature, assignee, date) {
12955
+ if (feature.assignee === assignee) return;
12956
+ if (feature.assignee !== null) {
12957
+ roadmap.assignmentHistory.push({
12958
+ feature: feature.name,
12959
+ assignee: feature.assignee,
12960
+ action: "unassigned",
12961
+ date
12962
+ });
12963
+ }
12964
+ feature.assignee = assignee;
12965
+ roadmap.assignmentHistory.push({
12966
+ feature: feature.name,
12967
+ assignee,
12968
+ action: "assigned",
12969
+ date
12970
+ });
12971
+ }
12972
+
12452
12973
  // src/interaction/types.ts
12453
12974
  var import_zod8 = require("zod");
12454
12975
  var InteractionTypeSchema = import_zod8.z.enum(["question", "confirmation", "transition"]);
@@ -12479,17 +13000,18 @@ var EmitInteractionInputSchema = import_zod8.z.object({
12479
13000
  });
12480
13001
 
12481
13002
  // src/blueprint/scanner.ts
12482
- var fs20 = __toESM(require("fs/promises"));
13003
+ var fs21 = __toESM(require("fs/promises"));
12483
13004
  var path20 = __toESM(require("path"));
12484
13005
  var ProjectScanner = class {
12485
13006
  constructor(rootDir) {
12486
13007
  this.rootDir = rootDir;
12487
13008
  }
13009
+ rootDir;
12488
13010
  async scan() {
12489
13011
  let projectName = path20.basename(this.rootDir);
12490
13012
  try {
12491
13013
  const pkgPath = path20.join(this.rootDir, "package.json");
12492
- const pkgRaw = await fs20.readFile(pkgPath, "utf-8");
13014
+ const pkgRaw = await fs21.readFile(pkgPath, "utf-8");
12493
13015
  const pkg = JSON.parse(pkgRaw);
12494
13016
  if (pkg.name) projectName = pkg.name;
12495
13017
  } catch {
@@ -12530,7 +13052,7 @@ var ProjectScanner = class {
12530
13052
  };
12531
13053
 
12532
13054
  // src/blueprint/generator.ts
12533
- var fs21 = __toESM(require("fs/promises"));
13055
+ var fs22 = __toESM(require("fs/promises"));
12534
13056
  var path21 = __toESM(require("path"));
12535
13057
  var ejs = __toESM(require("ejs"));
12536
13058
 
@@ -12615,13 +13137,13 @@ var BlueprintGenerator = class {
12615
13137
  styles: STYLES,
12616
13138
  scripts: SCRIPTS
12617
13139
  });
12618
- await fs21.mkdir(options.outputDir, { recursive: true });
12619
- await fs21.writeFile(path21.join(options.outputDir, "index.html"), html);
13140
+ await fs22.mkdir(options.outputDir, { recursive: true });
13141
+ await fs22.writeFile(path21.join(options.outputDir, "index.html"), html);
12620
13142
  }
12621
13143
  };
12622
13144
 
12623
13145
  // src/update-checker.ts
12624
- var fs22 = __toESM(require("fs"));
13146
+ var fs23 = __toESM(require("fs"));
12625
13147
  var path22 = __toESM(require("path"));
12626
13148
  var os = __toESM(require("os"));
12627
13149
  var import_child_process3 = require("child_process");
@@ -12640,7 +13162,7 @@ function shouldRunCheck(state, intervalMs) {
12640
13162
  }
12641
13163
  function readCheckState() {
12642
13164
  try {
12643
- const raw = fs22.readFileSync(getStatePath(), "utf-8");
13165
+ const raw = fs23.readFileSync(getStatePath(), "utf-8");
12644
13166
  const parsed = JSON.parse(raw);
12645
13167
  if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
12646
13168
  const state = parsed;
@@ -13153,7 +13675,7 @@ function getModelPrice(model, dataset) {
13153
13675
  }
13154
13676
 
13155
13677
  // src/pricing/cache.ts
13156
- var fs23 = __toESM(require("fs/promises"));
13678
+ var fs24 = __toESM(require("fs/promises"));
13157
13679
  var path23 = __toESM(require("path"));
13158
13680
 
13159
13681
  // src/pricing/fallback.json
@@ -13214,7 +13736,7 @@ function getStalenessMarkerPath(projectRoot) {
13214
13736
  }
13215
13737
  async function readDiskCache(projectRoot) {
13216
13738
  try {
13217
- const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
13739
+ const raw = await fs24.readFile(getCachePath(projectRoot), "utf-8");
13218
13740
  return JSON.parse(raw);
13219
13741
  } catch {
13220
13742
  return null;
@@ -13222,8 +13744,8 @@ async function readDiskCache(projectRoot) {
13222
13744
  }
13223
13745
  async function writeDiskCache(projectRoot, data) {
13224
13746
  const cachePath = getCachePath(projectRoot);
13225
- await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
13226
- await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
13747
+ await fs24.mkdir(path23.dirname(cachePath), { recursive: true });
13748
+ await fs24.writeFile(cachePath, JSON.stringify(data, null, 2));
13227
13749
  }
13228
13750
  async function fetchFromNetwork() {
13229
13751
  try {
@@ -13250,7 +13772,7 @@ function loadFallbackDataset() {
13250
13772
  async function checkAndWarnStaleness(projectRoot) {
13251
13773
  const markerPath = getStalenessMarkerPath(projectRoot);
13252
13774
  try {
13253
- const raw = await fs23.readFile(markerPath, "utf-8");
13775
+ const raw = await fs24.readFile(markerPath, "utf-8");
13254
13776
  const marker = JSON.parse(raw);
13255
13777
  const firstUse = new Date(marker.firstFallbackUse).getTime();
13256
13778
  const now = Date.now();
@@ -13262,8 +13784,8 @@ async function checkAndWarnStaleness(projectRoot) {
13262
13784
  }
13263
13785
  } catch {
13264
13786
  try {
13265
- await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
13266
- await fs23.writeFile(
13787
+ await fs24.mkdir(path23.dirname(markerPath), { recursive: true });
13788
+ await fs24.writeFile(
13267
13789
  markerPath,
13268
13790
  JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
13269
13791
  );
@@ -13273,7 +13795,7 @@ async function checkAndWarnStaleness(projectRoot) {
13273
13795
  }
13274
13796
  async function clearStalenessMarker(projectRoot) {
13275
13797
  try {
13276
- await fs23.unlink(getStalenessMarkerPath(projectRoot));
13798
+ await fs24.unlink(getStalenessMarkerPath(projectRoot));
13277
13799
  } catch {
13278
13800
  }
13279
13801
  }
@@ -13443,7 +13965,7 @@ function aggregateByDay(records) {
13443
13965
  }
13444
13966
 
13445
13967
  // src/usage/jsonl-reader.ts
13446
- var fs24 = __toESM(require("fs"));
13968
+ var fs25 = __toESM(require("fs"));
13447
13969
  var path24 = __toESM(require("path"));
13448
13970
  function parseLine(line, lineNumber) {
13449
13971
  let entry;
@@ -13486,7 +14008,7 @@ function readCostRecords(projectRoot) {
13486
14008
  const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
13487
14009
  let raw;
13488
14010
  try {
13489
- raw = fs24.readFileSync(costsFile, "utf-8");
14011
+ raw = fs25.readFileSync(costsFile, "utf-8");
13490
14012
  } catch {
13491
14013
  return [];
13492
14014
  }
@@ -13504,7 +14026,7 @@ function readCostRecords(projectRoot) {
13504
14026
  }
13505
14027
 
13506
14028
  // src/usage/cc-parser.ts
13507
- var fs25 = __toESM(require("fs"));
14029
+ var fs26 = __toESM(require("fs"));
13508
14030
  var path25 = __toESM(require("path"));
13509
14031
  var os2 = __toESM(require("os"));
13510
14032
  function extractUsage(entry) {
@@ -13552,7 +14074,7 @@ function parseCCLine(line, filePath, lineNumber) {
13552
14074
  function readCCFile(filePath) {
13553
14075
  let raw;
13554
14076
  try {
13555
- raw = fs25.readFileSync(filePath, "utf-8");
14077
+ raw = fs26.readFileSync(filePath, "utf-8");
13556
14078
  } catch {
13557
14079
  return [];
13558
14080
  }
@@ -13577,7 +14099,7 @@ function parseCCRecords() {
13577
14099
  const projectsDir = path25.join(homeDir, ".claude", "projects");
13578
14100
  let projectDirs;
13579
14101
  try {
13580
- projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
14102
+ projectDirs = fs26.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
13581
14103
  } catch {
13582
14104
  return [];
13583
14105
  }
@@ -13585,7 +14107,7 @@ function parseCCRecords() {
13585
14107
  for (const dir of projectDirs) {
13586
14108
  let files;
13587
14109
  try {
13588
- files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
14110
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
13589
14111
  } catch {
13590
14112
  continue;
13591
14113
  }
@@ -13644,6 +14166,7 @@ var VERSION = "0.15.0";
13644
14166
  ForbiddenImportCollector,
13645
14167
  GateConfigSchema,
13646
14168
  GateResultSchema,
14169
+ GitHubIssuesSyncAdapter,
13647
14170
  HandoffSchema,
13648
14171
  HarnessStateSchema,
13649
14172
  InteractionTypeSchema,
@@ -13665,6 +14188,7 @@ var VERSION = "0.15.0";
13665
14188
  RuleRegistry,
13666
14189
  SECURITY_DESCRIPTOR,
13667
14190
  STALENESS_WARNING_DAYS,
14191
+ STATUS_RANK,
13668
14192
  SecurityConfigSchema,
13669
14193
  SecurityScanner,
13670
14194
  SharableBoundaryConfigSchema,
@@ -13698,6 +14222,7 @@ var VERSION = "0.15.0";
13698
14222
  archiveLearnings,
13699
14223
  archiveSession,
13700
14224
  archiveStream,
14225
+ assignFeature,
13701
14226
  buildDependencyGraph,
13702
14227
  buildExclusionSet,
13703
14228
  buildSnapshot,
@@ -13762,6 +14287,7 @@ var VERSION = "0.15.0";
13762
14287
  formatGitHubSummary,
13763
14288
  formatOutline,
13764
14289
  formatTerminalOutput,
14290
+ fullSync,
13765
14291
  generateAgentsMap,
13766
14292
  generateSuggestions,
13767
14293
  getActionEmitter,
@@ -13779,6 +14305,7 @@ var VERSION = "0.15.0";
13779
14305
  injectionRules,
13780
14306
  insecureDefaultsRules,
13781
14307
  isDuplicateFinding,
14308
+ isRegression,
13782
14309
  isSmallSuggestion,
13783
14310
  isUpdateCheckEnabled,
13784
14311
  listActiveSessions,
@@ -13832,6 +14359,7 @@ var VERSION = "0.15.0";
13832
14359
  resetParserCache,
13833
14360
  resolveFileToLayer,
13834
14361
  resolveModelTier,
14362
+ resolveReverseStatus,
13835
14363
  resolveRuleSeverity,
13836
14364
  resolveSessionDir,
13837
14365
  resolveStreamPath,
@@ -13852,6 +14380,7 @@ var VERSION = "0.15.0";
13852
14380
  saveStreamIndex,
13853
14381
  scanForInjection,
13854
14382
  scopeContext,
14383
+ scoreRoadmapCandidates,
13855
14384
  searchSymbols,
13856
14385
  secretRules,
13857
14386
  serializeRoadmap,
@@ -13860,7 +14389,9 @@ var VERSION = "0.15.0";
13860
14389
  shouldRunCheck,
13861
14390
  spawnBackgroundCheck,
13862
14391
  syncConstraintNodes,
14392
+ syncFromExternal,
13863
14393
  syncRoadmap,
14394
+ syncToExternal,
13864
14395
  tagUncitedFindings,
13865
14396
  touchStream,
13866
14397
  trackAction,