@mcrescenzo/opencode-workflows 0.1.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/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +746 -0
- package/SECURITY.md +38 -0
- package/commands/repo-bughunt.md +94 -0
- package/commands/repo-review.md +148 -0
- package/commands/workflow-live-gates-release-check.md +143 -0
- package/docs/workflow-plugin.md +400 -0
- package/opencode-workflows.js +5 -0
- package/package.json +86 -0
- package/skills/opencode-workflow-authoring/SKILL.md +180 -0
- package/skills/repo-review-command-protocol/SKILL.md +56 -0
- package/skills/workflow-model-tiering/SKILL.md +57 -0
- package/skills/workflow-plan-review/SKILL.md +91 -0
- package/workflow-kernel/approval-hashing.js +39 -0
- package/workflow-kernel/async-util.js +33 -0
- package/workflow-kernel/audited-shell-policy.js +200 -0
- package/workflow-kernel/authority-policy.js +670 -0
- package/workflow-kernel/budget-accounting.js +142 -0
- package/workflow-kernel/capability-adapter.js +753 -0
- package/workflow-kernel/child-agent-runner.js +1264 -0
- package/workflow-kernel/constants.js +117 -0
- package/workflow-kernel/diagnostics.js +152 -0
- package/workflow-kernel/drain-runtime.js +421 -0
- package/workflow-kernel/errors.js +181 -0
- package/workflow-kernel/event-journal.js +487 -0
- package/workflow-kernel/extension-registry.js +144 -0
- package/workflow-kernel/free-text-redactor.js +91 -0
- package/workflow-kernel/gate-shapes.js +82 -0
- package/workflow-kernel/git-util.js +45 -0
- package/workflow-kernel/index.js +72 -0
- package/workflow-kernel/integration-mode.js +155 -0
- package/workflow-kernel/lane-effort-policy.js +134 -0
- package/workflow-kernel/lifecycle-control.js +608 -0
- package/workflow-kernel/live-gate-probes.js +916 -0
- package/workflow-kernel/notification-toast-cards.js +393 -0
- package/workflow-kernel/notification-toast-policy.js +179 -0
- package/workflow-kernel/notification-toast-scope.js +100 -0
- package/workflow-kernel/notification-toast.js +287 -0
- package/workflow-kernel/path-policy.js +219 -0
- package/workflow-kernel/result-readback.js +106 -0
- package/workflow-kernel/role-template-loading.js +606 -0
- package/workflow-kernel/run-context.js +139 -0
- package/workflow-kernel/run-observability.js +43 -0
- package/workflow-kernel/run-store-fs.js +231 -0
- package/workflow-kernel/run-store-locks.js +180 -0
- package/workflow-kernel/run-store-projections.js +421 -0
- package/workflow-kernel/run-store-rehydrate.js +68 -0
- package/workflow-kernel/run-store-state.js +147 -0
- package/workflow-kernel/run-store-status-format.js +1154 -0
- package/workflow-kernel/run-store-status.js +107 -0
- package/workflow-kernel/sandbox-executor.js +1131 -0
- package/workflow-kernel/session-access.js +56 -0
- package/workflow-kernel/structured-output.js +143 -0
- package/workflow-kernel/test-fix-drain-adapter.js +119 -0
- package/workflow-kernel/text-json.js +136 -0
- package/workflow-kernel/workflow-plugin.js +3017 -0
- package/workflow-kernel/workflow-source.js +444 -0
- package/workflow-kernel/worktree-adapter.js +256 -0
- package/workflow-kernel/worktree-git.js +147 -0
- package/workflows/repo-bughunt.js +362 -0
- package/workflows/repo-cleanup.js +383 -0
- package/workflows/repo-complexity.js +404 -0
- package/workflows/repo-deps.js +457 -0
- package/workflows/repo-modernize.js +395 -0
- package/workflows/repo-perf.js +394 -0
- package/workflows/repo-review.js +831 -0
- package/workflows/repo-security-audit.js +466 -0
- package/workflows/repo-test-gaps.js +377 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { WorkflowBudgetStoppedError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export function zeroTokens() {
|
|
4
|
+
return { input: 0, output: 0, reasoning: 0 };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function addTokens(left = zeroTokens(), right = zeroTokens()) {
|
|
8
|
+
return {
|
|
9
|
+
input: (left.input ?? 0) + (right.input ?? 0),
|
|
10
|
+
output: (left.output ?? 0) + (right.output ?? 0),
|
|
11
|
+
reasoning: (left.reasoning ?? 0) + (right.reasoning ?? 0),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function reservedCostOf(run) {
|
|
16
|
+
return Number.isFinite(run.reservedCost) ? run.reservedCost : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function reservedTokensOf(run) {
|
|
20
|
+
return Number.isFinite(run.reservedTokens) ? run.reservedTokens : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function reservedLanesOf(run) {
|
|
24
|
+
return Number.isFinite(run.reservedLanes) ? run.reservedLanes : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tokenCount(tokens = zeroTokens()) {
|
|
28
|
+
return (tokens.input ?? 0) + (tokens.output ?? 0) + (tokens.reasoning ?? 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function remainingBudget(run) {
|
|
32
|
+
const ceilings = run.budgetCeilings ?? {};
|
|
33
|
+
const totalCost = run.cost + run.replayedCost + reservedCostOf(run);
|
|
34
|
+
const totalTokens = tokenCount(addTokens(run.tokens, run.replayedTokens)) + reservedTokensOf(run);
|
|
35
|
+
return {
|
|
36
|
+
cost: Number.isFinite(ceilings.maxCost) ? ceilings.maxCost - totalCost : null,
|
|
37
|
+
tokens: Number.isFinite(ceilings.maxTokens) ? ceilings.maxTokens - totalTokens : null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function budgetSnapshot(run) {
|
|
42
|
+
return {
|
|
43
|
+
live: { tokens: run.tokens, cost: run.cost },
|
|
44
|
+
replayed: { tokens: run.replayedTokens, cost: run.replayedCost },
|
|
45
|
+
total: {
|
|
46
|
+
tokens: addTokens(run.tokens, run.replayedTokens),
|
|
47
|
+
cost: run.cost + run.replayedCost,
|
|
48
|
+
},
|
|
49
|
+
// In-flight reservations of not-yet-reported concurrent-lane spend (opencode-workflows-dx1n).
|
|
50
|
+
// Surfaced for observability only; `total` stays REAL reported spend so existing readers are
|
|
51
|
+
// unaffected, while checkBudgetBeforeLaunch folds these into its ceiling comparison.
|
|
52
|
+
reserved: { cost: reservedCostOf(run), tokens: reservedTokensOf(run), lanes: reservedLanesOf(run) },
|
|
53
|
+
ceilings: run.budgetCeilings,
|
|
54
|
+
remaining: remainingBudget(run),
|
|
55
|
+
remainingAgents: Math.max(0, run.maxAgents - run.agentsStarted),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function checkBudgetBeforeLaunch(run) {
|
|
60
|
+
const ceilings = run.budgetCeilings ?? {};
|
|
61
|
+
const remaining = remainingBudget(run);
|
|
62
|
+
// Ceilings apply to TOTAL spend (live + replayed) PLUS in-flight reservations. On a resumed run,
|
|
63
|
+
// cache-hit lanes accumulate into replayedCost/replayedTokens; counting only live spend would let
|
|
64
|
+
// a resume relaunch live lanes past a ceiling that total spend has already breached. The reserved
|
|
65
|
+
// term closes a check-then-act race across concurrent lanes: up to `concurrency` lanes clear the
|
|
66
|
+
// acquireAgentSlot semaphore and each pass this gate back-to-back BEFORE any has reported its
|
|
67
|
+
// real session.prompt spend, so without reservations aggregate spend can overshoot the ceiling by
|
|
68
|
+
// up to (concurrency-1) lanes' worth. Each launching lane reserves a conservative slice of the
|
|
69
|
+
// remaining headroom (reserveLaneBudget) synchronously before its prompt, so a concurrent lane's
|
|
70
|
+
// check here observes that in-flight commitment (opencode-workflows-dx1n).
|
|
71
|
+
if (remaining.cost !== null && remaining.cost <= 0) {
|
|
72
|
+
const totalCost = run.cost + run.replayedCost + reservedCostOf(run);
|
|
73
|
+
throw new WorkflowBudgetStoppedError(`Workflow cost ceiling reached: ${totalCost} >= ${ceilings.maxCost}`);
|
|
74
|
+
}
|
|
75
|
+
if (remaining.tokens !== null && remaining.tokens <= 0) {
|
|
76
|
+
const totalTokens = tokenCount(addTokens(run.tokens, run.replayedTokens)) + reservedTokensOf(run);
|
|
77
|
+
throw new WorkflowBudgetStoppedError(`Workflow token ceiling reached: ${totalTokens} >= ${ceilings.maxTokens}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Synchronously reserve a conservative per-lane slice of the REMAINING budget headroom for a lane
|
|
82
|
+
// that is about to launch but has not yet reported its session.prompt cost/tokens. Reserving before
|
|
83
|
+
// the async prompt begins — and folding run.reservedCost/reservedTokens into checkBudgetBeforeLaunch
|
|
84
|
+
// — is what stops a wave of up to `concurrency` concurrent lanes from each clearing the ceiling gate
|
|
85
|
+
// on stale (pre-spend) counters and collectively overshooting maxCost/maxTokens (opencode-workflows-
|
|
86
|
+
// dx1n). The slice is (remaining headroom / remaining concurrency slots): distributing the headroom
|
|
87
|
+
// evenly across the at-most-`concurrency` in-flight lanes makes their combined reservation approach
|
|
88
|
+
// the full headroom, so the (concurrency+1)-th launch attempt is gated instead of overshooting.
|
|
89
|
+
// Parameter-free (derived from the run's own ceilings + concurrency), so it needs no magic per-lane
|
|
90
|
+
// cost constant and self-scales to any ceiling. Returns the reservation for releaseLaneBudget to
|
|
91
|
+
// reconcile once the lane's real spend lands (or the attempt fails). A no-op (zero slice) when the
|
|
92
|
+
// corresponding ceiling is unset.
|
|
93
|
+
export function reserveLaneBudget(run) {
|
|
94
|
+
const ceilings = run.budgetCeilings ?? {};
|
|
95
|
+
const concurrency = Number.isInteger(run.concurrency) && run.concurrency > 0 ? run.concurrency : 1;
|
|
96
|
+
// reservedLanes counts lanes ALREADY holding a reservation; this lane also holds an acquireAgentSlot
|
|
97
|
+
// slot, so at most (concurrency-1) others precede it and remainingSlots >= 1.
|
|
98
|
+
const remainingSlots = Math.max(1, concurrency - reservedLanesOf(run));
|
|
99
|
+
|
|
100
|
+
let cost = 0;
|
|
101
|
+
if (Number.isFinite(ceilings.maxCost)) {
|
|
102
|
+
const headroom = ceilings.maxCost - (run.cost + run.replayedCost + reservedCostOf(run));
|
|
103
|
+
cost = Math.max(0, headroom / remainingSlots);
|
|
104
|
+
}
|
|
105
|
+
let tokens = 0;
|
|
106
|
+
if (Number.isFinite(ceilings.maxTokens)) {
|
|
107
|
+
const combined = addTokens(run.tokens, run.replayedTokens);
|
|
108
|
+
const liveTokens = combined.input + combined.output + combined.reasoning;
|
|
109
|
+
const headroom = ceilings.maxTokens - (liveTokens + reservedTokensOf(run));
|
|
110
|
+
tokens = Math.max(0, headroom / remainingSlots);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
run.reservedCost = reservedCostOf(run) + cost;
|
|
114
|
+
run.reservedTokens = reservedTokensOf(run) + tokens;
|
|
115
|
+
run.reservedLanes = reservedLanesOf(run) + 1;
|
|
116
|
+
return { cost, tokens };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Reconcile a reservation made by reserveLaneBudget once the lane's real spend has been folded into
|
|
120
|
+
// run.cost/run.tokens (success) or the attempt failed without spending (retry/terminal). Idempotent
|
|
121
|
+
// against a null/absent reservation and floored at zero so a double-release cannot drive the
|
|
122
|
+
// counters negative (opencode-workflows-dx1n).
|
|
123
|
+
export function releaseLaneBudget(run, reservation) {
|
|
124
|
+
if (!reservation) return;
|
|
125
|
+
run.reservedCost = Math.max(0, reservedCostOf(run) - (Number.isFinite(reservation.cost) ? reservation.cost : 0));
|
|
126
|
+
run.reservedTokens = Math.max(0, reservedTokensOf(run) - (Number.isFinite(reservation.tokens) ? reservation.tokens : 0));
|
|
127
|
+
run.reservedLanes = Math.max(0, reservedLanesOf(run) - 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function copyTokenTotals(target, source) {
|
|
131
|
+
if (!source || typeof source !== "object") return;
|
|
132
|
+
for (const key of ["input", "output", "reasoning"]) {
|
|
133
|
+
if (Number.isFinite(source[key])) target[key] = source[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function normalizeBudgetCeilings(ceilings = {}) {
|
|
138
|
+
return {
|
|
139
|
+
maxCost: Number.isFinite(ceilings.maxCost) ? ceilings.maxCost : undefined,
|
|
140
|
+
maxTokens: Number.isInteger(ceilings.maxTokens) ? ceilings.maxTokens : undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|