@pellux/goodvibes-sdk 0.37.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/artifacts/operator-contract.json +1 -1
- package/dist/events/communication.d.ts +21 -1
- package/dist/events/communication.d.ts.map +1 -1
- package/dist/platform/agents/message-bus-core.d.ts +7 -0
- package/dist/platform/agents/message-bus-core.d.ts.map +1 -1
- package/dist/platform/agents/message-bus-core.js +2 -1
- package/dist/platform/agents/orchestrator-runner.d.ts +37 -1
- package/dist/platform/agents/orchestrator-runner.d.ts.map +1 -1
- package/dist/platform/agents/orchestrator-runner.js +158 -7
- package/dist/platform/agents/orchestrator.d.ts +40 -0
- package/dist/platform/agents/orchestrator.d.ts.map +1 -1
- package/dist/platform/agents/orchestrator.js +29 -0
- package/dist/platform/agents/turn-knowledge-injection.d.ts +107 -0
- package/dist/platform/agents/turn-knowledge-injection.d.ts.map +1 -0
- package/dist/platform/agents/turn-knowledge-injection.js +152 -0
- package/dist/platform/agents/wrfc-controller.d.ts +23 -0
- package/dist/platform/agents/wrfc-controller.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-controller.js +55 -0
- package/dist/platform/config/manager.d.ts +9 -0
- package/dist/platform/config/manager.d.ts.map +1 -1
- package/dist/platform/config/manager.js +15 -0
- package/dist/platform/control-plane/approval-broker.d.ts +1 -0
- package/dist/platform/control-plane/approval-broker.d.ts.map +1 -1
- package/dist/platform/control-plane/approval-broker.js +2 -0
- package/dist/platform/core/adaptive-planner.d.ts +50 -0
- package/dist/platform/core/adaptive-planner.d.ts.map +1 -1
- package/dist/platform/core/adaptive-planner.js +49 -0
- package/dist/platform/core/index.d.ts +1 -0
- package/dist/platform/core/index.d.ts.map +1 -1
- package/dist/platform/core/index.js +1 -0
- package/dist/platform/core/orchestrator-runtime.d.ts +11 -0
- package/dist/platform/core/orchestrator-runtime.d.ts.map +1 -1
- package/dist/platform/core/orchestrator-turn-loop.d.ts +28 -1
- package/dist/platform/core/orchestrator-turn-loop.d.ts.map +1 -1
- package/dist/platform/core/orchestrator-turn-loop.js +115 -3
- package/dist/platform/core/orchestrator.d.ts +61 -0
- package/dist/platform/core/orchestrator.d.ts.map +1 -1
- package/dist/platform/core/orchestrator.js +74 -1
- package/dist/platform/core/plan-proposal.d.ts +165 -0
- package/dist/platform/core/plan-proposal.d.ts.map +1 -0
- package/dist/platform/core/plan-proposal.js +296 -0
- package/dist/platform/knowledge/knowledge-api.d.ts +26 -0
- package/dist/platform/knowledge/knowledge-api.d.ts.map +1 -1
- package/dist/platform/knowledge/knowledge-api.js +11 -0
- package/dist/platform/orchestration/budget.d.ts +22 -0
- package/dist/platform/orchestration/budget.d.ts.map +1 -0
- package/dist/platform/orchestration/budget.js +35 -0
- package/dist/platform/orchestration/cancellation.d.ts +31 -0
- package/dist/platform/orchestration/cancellation.d.ts.map +1 -0
- package/dist/platform/orchestration/cancellation.js +25 -0
- package/dist/platform/orchestration/controller-compat.d.ts +32 -0
- package/dist/platform/orchestration/controller-compat.d.ts.map +1 -0
- package/dist/platform/orchestration/controller-compat.js +13 -0
- package/dist/platform/orchestration/dirty-guard.d.ts +32 -0
- package/dist/platform/orchestration/dirty-guard.d.ts.map +1 -0
- package/dist/platform/orchestration/dirty-guard.js +147 -0
- package/dist/platform/orchestration/engine.d.ts +57 -0
- package/dist/platform/orchestration/engine.d.ts.map +1 -0
- package/dist/platform/orchestration/engine.js +418 -0
- package/dist/platform/orchestration/index.d.ts +18 -0
- package/dist/platform/orchestration/index.d.ts.map +1 -0
- package/dist/platform/orchestration/index.js +10 -0
- package/dist/platform/orchestration/persistence.d.ts +24 -0
- package/dist/platform/orchestration/persistence.d.ts.map +1 -0
- package/dist/platform/orchestration/persistence.js +176 -0
- package/dist/platform/orchestration/phase-runner.d.ts +87 -0
- package/dist/platform/orchestration/phase-runner.d.ts.map +1 -0
- package/dist/platform/orchestration/phase-runner.js +274 -0
- package/dist/platform/orchestration/scheduler.d.ts +45 -0
- package/dist/platform/orchestration/scheduler.d.ts.map +1 -0
- package/dist/platform/orchestration/scheduler.js +57 -0
- package/dist/platform/orchestration/types.d.ts +250 -0
- package/dist/platform/orchestration/types.d.ts.map +1 -0
- package/dist/platform/orchestration/types.js +15 -0
- package/dist/platform/runtime/emitters/communication.d.ts +11 -0
- package/dist/platform/runtime/emitters/communication.d.ts.map +1 -1
- package/dist/platform/runtime/emitters/communication.js +9 -0
- package/dist/platform/runtime/feature-flags/flags.d.ts.map +1 -1
- package/dist/platform/runtime/feature-flags/flags.js +17 -0
- package/dist/platform/runtime/fleet/adapters/agent.d.ts +56 -0
- package/dist/platform/runtime/fleet/adapters/agent.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/agent.js +143 -0
- package/dist/platform/runtime/fleet/adapters/automation.d.ts +30 -0
- package/dist/platform/runtime/fleet/adapters/automation.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/automation.js +41 -0
- package/dist/platform/runtime/fleet/adapters/background-process.d.ts +12 -0
- package/dist/platform/runtime/fleet/adapters/background-process.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/background-process.js +46 -0
- package/dist/platform/runtime/fleet/adapters/code-index.d.ts +21 -0
- package/dist/platform/runtime/fleet/adapters/code-index.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/code-index.js +44 -0
- package/dist/platform/runtime/fleet/adapters/orchestration.d.ts +47 -0
- package/dist/platform/runtime/fleet/adapters/orchestration.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/orchestration.js +225 -0
- package/dist/platform/runtime/fleet/adapters/schedule.d.ts +14 -0
- package/dist/platform/runtime/fleet/adapters/schedule.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/schedule.js +28 -0
- package/dist/platform/runtime/fleet/adapters/trigger.d.ts +12 -0
- package/dist/platform/runtime/fleet/adapters/trigger.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/trigger.js +22 -0
- package/dist/platform/runtime/fleet/adapters/watcher.d.ts +13 -0
- package/dist/platform/runtime/fleet/adapters/watcher.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/watcher.js +47 -0
- package/dist/platform/runtime/fleet/adapters/workflow.d.ts +11 -0
- package/dist/platform/runtime/fleet/adapters/workflow.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/workflow.js +34 -0
- package/dist/platform/runtime/fleet/adapters/wrfc.d.ts +22 -0
- package/dist/platform/runtime/fleet/adapters/wrfc.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/adapters/wrfc.js +228 -0
- package/dist/platform/runtime/fleet/index.d.ts +10 -0
- package/dist/platform/runtime/fleet/index.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/index.js +6 -0
- package/dist/platform/runtime/fleet/registry.d.ts +113 -0
- package/dist/platform/runtime/fleet/registry.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/registry.js +630 -0
- package/dist/platform/runtime/fleet/types.d.ts +199 -0
- package/dist/platform/runtime/fleet/types.d.ts.map +1 -0
- package/dist/platform/runtime/fleet/types.js +2 -0
- package/dist/platform/runtime/runtime-knowledge-api.d.ts +1 -1
- package/dist/platform/runtime/runtime-knowledge-api.d.ts.map +1 -1
- package/dist/platform/runtime/runtime-knowledge-api.js +1 -0
- package/dist/platform/runtime/services.d.ts +46 -0
- package/dist/platform/runtime/services.d.ts.map +1 -1
- package/dist/platform/runtime/services.js +78 -0
- package/dist/platform/state/code-index-chunking.d.ts +49 -0
- package/dist/platform/state/code-index-chunking.d.ts.map +1 -0
- package/dist/platform/state/code-index-chunking.js +111 -0
- package/dist/platform/state/code-index-db.d.ts +53 -0
- package/dist/platform/state/code-index-db.d.ts.map +1 -0
- package/dist/platform/state/code-index-db.js +101 -0
- package/dist/platform/state/code-index-store.d.ts +175 -0
- package/dist/platform/state/code-index-store.d.ts.map +1 -0
- package/dist/platform/state/code-index-store.js +612 -0
- package/dist/platform/state/index.d.ts +4 -2
- package/dist/platform/state/index.d.ts.map +1 -1
- package/dist/platform/state/index.js +2 -1
- package/dist/platform/state/knowledge-injection.d.ts +23 -0
- package/dist/platform/state/knowledge-injection.d.ts.map +1 -1
- package/dist/platform/state/knowledge-injection.js +34 -19
- package/dist/platform/state/memory-vector-store.d.ts +1 -13
- package/dist/platform/state/memory-vector-store.d.ts.map +1 -1
- package/dist/platform/state/memory-vector-store.js +6 -38
- package/dist/platform/state/sqlite-vec-loader.d.ts +25 -0
- package/dist/platform/state/sqlite-vec-loader.d.ts.map +1 -0
- package/dist/platform/state/sqlite-vec-loader.js +43 -0
- package/dist/platform/tools/agent/manager.d.ts +128 -1
- package/dist/platform/tools/agent/manager.d.ts.map +1 -1
- package/dist/platform/tools/agent/manager.js +137 -1
- package/dist/platform/tools/exec/runtime.d.ts.map +1 -1
- package/dist/platform/tools/exec/runtime.js +139 -44
- package/dist/platform/tools/exec/schema.d.ts +2 -0
- package/dist/platform/tools/exec/schema.d.ts.map +1 -1
- package/dist/platform/tools/fetch/runtime.d.ts +7 -0
- package/dist/platform/tools/fetch/runtime.d.ts.map +1 -1
- package/dist/platform/tools/fetch/runtime.js +8 -6
- package/dist/platform/tools/registry.d.ts +9 -3
- package/dist/platform/tools/registry.d.ts.map +1 -1
- package/dist/platform/tools/registry.js +9 -3
- package/dist/platform/types/tools.d.ts +14 -1
- package/dist/platform/types/tools.d.ts.map +1 -1
- package/dist/platform/version.js +1 -1
- package/dist/platform/workspace/checkpoint/side-git.d.ts +20 -8
- package/dist/platform/workspace/checkpoint/side-git.d.ts.map +1 -1
- package/dist/platform/workspace/checkpoint/side-git.js +21 -11
- package/package.json +17 -9
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** SDK-owned platform module. This implementation is maintained in goodvibes-sdk. */
|
|
2
|
+
/**
|
|
3
|
+
* Phase-runner (Wave 4, wo701) — runs one WorkItem through one Phase: spawn
|
|
4
|
+
* agent, await completion, verify claims, run gates, commit, cleanup.
|
|
5
|
+
*
|
|
6
|
+
* REUSES the hardened WRFC primitives verbatim (same functions WrfcController
|
|
7
|
+
* itself calls, so behavior can't fork): verifyEngineerClaims
|
|
8
|
+
* (wrfc-reporting.ts) for the phantom-work guard, runWrfcGateChecks
|
|
9
|
+
* (wrfc-gate-runtime.ts) for quality gates, AgentWorktree.commitWorkingTree
|
|
10
|
+
* for scoped commits, and the transport-retry / WrfcChainFailureKind pattern
|
|
11
|
+
* (isTransportFailureMessage + getWrfcTransportRetryLimit/DelayMs) for
|
|
12
|
+
* bounded respawn-on-transport-blip.
|
|
13
|
+
*
|
|
14
|
+
* REALITY-WINS DIVERGENCE from the brief's design (c): WrfcController itself
|
|
15
|
+
* (wrfc-controller.ts, verified) never calls AgentWorktree.create() for its
|
|
16
|
+
* role agents — engineer/reviewer/fixer/integrator all run in the SAME
|
|
17
|
+
* shared `projectRoot` working directory; AgentWorktree is used ONLY for its
|
|
18
|
+
* commitWorkingTree/merge/cleanup surface (merge/cleanup are safe no-ops
|
|
19
|
+
* when no isolated worktree dir exists, which is always, today). There is no
|
|
20
|
+
* per-agent `workingDirectory` override anywhere in AgentInput /
|
|
21
|
+
* AgentOrchestratorRunContext.createRunContext() (verified: the latter is
|
|
22
|
+
* fixed per AgentOrchestrator instance, not per-spawn), so a spawned agent
|
|
23
|
+
* cannot actually be pointed at an isolated worktree directory without new
|
|
24
|
+
* cross-cutting plumbing through AgentManager/AgentOrchestrator construction
|
|
25
|
+
* — well beyond this module's boundary, and not something WrfcController
|
|
26
|
+
* itself has either. This module therefore mirrors WrfcController's ACTUAL
|
|
27
|
+
* (shared-directory) behavior rather than the brief's aspirational
|
|
28
|
+
* per-item-isolated-worktree fan-out; true fan-out isolation is a valuable,
|
|
29
|
+
* separately-scoped follow-up (see the work-order report).
|
|
30
|
+
*
|
|
31
|
+
* SECOND REALITY-WINS DIVERGENCE: AgentManager.spawn()'s root-spawn
|
|
32
|
+
* normalization (tools/agent/wrfc-batch-policy.ts isRootReviewRoleTask) force
|
|
33
|
+
* -rewrites any PARENTLESS spawn whose template is literally
|
|
34
|
+
* reviewer/tester/verifier/qa/review/test, OR whose task text matches
|
|
35
|
+
* ROLE_ACTION_RE/ROLE_PREFIX_RE (e.g. "review the diff"), into an
|
|
36
|
+
* 'engineer'-templated WRFC-owner chain with `dangerously_disable_wrfc`
|
|
37
|
+
* forced back to `false` — REGARDLESS of what this module passes in.
|
|
38
|
+
* Phase-runner spawns are always parentless (a workstream has no owning
|
|
39
|
+
* AgentRecord), so it must dodge that heuristic by construction: never
|
|
40
|
+
* literally template review/test-flavored phases as one of those role
|
|
41
|
+
* strings (use 'general' instead — see templateForPhase), and phrase
|
|
42
|
+
* review-phase prompts with "assess/evaluate" rather than "review/test/
|
|
43
|
+
* verify" (see buildPhaseTask). This is load-bearing: changing this wording
|
|
44
|
+
* without checking wrfc-batch-policy.ts's regexes again risks silently
|
|
45
|
+
* re-activating the WRFC hijack for review-kind phases.
|
|
46
|
+
*/
|
|
47
|
+
import type { AgentManager } from '../tools/agent/manager.js';
|
|
48
|
+
import type { ConfigManager } from '../config/manager.js';
|
|
49
|
+
import type { RuntimeEventBus } from '../runtime/events/index.js';
|
|
50
|
+
import type { CancellationRegistry } from './cancellation.js';
|
|
51
|
+
import { type DirtyLaunchSnapshot } from './dirty-guard.js';
|
|
52
|
+
import type { Phase, PhaseResult, WorkItem, WorkItemUsage, Workstream } from './types.js';
|
|
53
|
+
/** Narrow structural pick — testable with stubs, mirrors AgentManagerLike (wrfc-config.ts). */
|
|
54
|
+
export type PhaseRunnerAgentManagerLike = Pick<AgentManager, 'spawn' | 'getStatus' | 'cancel' | 'registerCancellationSignal' | 'releaseCancellationSignal'>;
|
|
55
|
+
/** Structural pick of AgentWorktree's surface — matches WrfcController's WrfcWorktreeOps injection seam exactly, so the same test doubles work for both. */
|
|
56
|
+
export interface WrfcWorktreeOps {
|
|
57
|
+
merge(agentId: string): Promise<boolean>;
|
|
58
|
+
cleanup(agentId: string): Promise<void>;
|
|
59
|
+
commitWorkingTree(message: string, paths?: string[]): Promise<string | null>;
|
|
60
|
+
currentHead(): Promise<string | null>;
|
|
61
|
+
}
|
|
62
|
+
export interface PhaseRunnerDeps {
|
|
63
|
+
readonly agentManager: PhaseRunnerAgentManagerLike;
|
|
64
|
+
readonly configManager: Pick<ConfigManager, 'get' | 'getCategory'>;
|
|
65
|
+
readonly runtimeBus: RuntimeEventBus;
|
|
66
|
+
readonly projectRoot: string;
|
|
67
|
+
readonly sessionId: string;
|
|
68
|
+
readonly createWorktree?: (() => WrfcWorktreeOps) | undefined;
|
|
69
|
+
readonly cancellation: CancellationRegistry;
|
|
70
|
+
readonly priceUsage?: ((model: string | undefined, usage: WorkItemUsage) => number | null) | undefined;
|
|
71
|
+
readonly skipClaimVerification?: boolean | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The dirty-tree snapshot taken synchronously at engine launch (Wave 6,
|
|
74
|
+
* wo-F item 4 — see dirty-guard.ts). Absent (undefined) degrades to
|
|
75
|
+
* today's behavior: no exclusion, every candidate path is committed.
|
|
76
|
+
*/
|
|
77
|
+
readonly launchDirtySnapshot?: DirtyLaunchSnapshot | undefined;
|
|
78
|
+
}
|
|
79
|
+
export interface PhaseRunOutcome {
|
|
80
|
+
readonly result: PhaseResult;
|
|
81
|
+
readonly agentStatus: 'completed' | 'failed' | 'cancelled';
|
|
82
|
+
}
|
|
83
|
+
/** Combines a new phase's usage into a work item's running total. Single-source cost (never independently re-priced here). */
|
|
84
|
+
export declare function mergeUsage(a: WorkItemUsage, b: WorkItemUsage): WorkItemUsage;
|
|
85
|
+
/** Runs one WorkItem through one Phase to completion (or cancellation/failure). Recurses (bounded by transportRetryLimit) on a transport-classified spawn failure. */
|
|
86
|
+
export declare function runPhase(workstream: Workstream, item: WorkItem, phase: Phase, priorReports: readonly PhaseResult[], deps: PhaseRunnerDeps): Promise<PhaseRunOutcome>;
|
|
87
|
+
//# sourceMappingURL=phase-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phase-runner.d.ts","sourceRoot":"","sources":["../../../src/platform/orchestration/phase-runner.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,2BAA2B,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAelE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAiC,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC3F,OAAO,KAAK,EAAgC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExH,+FAA+F;AAC/F,MAAM,MAAM,2BAA2B,GAAG,IAAI,CAC5C,YAAY,EACZ,OAAO,GAAG,WAAW,GAAG,QAAQ,GAAG,4BAA4B,GAAG,2BAA2B,CAC9F,CAAC;AAEF,4JAA4J;AAC5J,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7E,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,YAAY,EAAE,2BAA2B,CAAC;IACnD,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,GAAG,aAAa,CAAC,CAAC;IACnE,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,MAAM,eAAe,CAAC,GAAG,SAAS,CAAC;IAC9D,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IACvG,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACrD;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CAChE;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;CAC5D;AA8ED,8HAA8H;AAC9H,wBAAgB,UAAU,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,GAAG,aAAa,CAgB5E;AAyGD,sKAAsK;AACtK,wBAAsB,QAAQ,CAC5B,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,SAAS,WAAW,EAAE,EACpC,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,eAAe,CAAC,CAuG1B"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/** SDK-owned platform module. This implementation is maintained in goodvibes-sdk. */
|
|
2
|
+
import { AgentWorktree } from '../agents/worktree.js';
|
|
3
|
+
import { parseCompletionReport, } from '../agents/completion-report.js';
|
|
4
|
+
import { verifyEngineerClaims } from '../agents/wrfc-reporting.js';
|
|
5
|
+
import { runWrfcGateChecks } from '../agents/wrfc-gate-runtime.js';
|
|
6
|
+
import { getWrfcTransportRetryDelayMs, getWrfcTransportRetryLimit } from '../agents/wrfc-config.js';
|
|
7
|
+
import { isTransportFailureMessage } from '../types/errors.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { summarizeError } from '../utils/error-display.js';
|
|
10
|
+
import { excludeUntouchedLaunchResidue } from './dirty-guard.js';
|
|
11
|
+
function templateForPhase(phase) {
|
|
12
|
+
return phase.kind === 'review' || phase.kind === 'gate' ? 'general' : 'engineer';
|
|
13
|
+
}
|
|
14
|
+
function buildPhaseTask(item, phase, priorReports) {
|
|
15
|
+
const priorContext = priorReports.length > 0
|
|
16
|
+
? `\n\nPrior phase reports for this work item:\n${priorReports.map((r) => `- ${r.phaseId}: ${r.report.summary}`).join('\n')}`
|
|
17
|
+
: '';
|
|
18
|
+
if (phase.kind === 'review' || phase.kind === 'gate') {
|
|
19
|
+
return `Assess the following work item's changes against its constraints and report findings. Do not modify files.\n\nWork item: ${item.title}\n${item.task}${priorContext}`;
|
|
20
|
+
}
|
|
21
|
+
if (phase.kind === 'fix') {
|
|
22
|
+
return `Address the following findings for this work item.\n\nWork item: ${item.title}\n${item.task}${priorContext}`;
|
|
23
|
+
}
|
|
24
|
+
return `${item.task}${priorContext}`;
|
|
25
|
+
}
|
|
26
|
+
function genericReport(summary) {
|
|
27
|
+
return { version: 1, archetype: 'generic', summary, result: summary };
|
|
28
|
+
}
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const timer = setTimeout(resolve, ms);
|
|
32
|
+
timer.unref?.();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function awaitAgentTermination(runtimeBus, agentManager, agentId) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const unsubscribe = runtimeBus.onDomain('agents', (envelope) => {
|
|
38
|
+
const event = envelope.payload;
|
|
39
|
+
if (event.agentId !== agentId)
|
|
40
|
+
return;
|
|
41
|
+
if (event.type !== 'AGENT_COMPLETED' && event.type !== 'AGENT_FAILED' && event.type !== 'AGENT_CANCELLED')
|
|
42
|
+
return;
|
|
43
|
+
unsubscribe();
|
|
44
|
+
const status = event.type === 'AGENT_COMPLETED' ? 'completed' : event.type === 'AGENT_CANCELLED' ? 'cancelled' : 'failed';
|
|
45
|
+
resolve({ status, record: agentManager.getStatus(agentId) });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function usageFromRecord(record, priceUsage) {
|
|
50
|
+
const u = record?.usage;
|
|
51
|
+
const base = {
|
|
52
|
+
inputTokens: u?.inputTokens ?? 0,
|
|
53
|
+
outputTokens: u?.outputTokens ?? 0,
|
|
54
|
+
cacheReadTokens: u?.cacheReadTokens ?? 0,
|
|
55
|
+
cacheWriteTokens: u?.cacheWriteTokens ?? 0,
|
|
56
|
+
reasoningTokens: u?.reasoningTokens,
|
|
57
|
+
llmCallCount: u?.llmCallCount ?? 0,
|
|
58
|
+
turnCount: u?.turnCount ?? 0,
|
|
59
|
+
toolCallCount: record?.toolCallCount ?? 0,
|
|
60
|
+
};
|
|
61
|
+
let costUsd = null;
|
|
62
|
+
let costState = 'unpriced';
|
|
63
|
+
if (u && priceUsage) {
|
|
64
|
+
try {
|
|
65
|
+
const priced = priceUsage(record?.model, { ...base, costUsd: null, costState: 'unpriced' });
|
|
66
|
+
if (priced !== null) {
|
|
67
|
+
costUsd = priced;
|
|
68
|
+
costState = 'priced';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// stays unpriced — never fabricate a cost from a throwing pricer.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { ...base, costUsd, costState };
|
|
76
|
+
}
|
|
77
|
+
/** Combines a new phase's usage into a work item's running total. Single-source cost (never independently re-priced here). */
|
|
78
|
+
export function mergeUsage(a, b) {
|
|
79
|
+
const sawReasoning = a.reasoningTokens !== undefined || b.reasoningTokens !== undefined;
|
|
80
|
+
const bothPriced = a.costState === 'priced' && b.costState === 'priced';
|
|
81
|
+
const neitherPriced = a.costUsd === null && b.costUsd === null;
|
|
82
|
+
return {
|
|
83
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
84
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
85
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
86
|
+
cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
|
|
87
|
+
reasoningTokens: sawReasoning ? (a.reasoningTokens ?? 0) + (b.reasoningTokens ?? 0) : undefined,
|
|
88
|
+
llmCallCount: a.llmCallCount + b.llmCallCount,
|
|
89
|
+
turnCount: a.turnCount + b.turnCount,
|
|
90
|
+
toolCallCount: a.toolCallCount + b.toolCallCount,
|
|
91
|
+
costUsd: a.costUsd !== null && b.costUsd !== null ? a.costUsd + b.costUsd : (a.costUsd ?? b.costUsd),
|
|
92
|
+
costState: bothPriced ? 'priced' : neitherPriced ? 'unpriced' : 'estimated',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Quality gates (global-config-driven, reused VERBATIM) + phase-required-gate assertion + phantom guard + reviewer constraint findings. */
|
|
96
|
+
async function evaluateGate(workstream, phase, report, deps) {
|
|
97
|
+
const results = [...await runWrfcGateChecks({
|
|
98
|
+
configManager: deps.configManager,
|
|
99
|
+
projectRoot: deps.projectRoot,
|
|
100
|
+
runtimeBus: deps.runtimeBus,
|
|
101
|
+
sessionId: deps.sessionId,
|
|
102
|
+
chainId: workstream.id,
|
|
103
|
+
})];
|
|
104
|
+
const ranNames = new Set(results.map((r) => r.gate));
|
|
105
|
+
const missingRequired = phase.gate.gates.filter((name) => !ranNames.has(name));
|
|
106
|
+
for (const name of missingRequired) {
|
|
107
|
+
results.push({ gate: name, passed: false, output: 'required gate is not configured/enabled', durationMs: 0 });
|
|
108
|
+
}
|
|
109
|
+
if (report.archetype === 'engineer' && !deps.skipClaimVerification) {
|
|
110
|
+
const verification = verifyEngineerClaims(report, deps.projectRoot);
|
|
111
|
+
if (verification.kind === 'unverified' || verification.kind === 'unverifiable_no_claims') {
|
|
112
|
+
results.push({ gate: 'phantom-work-guard', passed: false, output: verification.summary, durationMs: 0 });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let constraintFindings;
|
|
116
|
+
let unsatisfiedConstraintIds;
|
|
117
|
+
if (report.archetype === 'reviewer') {
|
|
118
|
+
const reviewer = report;
|
|
119
|
+
constraintFindings = reviewer.constraintFindings ?? [];
|
|
120
|
+
const unsatisfied = constraintFindings.filter((f) => !f.satisfied);
|
|
121
|
+
unsatisfiedConstraintIds = unsatisfied.map((f) => f.constraintId);
|
|
122
|
+
if (!reviewer.passed || unsatisfied.length > 0) {
|
|
123
|
+
results.push({
|
|
124
|
+
gate: 'reviewer-verdict',
|
|
125
|
+
passed: false,
|
|
126
|
+
output: unsatisfied.length > 0
|
|
127
|
+
? `${unsatisfied.length} unsatisfied constraint(s): ${unsatisfied.map((f) => f.constraintId).join(', ')}`
|
|
128
|
+
: 'reviewer did not pass',
|
|
129
|
+
durationMs: 0,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
passed: results.every((r) => r.passed),
|
|
135
|
+
results,
|
|
136
|
+
constraintFindings,
|
|
137
|
+
unsatisfiedConstraintIds,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function commitPhaseWork(item, phase, agentId, worktree, deps) {
|
|
141
|
+
if (phase.gate.scope === 'off')
|
|
142
|
+
return undefined;
|
|
143
|
+
let paths = phase.gate.scope === 'scoped' ? item.touchedPaths : undefined;
|
|
144
|
+
let exclusion;
|
|
145
|
+
if (paths && paths.length > 0 && deps.launchDirtySnapshot) {
|
|
146
|
+
const launchSnapshot = deps.launchDirtySnapshot;
|
|
147
|
+
if (launchSnapshot.size > 0) {
|
|
148
|
+
const { included, excluded } = excludeUntouchedLaunchResidue(deps.projectRoot, paths, launchSnapshot);
|
|
149
|
+
if (excluded.length > 0) {
|
|
150
|
+
exclusion = { excludedPaths: excluded, skipped: included.length === 0 };
|
|
151
|
+
if (included.length === 0) {
|
|
152
|
+
// Every candidate path is untouched launch-dirty residue — an
|
|
153
|
+
// honest "nothing this phase did needs committing", not a silent
|
|
154
|
+
// no-op AND not a fallback to sweeping the whole working tree.
|
|
155
|
+
logger.info('orchestration phase-runner: scoped commit skipped — every candidate path is untouched launch-dirty residue', {
|
|
156
|
+
itemId: item.id,
|
|
157
|
+
phaseId: phase.id,
|
|
158
|
+
excludedPaths: excluded,
|
|
159
|
+
});
|
|
160
|
+
return exclusion;
|
|
161
|
+
}
|
|
162
|
+
logger.info('orchestration phase-runner: excluded untouched launch-dirty residue from scoped commit', {
|
|
163
|
+
itemId: item.id,
|
|
164
|
+
phaseId: phase.id,
|
|
165
|
+
excludedPaths: excluded,
|
|
166
|
+
});
|
|
167
|
+
paths = [...included];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
await worktree.commitWorkingTree(`orchestration: ${item.title} — ${phase.kind} phase`, paths);
|
|
173
|
+
await worktree.merge(agentId);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
logger.warn('orchestration phase-runner: commit/merge failed', { itemId: item.id, phaseId: phase.id, error: summarizeError(error) });
|
|
177
|
+
}
|
|
178
|
+
return exclusion;
|
|
179
|
+
}
|
|
180
|
+
/** Runs one WorkItem through one Phase to completion (or cancellation/failure). Recurses (bounded by transportRetryLimit) on a transport-classified spawn failure. */
|
|
181
|
+
export async function runPhase(workstream, item, phase, priorReports, deps) {
|
|
182
|
+
const startedAt = Date.now();
|
|
183
|
+
const createWorktree = deps.createWorktree ?? (() => new AgentWorktree(deps.projectRoot));
|
|
184
|
+
const worktree = createWorktree();
|
|
185
|
+
const record = deps.agentManager.spawn({
|
|
186
|
+
mode: 'spawn',
|
|
187
|
+
task: buildPhaseTask(item, phase, priorReports),
|
|
188
|
+
template: templateForPhase(phase),
|
|
189
|
+
dangerously_disable_wrfc: true,
|
|
190
|
+
});
|
|
191
|
+
record.workItemId = item.id;
|
|
192
|
+
item.agentId = record.id;
|
|
193
|
+
item.allAgentIds.push(record.id);
|
|
194
|
+
item.branch ??= `agent/${item.id}`;
|
|
195
|
+
const signal = deps.cancellation.start(item.id);
|
|
196
|
+
deps.agentManager.registerCancellationSignal(record.id, signal);
|
|
197
|
+
let outcome;
|
|
198
|
+
try {
|
|
199
|
+
outcome = await awaitAgentTermination(deps.runtimeBus, deps.agentManager, record.id);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
deps.agentManager.releaseCancellationSignal(record.id);
|
|
203
|
+
deps.cancellation.release(item.id);
|
|
204
|
+
}
|
|
205
|
+
const usage = usageFromRecord(outcome.record, deps.priceUsage);
|
|
206
|
+
if (outcome.status === 'cancelled') {
|
|
207
|
+
await worktree.cleanup(record.id).catch(() => undefined);
|
|
208
|
+
return {
|
|
209
|
+
agentStatus: 'cancelled',
|
|
210
|
+
result: {
|
|
211
|
+
itemId: item.id,
|
|
212
|
+
phaseId: phase.id,
|
|
213
|
+
agentId: record.id,
|
|
214
|
+
report: genericReport('cancelled by operator'),
|
|
215
|
+
gate: { passed: false, results: [] },
|
|
216
|
+
startedAt,
|
|
217
|
+
completedAt: Date.now(),
|
|
218
|
+
usage,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (outcome.status === 'failed') {
|
|
223
|
+
const transportFailure = isTransportFailureMessage(outcome.record?.error ?? '');
|
|
224
|
+
const retryLimit = getWrfcTransportRetryLimit(deps.configManager);
|
|
225
|
+
if (transportFailure && item.transportRetryCount < retryLimit) {
|
|
226
|
+
item.transportRetryCount += 1;
|
|
227
|
+
await worktree.cleanup(record.id).catch(() => undefined);
|
|
228
|
+
await sleep(getWrfcTransportRetryDelayMs(deps.configManager));
|
|
229
|
+
return runPhase(workstream, item, phase, priorReports, deps);
|
|
230
|
+
}
|
|
231
|
+
await worktree.cleanup(record.id).catch(() => undefined);
|
|
232
|
+
return {
|
|
233
|
+
agentStatus: 'failed',
|
|
234
|
+
result: {
|
|
235
|
+
itemId: item.id,
|
|
236
|
+
phaseId: phase.id,
|
|
237
|
+
agentId: record.id,
|
|
238
|
+
report: genericReport(outcome.record?.error ?? 'agent failed'),
|
|
239
|
+
gate: { passed: false, results: [] },
|
|
240
|
+
startedAt,
|
|
241
|
+
completedAt: Date.now(),
|
|
242
|
+
usage,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const report = parseCompletionReport(outcome.record?.fullOutput ?? '') ?? genericReport(outcome.record?.fullOutput ?? '');
|
|
247
|
+
if (report.archetype === 'engineer') {
|
|
248
|
+
const engineerReport = report;
|
|
249
|
+
for (const path of [...engineerReport.filesCreated, ...engineerReport.filesModified, ...engineerReport.filesDeleted]) {
|
|
250
|
+
if (!item.touchedPaths.includes(path))
|
|
251
|
+
item.touchedPaths.push(path);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const gate = await evaluateGate(workstream, phase, report, deps);
|
|
255
|
+
let commitExclusion;
|
|
256
|
+
if (gate.passed) {
|
|
257
|
+
commitExclusion = await commitPhaseWork(item, phase, record.id, worktree, deps);
|
|
258
|
+
}
|
|
259
|
+
await worktree.cleanup(record.id).catch(() => undefined);
|
|
260
|
+
return {
|
|
261
|
+
agentStatus: 'completed',
|
|
262
|
+
result: {
|
|
263
|
+
itemId: item.id,
|
|
264
|
+
phaseId: phase.id,
|
|
265
|
+
agentId: record.id,
|
|
266
|
+
report,
|
|
267
|
+
gate,
|
|
268
|
+
startedAt,
|
|
269
|
+
completedAt: Date.now(),
|
|
270
|
+
usage,
|
|
271
|
+
...(commitExclusion ? { commitExclusion } : {}),
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** SDK-owned platform module. This implementation is maintained in goodvibes-sdk. */
|
|
2
|
+
/**
|
|
3
|
+
* Scheduler (Wave 4, wo701) — pure capacity-matching helpers, no side
|
|
4
|
+
* effects. The hard departure from WrfcController's pairwise
|
|
5
|
+
* engineer<->reviewer binding (startReview:883/startFix:1042): each tick,
|
|
6
|
+
* for every phase in ordinal order, free capacity slots (capacity minus
|
|
7
|
+
* in-flight) are filled from whichever waiting items are queued for that
|
|
8
|
+
* phase — an item advances the instant ITS gate passes, claimed by
|
|
9
|
+
* whatever slot happens to be free, never bound to a specific sibling item.
|
|
10
|
+
*/
|
|
11
|
+
import type { Phase, WorkItem, Workstream } from './types.js';
|
|
12
|
+
export declare function sortedPhases(workstream: Workstream): Phase[];
|
|
13
|
+
export declare function firstPhase(workstream: Workstream): Phase | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The next phase in ORDINARY forward progression. Deliberately skips
|
|
16
|
+
* 'fix'-kind phases: a dynamically-inserted fix phase sits at an ordinal
|
|
17
|
+
* after its review (see engine.ts findOrInsertFixPhase) but is reachable
|
|
18
|
+
* ONLY via the explicit review-failure re-route, never as "what comes next"
|
|
19
|
+
* for an item whose review already passed — otherwise a later item that
|
|
20
|
+
* never needed fixing would wrongly detour through it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function nextPhaseAfter(workstream: Workstream, ordinal: number): Phase | undefined;
|
|
23
|
+
export declare function phaseById(workstream: Workstream, phaseId: string): Phase | undefined;
|
|
24
|
+
/** The nearest preceding review-kind phase — the return target after a dynamically-inserted fix phase's gate passes. Purely structural (survives serialization with zero extra bookkeeping). */
|
|
25
|
+
export declare function reviewPhaseBefore(workstream: Workstream, phase: Phase): Phase | undefined;
|
|
26
|
+
export interface PhaseClaim {
|
|
27
|
+
readonly item: WorkItem;
|
|
28
|
+
readonly phase: Phase;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Which (item, phase) pairs have free capacity to claim RIGHT NOW. Pure —
|
|
32
|
+
* no side effects, no budget check (the caller applies budget.checkBudget
|
|
33
|
+
* before actually claiming, since budget is a *decision*, not a capacity
|
|
34
|
+
* fact this function should own).
|
|
35
|
+
*
|
|
36
|
+
* 'blocked-budget' items are deliberately included in the waiting set, not
|
|
37
|
+
* just 'pending'/'awaiting-capacity': a budget block is a recoverable
|
|
38
|
+
* decision (see BudgetCeiling/WorkItemState docs, types.ts), never a
|
|
39
|
+
* capacity fact, so a previously-blocked item must be reconsidered on every
|
|
40
|
+
* tick the instant a slot is free — the caller's budget.checkBudget call
|
|
41
|
+
* re-decides it fresh each time, honestly re-blocking it if the ceiling
|
|
42
|
+
* still refuses.
|
|
43
|
+
*/
|
|
44
|
+
export declare function computeClaims(workstream: Workstream): PhaseClaim[];
|
|
45
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../../src/platform/orchestration/scheduler.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE9D,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,KAAK,EAAE,CAE5D;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,UAAU,GAAG,KAAK,GAAG,SAAS,CAEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAEzF;AAED,wBAAgB,SAAS,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAEpF;AAED,gMAAgM;AAChM,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAEzF;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,CAiBlE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** SDK-owned platform module. This implementation is maintained in goodvibes-sdk. */
|
|
2
|
+
export function sortedPhases(workstream) {
|
|
3
|
+
return [...workstream.phases].sort((a, b) => a.ordinal - b.ordinal);
|
|
4
|
+
}
|
|
5
|
+
export function firstPhase(workstream) {
|
|
6
|
+
return sortedPhases(workstream)[0];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The next phase in ORDINARY forward progression. Deliberately skips
|
|
10
|
+
* 'fix'-kind phases: a dynamically-inserted fix phase sits at an ordinal
|
|
11
|
+
* after its review (see engine.ts findOrInsertFixPhase) but is reachable
|
|
12
|
+
* ONLY via the explicit review-failure re-route, never as "what comes next"
|
|
13
|
+
* for an item whose review already passed — otherwise a later item that
|
|
14
|
+
* never needed fixing would wrongly detour through it.
|
|
15
|
+
*/
|
|
16
|
+
export function nextPhaseAfter(workstream, ordinal) {
|
|
17
|
+
return sortedPhases(workstream).find((p) => p.ordinal > ordinal && p.kind !== 'fix');
|
|
18
|
+
}
|
|
19
|
+
export function phaseById(workstream, phaseId) {
|
|
20
|
+
return workstream.phases.find((p) => p.id === phaseId);
|
|
21
|
+
}
|
|
22
|
+
/** The nearest preceding review-kind phase — the return target after a dynamically-inserted fix phase's gate passes. Purely structural (survives serialization with zero extra bookkeeping). */
|
|
23
|
+
export function reviewPhaseBefore(workstream, phase) {
|
|
24
|
+
return sortedPhases(workstream).filter((p) => p.ordinal < phase.ordinal && p.kind === 'review').pop();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Which (item, phase) pairs have free capacity to claim RIGHT NOW. Pure —
|
|
28
|
+
* no side effects, no budget check (the caller applies budget.checkBudget
|
|
29
|
+
* before actually claiming, since budget is a *decision*, not a capacity
|
|
30
|
+
* fact this function should own).
|
|
31
|
+
*
|
|
32
|
+
* 'blocked-budget' items are deliberately included in the waiting set, not
|
|
33
|
+
* just 'pending'/'awaiting-capacity': a budget block is a recoverable
|
|
34
|
+
* decision (see BudgetCeiling/WorkItemState docs, types.ts), never a
|
|
35
|
+
* capacity fact, so a previously-blocked item must be reconsidered on every
|
|
36
|
+
* tick the instant a slot is free — the caller's budget.checkBudget call
|
|
37
|
+
* re-decides it fresh each time, honestly re-blocking it if the ceiling
|
|
38
|
+
* still refuses.
|
|
39
|
+
*/
|
|
40
|
+
export function computeClaims(workstream) {
|
|
41
|
+
const claims = [];
|
|
42
|
+
for (const phase of sortedPhases(workstream)) {
|
|
43
|
+
const inFlight = workstream.items.filter((item) => item.currentPhaseId === phase.id && item.state === 'in-phase').length;
|
|
44
|
+
let free = phase.capacity - inFlight;
|
|
45
|
+
if (free <= 0)
|
|
46
|
+
continue;
|
|
47
|
+
const waiting = workstream.items.filter((item) => item.currentPhaseId === phase.id
|
|
48
|
+
&& (item.state === 'pending' || item.state === 'awaiting-capacity' || item.state === 'blocked-budget'));
|
|
49
|
+
for (const item of waiting) {
|
|
50
|
+
if (free <= 0)
|
|
51
|
+
break;
|
|
52
|
+
claims.push({ item, phase });
|
|
53
|
+
free -= 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return claims;
|
|
57
|
+
}
|