@harness-engineering/orchestrator 0.8.2 → 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
  }
@@ -1099,6 +1126,25 @@ declare class WorkspaceManager {
1099
1126
  * swallowed — dispatch should not be blocked by transient network issues.
1100
1127
  */
1101
1128
  private tryFetch;
1129
+ /**
1130
+ * Default paths seeded into a fresh worktree when {@link WorkspaceConfig.seedPaths}
1131
+ * is unset — the artifacts produced by the brainstorm → orchestrator handoff.
1132
+ */
1133
+ private static readonly DEFAULT_SEED_PATHS;
1134
+ /**
1135
+ * Copies the configured seed paths from the root working tree into a
1136
+ * freshly-created worktree, overlaying the committed checkout.
1137
+ *
1138
+ * A new worktree is based on a committed remote ref, so it does not contain
1139
+ * uncommitted artifacts that exist only in the root working tree (a
1140
+ * just-written proposal under `.harness/proposals/`, a promoted row in
1141
+ * `docs/roadmap.md`). Seeding carries them over so a dispatched agent sees
1142
+ * the same state the orchestrator dispatched from.
1143
+ *
1144
+ * Best-effort by design: a missing source is skipped, and a copy failure is
1145
+ * swallowed — neither must ever block dispatch.
1146
+ */
1147
+ private seedWorkspace;
1102
1148
  /**
1103
1149
  * Resolves the ref that new worktrees should be based on.
1104
1150
  *
@@ -1487,6 +1533,18 @@ declare class BackendRouter {
1487
1533
  private validateReferences;
1488
1534
  }
1489
1535
 
1536
+ /**
1537
+ * Derives the worktree seed paths from a workflow config when
1538
+ * {@link WorkspaceConfig.seedPaths} is not set explicitly.
1539
+ *
1540
+ * A new worktree is checked out from a committed remote ref and therefore lacks
1541
+ * the uncommitted artifacts of the brainstorm → orchestrator handoff. Seeding
1542
+ * carries them over: the proposal directory, plus the roadmap file at its
1543
+ * *configured* location — a roadmap tracker may point `filePath` somewhere
1544
+ * other than the default `docs/roadmap.md`, and the default seed list would
1545
+ * otherwise miss it.
1546
+ */
1547
+ declare function deriveSeedPaths(config: WorkflowConfig): string[];
1490
1548
  /**
1491
1549
  * The central orchestrator that manages the lifecycle of coding agents.
1492
1550
  *
@@ -2817,4 +2875,4 @@ declare function emitProposalCreated(bus: EventEmitter, proposal: SkillProposal)
2817
2875
  declare function emitProposalApproved(bus: EventEmitter, proposal: SkillProposal): void;
2818
2876
  declare function emitProposalRejected(bus: EventEmitter, proposal: SkillProposal): void;
2819
2877
 
2820
- export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BUILT_IN_TASKS, BackendDefSchema, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAX_ATTEMPTS, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, type PersistedOutputEntry, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunOrigin, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, migrateAgentConfig, normalizeFts5Query, normalizeLocalModel, openSearchIndex, promote, reconcile, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, routingWarnings, runGate, savePublishedIndex, searchIndexPath, selectCandidates, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateCustomTasks, validateWorkflowConfig, wireNotificationSinks, wrapAsEnvelope };
2878
+ export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BUILT_IN_TASKS, BackendDefSchema, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAX_ATTEMPTS, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, type PersistedOutputEntry, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunOrigin, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, deriveSeedPaths, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, migrateAgentConfig, normalizeFts5Query, normalizeLocalModel, openSearchIndex, promote, reconcile, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, routingWarnings, runGate, savePublishedIndex, searchIndexPath, selectCandidates, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateCustomTasks, validateWorkflowConfig, wireNotificationSinks, wrapAsEnvelope };
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
  }
@@ -1099,6 +1126,25 @@ declare class WorkspaceManager {
1099
1126
  * swallowed — dispatch should not be blocked by transient network issues.
1100
1127
  */
1101
1128
  private tryFetch;
1129
+ /**
1130
+ * Default paths seeded into a fresh worktree when {@link WorkspaceConfig.seedPaths}
1131
+ * is unset — the artifacts produced by the brainstorm → orchestrator handoff.
1132
+ */
1133
+ private static readonly DEFAULT_SEED_PATHS;
1134
+ /**
1135
+ * Copies the configured seed paths from the root working tree into a
1136
+ * freshly-created worktree, overlaying the committed checkout.
1137
+ *
1138
+ * A new worktree is based on a committed remote ref, so it does not contain
1139
+ * uncommitted artifacts that exist only in the root working tree (a
1140
+ * just-written proposal under `.harness/proposals/`, a promoted row in
1141
+ * `docs/roadmap.md`). Seeding carries them over so a dispatched agent sees
1142
+ * the same state the orchestrator dispatched from.
1143
+ *
1144
+ * Best-effort by design: a missing source is skipped, and a copy failure is
1145
+ * swallowed — neither must ever block dispatch.
1146
+ */
1147
+ private seedWorkspace;
1102
1148
  /**
1103
1149
  * Resolves the ref that new worktrees should be based on.
1104
1150
  *
@@ -1487,6 +1533,18 @@ declare class BackendRouter {
1487
1533
  private validateReferences;
1488
1534
  }
1489
1535
 
1536
+ /**
1537
+ * Derives the worktree seed paths from a workflow config when
1538
+ * {@link WorkspaceConfig.seedPaths} is not set explicitly.
1539
+ *
1540
+ * A new worktree is checked out from a committed remote ref and therefore lacks
1541
+ * the uncommitted artifacts of the brainstorm → orchestrator handoff. Seeding
1542
+ * carries them over: the proposal directory, plus the roadmap file at its
1543
+ * *configured* location — a roadmap tracker may point `filePath` somewhere
1544
+ * other than the default `docs/roadmap.md`, and the default seed list would
1545
+ * otherwise miss it.
1546
+ */
1547
+ declare function deriveSeedPaths(config: WorkflowConfig): string[];
1490
1548
  /**
1491
1549
  * The central orchestrator that manages the lifecycle of coding agents.
1492
1550
  *
@@ -2817,4 +2875,4 @@ declare function emitProposalCreated(bus: EventEmitter, proposal: SkillProposal)
2817
2875
  declare function emitProposalApproved(bus: EventEmitter, proposal: SkillProposal): void;
2818
2876
  declare function emitProposalRejected(bus: EventEmitter, proposal: SkillProposal): void;
2819
2877
 
2820
- export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BUILT_IN_TASKS, BackendDefSchema, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAX_ATTEMPTS, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, type PersistedOutputEntry, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunOrigin, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, migrateAgentConfig, normalizeFts5Query, normalizeLocalModel, openSearchIndex, promote, reconcile, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, routingWarnings, runGate, savePublishedIndex, searchIndexPath, selectCandidates, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateCustomTasks, validateWorkflowConfig, wireNotificationSinks, wrapAsEnvelope };
2878
+ export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BUILT_IN_TASKS, BackendDefSchema, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAX_ATTEMPTS, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, type PersistedOutputEntry, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunOrigin, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, deriveSeedPaths, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, migrateAgentConfig, normalizeFts5Query, normalizeLocalModel, openSearchIndex, promote, reconcile, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, routingWarnings, runGate, savePublishedIndex, searchIndexPath, selectCandidates, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateCustomTasks, validateWorkflowConfig, wireNotificationSinks, wrapAsEnvelope };
package/dist/index.js CHANGED
@@ -73,6 +73,7 @@ __export(index_exports, {
73
73
  createEmptyState: () => createEmptyState,
74
74
  crossFieldRoutingIssues: () => crossFieldRoutingIssues,
75
75
  defaultFetchModels: () => defaultFetchModels,
76
+ deriveSeedPaths: () => deriveSeedPaths,
76
77
  detectScopeTier: () => detectScopeTier,
77
78
  discoverSkillCatalog: () => discoverSkillCatalog,
78
79
  discoverSkillCatalogNames: () => discoverSkillCatalogNames,
@@ -1340,7 +1341,7 @@ var ClaimManager = class {
1340
1341
  // src/core/pr-detector.ts
1341
1342
  var import_node_child_process = require("child_process");
1342
1343
  var import_node_util = require("util");
1343
- var PRDetector = class {
1344
+ var PRDetector = class _PRDetector {
1344
1345
  logger;
1345
1346
  execFileFn;
1346
1347
  projectRoot;
@@ -1456,35 +1457,124 @@ var PRDetector = class {
1456
1457
  }
1457
1458
  }
1458
1459
  /**
1459
- * Filters out candidates that already have an open GitHub PR, running
1460
- * checks with limited concurrency to avoid overwhelming the GitHub API.
1461
- * For candidates with an externalId, searches for PRs linked to the
1462
- * GitHub issue. Falls back to `feat/<identifier>` branch lookup otherwise.
1463
- * 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.
1464
1532
  */
1465
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();
1466
1549
  const concurrency = 3;
1467
- const results = [];
1468
- for (let i = 0; i < candidates.length; i += concurrency) {
1469
- const batch = candidates.slice(i, i + concurrency);
1470
- const batchResults = await Promise.allSettled(
1471
- batch.map(async (candidate) => {
1472
- const hasOpenPR = candidate.externalId ? await this.hasOpenPRForExternalId(candidate.externalId) : await this.hasOpenPRForIdentifier(candidate.identifier);
1473
- return { candidate, hasOpenPR };
1474
- })
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
+ }))
1475
1557
  );
1476
- 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
+ }
1477
1563
  }
1478
1564
  const filtered = [];
1479
- for (let i = 0; i < results.length; i++) {
1480
- const result = results[i];
1481
- if (!result || result.status === "rejected") {
1482
- filtered.push(candidates[i]);
1483
- 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}`;
1484
1576
  }
1485
- const { candidate, hasOpenPR } = result.value;
1486
1577
  if (hasOpenPR) {
1487
- const via = candidate.externalId ? `externalId ${candidate.externalId}` : `feat/${candidate.identifier}`;
1488
1578
  this.logger.info(`Skipping ${candidate.title}: open PR exists (${via})`);
1489
1579
  } else {
1490
1580
  filtered.push(candidate);
@@ -2308,7 +2398,8 @@ var RoadmapTrackerAdapter = class {
2308
2398
  if (!target) return (0, import_types4.Ok)(void 0);
2309
2399
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2310
2400
  if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
2311
- target.status = terminal;
2401
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2402
+ (0, import_core.setStatus)(roadmap, target, terminal, now.slice(0, 10));
2312
2403
  await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2313
2404
  return (0, import_types4.Ok)(void 0);
2314
2405
  } catch (error) {
@@ -2329,15 +2420,15 @@ var RoadmapTrackerAdapter = class {
2329
2420
  const roadmap = roadmapResult.value;
2330
2421
  const target = this.findFeatureById(roadmap.milestones, issueId);
2331
2422
  if (!target) return (0, import_types4.Ok)(void 0);
2332
- if (target.assignee != null && target.assignee !== orchestratorId) {
2423
+ if (!(0, import_core.isClaimableBy)(target, orchestratorId)) {
2333
2424
  return (0, import_types4.Ok)(void 0);
2334
2425
  }
2335
2426
  if (target.status === "in-progress" && target.assignee === orchestratorId) {
2336
2427
  return (0, import_types4.Ok)(void 0);
2337
2428
  }
2338
- target.status = "in-progress";
2339
- target.assignee = orchestratorId;
2340
- 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;
2341
2432
  await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2342
2433
  return (0, import_types4.Ok)(void 0);
2343
2434
  } catch (error) {
@@ -2364,8 +2455,8 @@ var RoadmapTrackerAdapter = class {
2364
2455
  if (this.config.activeStates.includes(target.status) && target.assignee === null) {
2365
2456
  return (0, import_types4.Ok)(void 0);
2366
2457
  }
2367
- target.status = activeState;
2368
- target.assignee = null;
2458
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2459
+ (0, import_core.setStatus)(roadmap, target, activeState, now.slice(0, 10));
2369
2460
  target.updatedAt = null;
2370
2461
  await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2371
2462
  return (0, import_types4.Ok)(void 0);
@@ -2459,7 +2550,7 @@ var path8 = __toESM(require("path"));
2459
2550
  var import_node_child_process2 = require("child_process");
2460
2551
  var import_node_util2 = require("util");
2461
2552
  var import_types6 = require("@harness-engineering/types");
2462
- var WorkspaceManager = class {
2553
+ var WorkspaceManager = class _WorkspaceManager {
2463
2554
  config;
2464
2555
  /** Absolute path to the git repository root (resolved lazily). */
2465
2556
  repoRoot = null;
@@ -2530,6 +2621,7 @@ var WorkspaceManager = class {
2530
2621
  await this.tryFetch(repoRoot);
2531
2622
  const baseRef = await this.resolveBaseRef(repoRoot);
2532
2623
  await this.git(["worktree", "add", "--detach", workspacePath, baseRef], repoRoot);
2624
+ await this.seedWorkspace(workspacePath, repoRoot);
2533
2625
  return (0, import_types6.Ok)(workspacePath);
2534
2626
  } catch (error) {
2535
2627
  return (0, import_types6.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2546,6 +2638,44 @@ var WorkspaceManager = class {
2546
2638
  } catch {
2547
2639
  }
2548
2640
  }
2641
+ /**
2642
+ * Default paths seeded into a fresh worktree when {@link WorkspaceConfig.seedPaths}
2643
+ * is unset — the artifacts produced by the brainstorm → orchestrator handoff.
2644
+ */
2645
+ static DEFAULT_SEED_PATHS = [".harness/proposals", "docs/roadmap.md"];
2646
+ /**
2647
+ * Copies the configured seed paths from the root working tree into a
2648
+ * freshly-created worktree, overlaying the committed checkout.
2649
+ *
2650
+ * A new worktree is based on a committed remote ref, so it does not contain
2651
+ * uncommitted artifacts that exist only in the root working tree (a
2652
+ * just-written proposal under `.harness/proposals/`, a promoted row in
2653
+ * `docs/roadmap.md`). Seeding carries them over so a dispatched agent sees
2654
+ * the same state the orchestrator dispatched from.
2655
+ *
2656
+ * Best-effort by design: a missing source is skipped, and a copy failure is
2657
+ * swallowed — neither must ever block dispatch.
2658
+ */
2659
+ async seedWorkspace(workspacePath, repoRoot) {
2660
+ const seedPaths = this.config.seedPaths ?? _WorkspaceManager.DEFAULT_SEED_PATHS;
2661
+ for (const entry of seedPaths) {
2662
+ const rel = path8.isAbsolute(entry) ? path8.relative(repoRoot, entry).replaceAll("\\", "/") : entry;
2663
+ if (!rel || rel === ".." || rel.startsWith("../") || path8.isAbsolute(rel)) {
2664
+ continue;
2665
+ }
2666
+ const src = path8.join(repoRoot, rel);
2667
+ try {
2668
+ await fs9.access(src);
2669
+ } catch {
2670
+ continue;
2671
+ }
2672
+ const dest = path8.join(workspacePath, rel);
2673
+ try {
2674
+ await fs9.cp(src, dest, { recursive: true, force: true });
2675
+ } catch {
2676
+ }
2677
+ }
2678
+ }
2549
2679
  /**
2550
2680
  * Resolves the ref that new worktrees should be based on.
2551
2681
  *
@@ -12440,6 +12570,10 @@ function reportCycle(path24, next, errors, reported) {
12440
12570
  }
12441
12571
 
12442
12572
  // src/orchestrator.ts
12573
+ function deriveSeedPaths(config) {
12574
+ const roadmapPath = config.tracker.kind === "roadmap" && config.tracker.filePath ? config.tracker.filePath : "docs/roadmap.md";
12575
+ return [".harness/proposals", roadmapPath];
12576
+ }
12443
12577
  var Orchestrator = class extends import_node_events.EventEmitter {
12444
12578
  state;
12445
12579
  config;
@@ -12603,12 +12737,21 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12603
12737
  );
12604
12738
  }
12605
12739
  this.tracker = overrides?.tracker || this.createTracker();
12606
- this.workspace = new WorkspaceManager(config.workspace, {
12607
- emitEvent: (event) => {
12608
- this.server?.broadcastMaintenance("maintenance:baseref_fallback", event);
12609
- this.emit("maintenance:baseref_fallback", event);
12740
+ this.workspace = new WorkspaceManager(
12741
+ {
12742
+ ...config.workspace,
12743
+ // Seed the brainstorm handoff artifacts into each fresh worktree. An
12744
+ // explicit `seedPaths` wins; otherwise derive from the tracker config
12745
+ // so a non-default roadmap location is still carried over.
12746
+ seedPaths: config.workspace.seedPaths ?? deriveSeedPaths(config)
12747
+ },
12748
+ {
12749
+ emitEvent: (event) => {
12750
+ this.server?.broadcastMaintenance("maintenance:baseref_fallback", event);
12751
+ this.emit("maintenance:baseref_fallback", event);
12752
+ }
12610
12753
  }
12611
- });
12754
+ );
12612
12755
  this.hooks = new WorkspaceHooks(config.hooks);
12613
12756
  this.renderer = new PromptRenderer();
12614
12757
  this.overrideBackend = overrides?.backend ?? null;
@@ -14770,6 +14913,7 @@ function buildArchiveHooks(opts) {
14770
14913
  createEmptyState,
14771
14914
  crossFieldRoutingIssues,
14772
14915
  defaultFetchModels,
14916
+ deriveSeedPaths,
14773
14917
  detectScopeTier,
14774
14918
  discoverSkillCatalog,
14775
14919
  discoverSkillCatalogNames,
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);
@@ -2348,7 +2441,7 @@ import * as path8 from "path";
2348
2441
  import { execFile as execFile2 } from "child_process";
2349
2442
  import { promisify as promisify2 } from "util";
2350
2443
  import { Ok as Ok6, Err as Err4 } from "@harness-engineering/types";
2351
- var WorkspaceManager = class {
2444
+ var WorkspaceManager = class _WorkspaceManager {
2352
2445
  config;
2353
2446
  /** Absolute path to the git repository root (resolved lazily). */
2354
2447
  repoRoot = null;
@@ -2419,6 +2512,7 @@ var WorkspaceManager = class {
2419
2512
  await this.tryFetch(repoRoot);
2420
2513
  const baseRef = await this.resolveBaseRef(repoRoot);
2421
2514
  await this.git(["worktree", "add", "--detach", workspacePath, baseRef], repoRoot);
2515
+ await this.seedWorkspace(workspacePath, repoRoot);
2422
2516
  return Ok6(workspacePath);
2423
2517
  } catch (error) {
2424
2518
  return Err4(error instanceof Error ? error : new Error(String(error)));
@@ -2435,6 +2529,44 @@ var WorkspaceManager = class {
2435
2529
  } catch {
2436
2530
  }
2437
2531
  }
2532
+ /**
2533
+ * Default paths seeded into a fresh worktree when {@link WorkspaceConfig.seedPaths}
2534
+ * is unset — the artifacts produced by the brainstorm → orchestrator handoff.
2535
+ */
2536
+ static DEFAULT_SEED_PATHS = [".harness/proposals", "docs/roadmap.md"];
2537
+ /**
2538
+ * Copies the configured seed paths from the root working tree into a
2539
+ * freshly-created worktree, overlaying the committed checkout.
2540
+ *
2541
+ * A new worktree is based on a committed remote ref, so it does not contain
2542
+ * uncommitted artifacts that exist only in the root working tree (a
2543
+ * just-written proposal under `.harness/proposals/`, a promoted row in
2544
+ * `docs/roadmap.md`). Seeding carries them over so a dispatched agent sees
2545
+ * the same state the orchestrator dispatched from.
2546
+ *
2547
+ * Best-effort by design: a missing source is skipped, and a copy failure is
2548
+ * swallowed — neither must ever block dispatch.
2549
+ */
2550
+ async seedWorkspace(workspacePath, repoRoot) {
2551
+ const seedPaths = this.config.seedPaths ?? _WorkspaceManager.DEFAULT_SEED_PATHS;
2552
+ for (const entry of seedPaths) {
2553
+ const rel = path8.isAbsolute(entry) ? path8.relative(repoRoot, entry).replaceAll("\\", "/") : entry;
2554
+ if (!rel || rel === ".." || rel.startsWith("../") || path8.isAbsolute(rel)) {
2555
+ continue;
2556
+ }
2557
+ const src = path8.join(repoRoot, rel);
2558
+ try {
2559
+ await fs9.access(src);
2560
+ } catch {
2561
+ continue;
2562
+ }
2563
+ const dest = path8.join(workspacePath, rel);
2564
+ try {
2565
+ await fs9.cp(src, dest, { recursive: true, force: true });
2566
+ } catch {
2567
+ }
2568
+ }
2569
+ }
2438
2570
  /**
2439
2571
  * Resolves the ref that new worktrees should be based on.
2440
2572
  *
@@ -10738,7 +10870,7 @@ var StructuredLogger = class {
10738
10870
 
10739
10871
  // src/workspace/config-scanner.ts
10740
10872
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
10741
- import { join as join15, relative } from "path";
10873
+ import { join as join15, relative as relative2 } from "path";
10742
10874
  import {
10743
10875
  scanForInjection,
10744
10876
  SecurityScanner,
@@ -10775,7 +10907,7 @@ async function scanSingleFile(filePath, targetDir, scanner) {
10775
10907
  findings.push(...mapSecurityFindings(secFindings, findings));
10776
10908
  const adjusted = adjustFindingSeverity(findings);
10777
10909
  return {
10778
- file: relative(targetDir, filePath).replaceAll("\\", "/"),
10910
+ file: relative2(targetDir, filePath).replaceAll("\\", "/"),
10779
10911
  findings: adjusted,
10780
10912
  overallSeverity: computeOverallSeverity(adjusted)
10781
10913
  };
@@ -12411,6 +12543,10 @@ function reportCycle(path24, next, errors, reported) {
12411
12543
  }
12412
12544
 
12413
12545
  // src/orchestrator.ts
12546
+ function deriveSeedPaths(config) {
12547
+ const roadmapPath = config.tracker.kind === "roadmap" && config.tracker.filePath ? config.tracker.filePath : "docs/roadmap.md";
12548
+ return [".harness/proposals", roadmapPath];
12549
+ }
12414
12550
  var Orchestrator = class extends EventEmitter {
12415
12551
  state;
12416
12552
  config;
@@ -12574,12 +12710,21 @@ var Orchestrator = class extends EventEmitter {
12574
12710
  );
12575
12711
  }
12576
12712
  this.tracker = overrides?.tracker || this.createTracker();
12577
- this.workspace = new WorkspaceManager(config.workspace, {
12578
- emitEvent: (event) => {
12579
- this.server?.broadcastMaintenance("maintenance:baseref_fallback", event);
12580
- this.emit("maintenance:baseref_fallback", event);
12713
+ this.workspace = new WorkspaceManager(
12714
+ {
12715
+ ...config.workspace,
12716
+ // Seed the brainstorm handoff artifacts into each fresh worktree. An
12717
+ // explicit `seedPaths` wins; otherwise derive from the tracker config
12718
+ // so a non-default roadmap location is still carried over.
12719
+ seedPaths: config.workspace.seedPaths ?? deriveSeedPaths(config)
12720
+ },
12721
+ {
12722
+ emitEvent: (event) => {
12723
+ this.server?.broadcastMaintenance("maintenance:baseref_fallback", event);
12724
+ this.emit("maintenance:baseref_fallback", event);
12725
+ }
12581
12726
  }
12582
- });
12727
+ );
12583
12728
  this.hooks = new WorkspaceHooks(config.hooks);
12584
12729
  this.renderer = new PromptRenderer();
12585
12730
  this.overrideBackend = overrides?.backend ?? null;
@@ -14742,6 +14887,7 @@ export {
14742
14887
  createEmptyState,
14743
14888
  crossFieldRoutingIssues,
14744
14889
  defaultFetchModels,
14890
+ deriveSeedPaths,
14745
14891
  detectScopeTier,
14746
14892
  discoverSkillCatalog,
14747
14893
  discoverSkillCatalogNames,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/orchestrator",
3
- "version": "0.8.2",
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.0",
52
- "@harness-engineering/graph": "0.11.0",
53
- "@harness-engineering/intelligence": "0.3.0",
54
- "@harness-engineering/types": "0.16.0"
51
+ "@harness-engineering/core": "0.31.0",
52
+ "@harness-engineering/graph": "0.11.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",