@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/CODE_OF_CONDUCT.md +134 -0
  3. package/CONTRIBUTING.md +95 -0
  4. package/LICENSE +21 -0
  5. package/README.md +746 -0
  6. package/SECURITY.md +38 -0
  7. package/commands/repo-bughunt.md +94 -0
  8. package/commands/repo-review.md +148 -0
  9. package/commands/workflow-live-gates-release-check.md +143 -0
  10. package/docs/workflow-plugin.md +400 -0
  11. package/opencode-workflows.js +5 -0
  12. package/package.json +86 -0
  13. package/skills/opencode-workflow-authoring/SKILL.md +180 -0
  14. package/skills/repo-review-command-protocol/SKILL.md +56 -0
  15. package/skills/workflow-model-tiering/SKILL.md +57 -0
  16. package/skills/workflow-plan-review/SKILL.md +91 -0
  17. package/workflow-kernel/approval-hashing.js +39 -0
  18. package/workflow-kernel/async-util.js +33 -0
  19. package/workflow-kernel/audited-shell-policy.js +200 -0
  20. package/workflow-kernel/authority-policy.js +670 -0
  21. package/workflow-kernel/budget-accounting.js +142 -0
  22. package/workflow-kernel/capability-adapter.js +753 -0
  23. package/workflow-kernel/child-agent-runner.js +1264 -0
  24. package/workflow-kernel/constants.js +117 -0
  25. package/workflow-kernel/diagnostics.js +152 -0
  26. package/workflow-kernel/drain-runtime.js +421 -0
  27. package/workflow-kernel/errors.js +181 -0
  28. package/workflow-kernel/event-journal.js +487 -0
  29. package/workflow-kernel/extension-registry.js +144 -0
  30. package/workflow-kernel/free-text-redactor.js +91 -0
  31. package/workflow-kernel/gate-shapes.js +82 -0
  32. package/workflow-kernel/git-util.js +45 -0
  33. package/workflow-kernel/index.js +72 -0
  34. package/workflow-kernel/integration-mode.js +155 -0
  35. package/workflow-kernel/lane-effort-policy.js +134 -0
  36. package/workflow-kernel/lifecycle-control.js +608 -0
  37. package/workflow-kernel/live-gate-probes.js +916 -0
  38. package/workflow-kernel/notification-toast-cards.js +393 -0
  39. package/workflow-kernel/notification-toast-policy.js +179 -0
  40. package/workflow-kernel/notification-toast-scope.js +100 -0
  41. package/workflow-kernel/notification-toast.js +287 -0
  42. package/workflow-kernel/path-policy.js +219 -0
  43. package/workflow-kernel/result-readback.js +106 -0
  44. package/workflow-kernel/role-template-loading.js +606 -0
  45. package/workflow-kernel/run-context.js +139 -0
  46. package/workflow-kernel/run-observability.js +43 -0
  47. package/workflow-kernel/run-store-fs.js +231 -0
  48. package/workflow-kernel/run-store-locks.js +180 -0
  49. package/workflow-kernel/run-store-projections.js +421 -0
  50. package/workflow-kernel/run-store-rehydrate.js +68 -0
  51. package/workflow-kernel/run-store-state.js +147 -0
  52. package/workflow-kernel/run-store-status-format.js +1154 -0
  53. package/workflow-kernel/run-store-status.js +107 -0
  54. package/workflow-kernel/sandbox-executor.js +1131 -0
  55. package/workflow-kernel/session-access.js +56 -0
  56. package/workflow-kernel/structured-output.js +143 -0
  57. package/workflow-kernel/test-fix-drain-adapter.js +119 -0
  58. package/workflow-kernel/text-json.js +136 -0
  59. package/workflow-kernel/workflow-plugin.js +3017 -0
  60. package/workflow-kernel/workflow-source.js +444 -0
  61. package/workflow-kernel/worktree-adapter.js +256 -0
  62. package/workflow-kernel/worktree-git.js +147 -0
  63. package/workflows/repo-bughunt.js +362 -0
  64. package/workflows/repo-cleanup.js +383 -0
  65. package/workflows/repo-complexity.js +404 -0
  66. package/workflows/repo-deps.js +457 -0
  67. package/workflows/repo-modernize.js +395 -0
  68. package/workflows/repo-perf.js +394 -0
  69. package/workflows/repo-review.js +831 -0
  70. package/workflows/repo-security-audit.js +466 -0
  71. 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
+ }