@harness-engineering/orchestrator 0.8.3 → 0.8.4

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.d.mts CHANGED
@@ -296,11 +296,38 @@ declare class PRDetector {
296
296
  */
297
297
  hasOpenPRForIdentifier(identifier: string): Promise<boolean>;
298
298
  /**
299
- * Filters out candidates that already have an open GitHub PR, running
300
- * checks with limited concurrency to avoid overwhelming the GitHub API.
301
- * For candidates with an externalId, searches for PRs linked to the
302
- * GitHub issue. Falls back to `feat/<identifier>` branch lookup otherwise.
303
- * Fail-open on API errors.
299
+ * GitHub closing keywords that link a PR to an issue it will close.
300
+ * @see https://docs.github.com/articles/closing-issues-using-keywords
301
+ */
302
+ private static readonly CLOSING_REF_RE;
303
+ /**
304
+ * Extracts the issue numbers a PR body declares it will close
305
+ * (e.g. `Closes #42`, `fixes #7`, `Resolves: #99`).
306
+ */
307
+ private parseClosingIssueNumbers;
308
+ /**
309
+ * Lists every open PR for a repo in a single `gh pr list` call and returns
310
+ * the set of issue numbers those PRs close (parsed from their bodies).
311
+ *
312
+ * This is the batched replacement for per-issue `gh pr list --search
313
+ * "closes #N"` queries. Every `gh pr list` form (search or plain list) is
314
+ * served by GitHub's GraphQL API and consumes from the shared ~5000/hr
315
+ * GraphQL budget, so issuing one query per candidate per tick exhausted the
316
+ * limit on busy boards. A single `--state open` list returns every open PR
317
+ * in one request regardless of candidate count, collapsing N calls into 1.
318
+ *
319
+ * Returns `null` if the check itself failed (gh missing, rate limited,
320
+ * network error) so callers can fail open rather than block real work.
321
+ */
322
+ fetchOpenPRClosures(owner: string, repo: string): Promise<Set<number> | null>;
323
+ /**
324
+ * Filters out candidates that already have an open GitHub PR.
325
+ *
326
+ * For GitHub-issue candidates the check is batched: one `gh pr list` per
327
+ * distinct repo (not one search per issue), parsing closing references
328
+ * locally. Candidates without a GitHub externalId fall back to a
329
+ * `feat/<identifier>` branch lookup, throttled to a few concurrent gh calls.
330
+ * Fail-open on API errors so a flaky/rate-limited GitHub never blocks work.
304
331
  */
305
332
  filterCandidatesWithOpenPRs(candidates: Issue[]): Promise<Issue[]>;
306
333
  }
package/dist/index.d.ts CHANGED
@@ -296,11 +296,38 @@ declare class PRDetector {
296
296
  */
297
297
  hasOpenPRForIdentifier(identifier: string): Promise<boolean>;
298
298
  /**
299
- * Filters out candidates that already have an open GitHub PR, running
300
- * checks with limited concurrency to avoid overwhelming the GitHub API.
301
- * For candidates with an externalId, searches for PRs linked to the
302
- * GitHub issue. Falls back to `feat/<identifier>` branch lookup otherwise.
303
- * Fail-open on API errors.
299
+ * GitHub closing keywords that link a PR to an issue it will close.
300
+ * @see https://docs.github.com/articles/closing-issues-using-keywords
301
+ */
302
+ private static readonly CLOSING_REF_RE;
303
+ /**
304
+ * Extracts the issue numbers a PR body declares it will close
305
+ * (e.g. `Closes #42`, `fixes #7`, `Resolves: #99`).
306
+ */
307
+ private parseClosingIssueNumbers;
308
+ /**
309
+ * Lists every open PR for a repo in a single `gh pr list` call and returns
310
+ * the set of issue numbers those PRs close (parsed from their bodies).
311
+ *
312
+ * This is the batched replacement for per-issue `gh pr list --search
313
+ * "closes #N"` queries. Every `gh pr list` form (search or plain list) is
314
+ * served by GitHub's GraphQL API and consumes from the shared ~5000/hr
315
+ * GraphQL budget, so issuing one query per candidate per tick exhausted the
316
+ * limit on busy boards. A single `--state open` list returns every open PR
317
+ * in one request regardless of candidate count, collapsing N calls into 1.
318
+ *
319
+ * Returns `null` if the check itself failed (gh missing, rate limited,
320
+ * network error) so callers can fail open rather than block real work.
321
+ */
322
+ fetchOpenPRClosures(owner: string, repo: string): Promise<Set<number> | null>;
323
+ /**
324
+ * Filters out candidates that already have an open GitHub PR.
325
+ *
326
+ * For GitHub-issue candidates the check is batched: one `gh pr list` per
327
+ * distinct repo (not one search per issue), parsing closing references
328
+ * locally. Candidates without a GitHub externalId fall back to a
329
+ * `feat/<identifier>` branch lookup, throttled to a few concurrent gh calls.
330
+ * Fail-open on API errors so a flaky/rate-limited GitHub never blocks work.
304
331
  */
305
332
  filterCandidatesWithOpenPRs(candidates: Issue[]): Promise<Issue[]>;
306
333
  }
package/dist/index.js CHANGED
@@ -1341,7 +1341,7 @@ var ClaimManager = class {
1341
1341
  // src/core/pr-detector.ts
1342
1342
  var import_node_child_process = require("child_process");
1343
1343
  var import_node_util = require("util");
1344
- var PRDetector = class {
1344
+ var PRDetector = class _PRDetector {
1345
1345
  logger;
1346
1346
  execFileFn;
1347
1347
  projectRoot;
@@ -1457,35 +1457,124 @@ var PRDetector = class {
1457
1457
  }
1458
1458
  }
1459
1459
  /**
1460
- * Filters out candidates that already have an open GitHub PR, running
1461
- * checks with limited concurrency to avoid overwhelming the GitHub API.
1462
- * For candidates with an externalId, searches for PRs linked to the
1463
- * GitHub issue. Falls back to `feat/<identifier>` branch lookup otherwise.
1464
- * Fail-open on API errors.
1460
+ * GitHub closing keywords that link a PR to an issue it will close.
1461
+ * @see https://docs.github.com/articles/closing-issues-using-keywords
1462
+ */
1463
+ static CLOSING_REF_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)[\s:]+#(\d+)/gi;
1464
+ /**
1465
+ * Extracts the issue numbers a PR body declares it will close
1466
+ * (e.g. `Closes #42`, `fixes #7`, `Resolves: #99`).
1467
+ */
1468
+ parseClosingIssueNumbers(body) {
1469
+ const nums = [];
1470
+ for (const match of body.matchAll(_PRDetector.CLOSING_REF_RE)) {
1471
+ nums.push(parseInt(match[1], 10));
1472
+ }
1473
+ return nums;
1474
+ }
1475
+ /**
1476
+ * Lists every open PR for a repo in a single `gh pr list` call and returns
1477
+ * the set of issue numbers those PRs close (parsed from their bodies).
1478
+ *
1479
+ * This is the batched replacement for per-issue `gh pr list --search
1480
+ * "closes #N"` queries. Every `gh pr list` form (search or plain list) is
1481
+ * served by GitHub's GraphQL API and consumes from the shared ~5000/hr
1482
+ * GraphQL budget, so issuing one query per candidate per tick exhausted the
1483
+ * limit on busy boards. A single `--state open` list returns every open PR
1484
+ * in one request regardless of candidate count, collapsing N calls into 1.
1485
+ *
1486
+ * Returns `null` if the check itself failed (gh missing, rate limited,
1487
+ * network error) so callers can fail open rather than block real work.
1488
+ */
1489
+ async fetchOpenPRClosures(owner, repo) {
1490
+ try {
1491
+ const exec = (0, import_node_util.promisify)(this.execFileFn);
1492
+ const { stdout } = await exec(
1493
+ "gh",
1494
+ [
1495
+ "pr",
1496
+ "list",
1497
+ "--repo",
1498
+ `${owner}/${repo}`,
1499
+ "--state",
1500
+ "open",
1501
+ "--json",
1502
+ "body",
1503
+ "--limit",
1504
+ "200"
1505
+ ],
1506
+ {
1507
+ cwd: this.projectRoot,
1508
+ timeout: 15e3
1509
+ }
1510
+ );
1511
+ const prs = JSON.parse(stdout);
1512
+ const closed = /* @__PURE__ */ new Set();
1513
+ for (const pr of prs) {
1514
+ for (const n of this.parseClosingIssueNumbers(pr.body ?? "")) closed.add(n);
1515
+ }
1516
+ return closed;
1517
+ } catch (err) {
1518
+ this.logger.debug(`Failed to list open PRs for ${owner}/${repo}`, {
1519
+ error: String(err)
1520
+ });
1521
+ return null;
1522
+ }
1523
+ }
1524
+ /**
1525
+ * Filters out candidates that already have an open GitHub PR.
1526
+ *
1527
+ * For GitHub-issue candidates the check is batched: one `gh pr list` per
1528
+ * distinct repo (not one search per issue), parsing closing references
1529
+ * locally. Candidates without a GitHub externalId fall back to a
1530
+ * `feat/<identifier>` branch lookup, throttled to a few concurrent gh calls.
1531
+ * Fail-open on API errors so a flaky/rate-limited GitHub never blocks work.
1465
1532
  */
1466
1533
  async filterCandidatesWithOpenPRs(candidates) {
1534
+ const repos = /* @__PURE__ */ new Map();
1535
+ for (const candidate of candidates) {
1536
+ const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
1537
+ if (parsed) repos.set(`${parsed.owner}/${parsed.repo}`, parsed);
1538
+ }
1539
+ const repoClosures = /* @__PURE__ */ new Map();
1540
+ await Promise.all(
1541
+ [...repos.values()].map(async ({ owner, repo }) => {
1542
+ repoClosures.set(`${owner}/${repo}`, await this.fetchOpenPRClosures(owner, repo));
1543
+ })
1544
+ );
1545
+ const identifierCandidates = candidates.filter(
1546
+ (c) => !(c.externalId && this.parseExternalId(c.externalId))
1547
+ );
1548
+ const identifiersWithOpenPR = /* @__PURE__ */ new Set();
1467
1549
  const concurrency = 3;
1468
- const results = [];
1469
- for (let i = 0; i < candidates.length; i += concurrency) {
1470
- const batch = candidates.slice(i, i + concurrency);
1471
- const batchResults = await Promise.allSettled(
1472
- batch.map(async (candidate) => {
1473
- const hasOpenPR = candidate.externalId ? await this.hasOpenPRForExternalId(candidate.externalId) : await this.hasOpenPRForIdentifier(candidate.identifier);
1474
- return { candidate, hasOpenPR };
1475
- })
1550
+ for (let i = 0; i < identifierCandidates.length; i += concurrency) {
1551
+ const batch = identifierCandidates.slice(i, i + concurrency);
1552
+ const settled = await Promise.allSettled(
1553
+ batch.map(async (c) => ({
1554
+ identifier: c.identifier,
1555
+ hasOpenPR: await this.hasOpenPRForIdentifier(c.identifier)
1556
+ }))
1476
1557
  );
1477
- results.push(...batchResults);
1558
+ for (const result of settled) {
1559
+ if (result.status === "fulfilled" && result.value.hasOpenPR) {
1560
+ identifiersWithOpenPR.add(result.value.identifier);
1561
+ }
1562
+ }
1478
1563
  }
1479
1564
  const filtered = [];
1480
- for (let i = 0; i < results.length; i++) {
1481
- const result = results[i];
1482
- if (!result || result.status === "rejected") {
1483
- filtered.push(candidates[i]);
1484
- continue;
1565
+ for (const candidate of candidates) {
1566
+ const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
1567
+ let hasOpenPR;
1568
+ let via;
1569
+ if (parsed) {
1570
+ const closures = repoClosures.get(`${parsed.owner}/${parsed.repo}`);
1571
+ hasOpenPR = closures ? closures.has(parsed.number) : false;
1572
+ via = `externalId ${candidate.externalId}`;
1573
+ } else {
1574
+ hasOpenPR = identifiersWithOpenPR.has(candidate.identifier);
1575
+ via = `feat/${candidate.identifier}`;
1485
1576
  }
1486
- const { candidate, hasOpenPR } = result.value;
1487
1577
  if (hasOpenPR) {
1488
- const via = candidate.externalId ? `externalId ${candidate.externalId}` : `feat/${candidate.identifier}`;
1489
1578
  this.logger.info(`Skipping ${candidate.title}: open PR exists (${via})`);
1490
1579
  } else {
1491
1580
  filtered.push(candidate);
@@ -2309,7 +2398,8 @@ var RoadmapTrackerAdapter = class {
2309
2398
  if (!target) return (0, import_types4.Ok)(void 0);
2310
2399
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2311
2400
  if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
2312
- target.status = terminal;
2401
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2402
+ (0, import_core.setStatus)(roadmap, target, terminal, now.slice(0, 10));
2313
2403
  await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2314
2404
  return (0, import_types4.Ok)(void 0);
2315
2405
  } catch (error) {
@@ -2330,15 +2420,15 @@ var RoadmapTrackerAdapter = class {
2330
2420
  const roadmap = roadmapResult.value;
2331
2421
  const target = this.findFeatureById(roadmap.milestones, issueId);
2332
2422
  if (!target) return (0, import_types4.Ok)(void 0);
2333
- if (target.assignee != null && target.assignee !== orchestratorId) {
2423
+ if (!(0, import_core.isClaimableBy)(target, orchestratorId)) {
2334
2424
  return (0, import_types4.Ok)(void 0);
2335
2425
  }
2336
2426
  if (target.status === "in-progress" && target.assignee === orchestratorId) {
2337
2427
  return (0, import_types4.Ok)(void 0);
2338
2428
  }
2339
- target.status = "in-progress";
2340
- target.assignee = orchestratorId;
2341
- target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2429
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2430
+ (0, import_core.claim)(roadmap, target, orchestratorId, now.slice(0, 10));
2431
+ target.updatedAt = now;
2342
2432
  await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2343
2433
  return (0, import_types4.Ok)(void 0);
2344
2434
  } catch (error) {
@@ -2365,8 +2455,8 @@ var RoadmapTrackerAdapter = class {
2365
2455
  if (this.config.activeStates.includes(target.status) && target.assignee === null) {
2366
2456
  return (0, import_types4.Ok)(void 0);
2367
2457
  }
2368
- target.status = activeState;
2369
- target.assignee = null;
2458
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2459
+ (0, import_core.setStatus)(roadmap, target, activeState, now.slice(0, 10));
2370
2460
  target.updatedAt = null;
2371
2461
  await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2372
2462
  return (0, import_types4.Ok)(void 0);
package/dist/index.mjs CHANGED
@@ -1219,7 +1219,7 @@ var ClaimManager = class {
1219
1219
  // src/core/pr-detector.ts
1220
1220
  import { execFile } from "child_process";
1221
1221
  import { promisify } from "util";
1222
- var PRDetector = class {
1222
+ var PRDetector = class _PRDetector {
1223
1223
  logger;
1224
1224
  execFileFn;
1225
1225
  projectRoot;
@@ -1335,35 +1335,124 @@ var PRDetector = class {
1335
1335
  }
1336
1336
  }
1337
1337
  /**
1338
- * Filters out candidates that already have an open GitHub PR, running
1339
- * checks with limited concurrency to avoid overwhelming the GitHub API.
1340
- * For candidates with an externalId, searches for PRs linked to the
1341
- * GitHub issue. Falls back to `feat/<identifier>` branch lookup otherwise.
1342
- * Fail-open on API errors.
1338
+ * GitHub closing keywords that link a PR to an issue it will close.
1339
+ * @see https://docs.github.com/articles/closing-issues-using-keywords
1340
+ */
1341
+ static CLOSING_REF_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)[\s:]+#(\d+)/gi;
1342
+ /**
1343
+ * Extracts the issue numbers a PR body declares it will close
1344
+ * (e.g. `Closes #42`, `fixes #7`, `Resolves: #99`).
1345
+ */
1346
+ parseClosingIssueNumbers(body) {
1347
+ const nums = [];
1348
+ for (const match of body.matchAll(_PRDetector.CLOSING_REF_RE)) {
1349
+ nums.push(parseInt(match[1], 10));
1350
+ }
1351
+ return nums;
1352
+ }
1353
+ /**
1354
+ * Lists every open PR for a repo in a single `gh pr list` call and returns
1355
+ * the set of issue numbers those PRs close (parsed from their bodies).
1356
+ *
1357
+ * This is the batched replacement for per-issue `gh pr list --search
1358
+ * "closes #N"` queries. Every `gh pr list` form (search or plain list) is
1359
+ * served by GitHub's GraphQL API and consumes from the shared ~5000/hr
1360
+ * GraphQL budget, so issuing one query per candidate per tick exhausted the
1361
+ * limit on busy boards. A single `--state open` list returns every open PR
1362
+ * in one request regardless of candidate count, collapsing N calls into 1.
1363
+ *
1364
+ * Returns `null` if the check itself failed (gh missing, rate limited,
1365
+ * network error) so callers can fail open rather than block real work.
1366
+ */
1367
+ async fetchOpenPRClosures(owner, repo) {
1368
+ try {
1369
+ const exec = promisify(this.execFileFn);
1370
+ const { stdout } = await exec(
1371
+ "gh",
1372
+ [
1373
+ "pr",
1374
+ "list",
1375
+ "--repo",
1376
+ `${owner}/${repo}`,
1377
+ "--state",
1378
+ "open",
1379
+ "--json",
1380
+ "body",
1381
+ "--limit",
1382
+ "200"
1383
+ ],
1384
+ {
1385
+ cwd: this.projectRoot,
1386
+ timeout: 15e3
1387
+ }
1388
+ );
1389
+ const prs = JSON.parse(stdout);
1390
+ const closed = /* @__PURE__ */ new Set();
1391
+ for (const pr of prs) {
1392
+ for (const n of this.parseClosingIssueNumbers(pr.body ?? "")) closed.add(n);
1393
+ }
1394
+ return closed;
1395
+ } catch (err) {
1396
+ this.logger.debug(`Failed to list open PRs for ${owner}/${repo}`, {
1397
+ error: String(err)
1398
+ });
1399
+ return null;
1400
+ }
1401
+ }
1402
+ /**
1403
+ * Filters out candidates that already have an open GitHub PR.
1404
+ *
1405
+ * For GitHub-issue candidates the check is batched: one `gh pr list` per
1406
+ * distinct repo (not one search per issue), parsing closing references
1407
+ * locally. Candidates without a GitHub externalId fall back to a
1408
+ * `feat/<identifier>` branch lookup, throttled to a few concurrent gh calls.
1409
+ * Fail-open on API errors so a flaky/rate-limited GitHub never blocks work.
1343
1410
  */
1344
1411
  async filterCandidatesWithOpenPRs(candidates) {
1412
+ const repos = /* @__PURE__ */ new Map();
1413
+ for (const candidate of candidates) {
1414
+ const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
1415
+ if (parsed) repos.set(`${parsed.owner}/${parsed.repo}`, parsed);
1416
+ }
1417
+ const repoClosures = /* @__PURE__ */ new Map();
1418
+ await Promise.all(
1419
+ [...repos.values()].map(async ({ owner, repo }) => {
1420
+ repoClosures.set(`${owner}/${repo}`, await this.fetchOpenPRClosures(owner, repo));
1421
+ })
1422
+ );
1423
+ const identifierCandidates = candidates.filter(
1424
+ (c) => !(c.externalId && this.parseExternalId(c.externalId))
1425
+ );
1426
+ const identifiersWithOpenPR = /* @__PURE__ */ new Set();
1345
1427
  const concurrency = 3;
1346
- const results = [];
1347
- for (let i = 0; i < candidates.length; i += concurrency) {
1348
- const batch = candidates.slice(i, i + concurrency);
1349
- const batchResults = await Promise.allSettled(
1350
- batch.map(async (candidate) => {
1351
- const hasOpenPR = candidate.externalId ? await this.hasOpenPRForExternalId(candidate.externalId) : await this.hasOpenPRForIdentifier(candidate.identifier);
1352
- return { candidate, hasOpenPR };
1353
- })
1428
+ for (let i = 0; i < identifierCandidates.length; i += concurrency) {
1429
+ const batch = identifierCandidates.slice(i, i + concurrency);
1430
+ const settled = await Promise.allSettled(
1431
+ batch.map(async (c) => ({
1432
+ identifier: c.identifier,
1433
+ hasOpenPR: await this.hasOpenPRForIdentifier(c.identifier)
1434
+ }))
1354
1435
  );
1355
- results.push(...batchResults);
1436
+ for (const result of settled) {
1437
+ if (result.status === "fulfilled" && result.value.hasOpenPR) {
1438
+ identifiersWithOpenPR.add(result.value.identifier);
1439
+ }
1440
+ }
1356
1441
  }
1357
1442
  const filtered = [];
1358
- for (let i = 0; i < results.length; i++) {
1359
- const result = results[i];
1360
- if (!result || result.status === "rejected") {
1361
- filtered.push(candidates[i]);
1362
- continue;
1443
+ for (const candidate of candidates) {
1444
+ const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
1445
+ let hasOpenPR;
1446
+ let via;
1447
+ if (parsed) {
1448
+ const closures = repoClosures.get(`${parsed.owner}/${parsed.repo}`);
1449
+ hasOpenPR = closures ? closures.has(parsed.number) : false;
1450
+ via = `externalId ${candidate.externalId}`;
1451
+ } else {
1452
+ hasOpenPR = identifiersWithOpenPR.has(candidate.identifier);
1453
+ via = `feat/${candidate.identifier}`;
1363
1454
  }
1364
- const { candidate, hasOpenPR } = result.value;
1365
1455
  if (hasOpenPR) {
1366
- const via = candidate.externalId ? `externalId ${candidate.externalId}` : `feat/${candidate.identifier}`;
1367
1456
  this.logger.info(`Skipping ${candidate.title}: open PR exists (${via})`);
1368
1457
  } else {
1369
1458
  filtered.push(candidate);
@@ -2124,7 +2213,10 @@ import * as fs8 from "fs/promises";
2124
2213
  import { createHash as createHash2 } from "crypto";
2125
2214
  import {
2126
2215
  parseRoadmap,
2127
- serializeRoadmap
2216
+ serializeRoadmap,
2217
+ claim as claimFeature,
2218
+ setStatus as setFeatureStatus,
2219
+ isClaimableBy
2128
2220
  } from "@harness-engineering/core";
2129
2221
  import {
2130
2222
  Ok as Ok4,
@@ -2197,7 +2289,8 @@ var RoadmapTrackerAdapter = class {
2197
2289
  if (!target) return Ok4(void 0);
2198
2290
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2199
2291
  if (normalizedTerminal.includes(target.status.toLowerCase())) return Ok4(void 0);
2200
- target.status = terminal;
2292
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2293
+ setFeatureStatus(roadmap, target, terminal, now.slice(0, 10));
2201
2294
  await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2202
2295
  return Ok4(void 0);
2203
2296
  } catch (error) {
@@ -2218,15 +2311,15 @@ var RoadmapTrackerAdapter = class {
2218
2311
  const roadmap = roadmapResult.value;
2219
2312
  const target = this.findFeatureById(roadmap.milestones, issueId);
2220
2313
  if (!target) return Ok4(void 0);
2221
- if (target.assignee != null && target.assignee !== orchestratorId) {
2314
+ if (!isClaimableBy(target, orchestratorId)) {
2222
2315
  return Ok4(void 0);
2223
2316
  }
2224
2317
  if (target.status === "in-progress" && target.assignee === orchestratorId) {
2225
2318
  return Ok4(void 0);
2226
2319
  }
2227
- target.status = "in-progress";
2228
- target.assignee = orchestratorId;
2229
- target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2320
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2321
+ claimFeature(roadmap, target, orchestratorId, now.slice(0, 10));
2322
+ target.updatedAt = now;
2230
2323
  await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2231
2324
  return Ok4(void 0);
2232
2325
  } catch (error) {
@@ -2253,8 +2346,8 @@ var RoadmapTrackerAdapter = class {
2253
2346
  if (this.config.activeStates.includes(target.status) && target.assignee === null) {
2254
2347
  return Ok4(void 0);
2255
2348
  }
2256
- target.status = activeState;
2257
- target.assignee = null;
2349
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2350
+ setFeatureStatus(roadmap, target, activeState, now.slice(0, 10));
2258
2351
  target.updatedAt = null;
2259
2352
  await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2260
2353
  return Ok4(void 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/orchestrator",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Orchestrator daemon for dispatching coding agents to issues",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -48,10 +48,10 @@
48
48
  "ws": "^8.21.0",
49
49
  "yaml": "^2.8.3",
50
50
  "zod": "^3.25.76",
51
- "@harness-engineering/core": "0.30.1",
51
+ "@harness-engineering/core": "0.31.0",
52
52
  "@harness-engineering/graph": "0.11.1",
53
- "@harness-engineering/types": "0.16.1",
54
- "@harness-engineering/intelligence": "0.3.1"
53
+ "@harness-engineering/intelligence": "0.3.1",
54
+ "@harness-engineering/types": "0.16.1"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@asteasolutions/zod-to-openapi": "^7.3.0",