@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 +64 -6
- package/dist/index.d.ts +64 -6
- package/dist/index.js +179 -35
- package/dist/index.mjs +184 -38
- package/package.json +5 -5
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
|
-
*
|
|
300
|
-
*
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
*
|
|
300
|
-
*
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
-
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
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 (
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
2339
|
-
|
|
2340
|
-
target.updatedAt =
|
|
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
|
-
|
|
2368
|
-
target.
|
|
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(
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
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
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
|
|
1347
|
-
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
-
|
|
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 (
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2228
|
-
target.
|
|
2229
|
-
target.updatedAt =
|
|
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
|
-
|
|
2257
|
-
target.
|
|
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:
|
|
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(
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
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.
|
|
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.
|
|
52
|
-
"@harness-engineering/graph": "0.11.
|
|
53
|
-
"@harness-engineering/intelligence": "0.3.
|
|
54
|
-
"@harness-engineering/types": "0.16.
|
|
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",
|