@bongos/core 1.19.315 → 1.19.317

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 (38) hide show
  1. package/.bongos-core.json +104 -29
  2. package/.claude/skills/merge-mode/SKILL.md +16 -0
  3. package/.claude/skills/planning-session/SKILL.md +10 -0
  4. package/modules/lifecycle/db-analytics.js +719 -0
  5. package/modules/lifecycle/db-claim-reads.js +165 -0
  6. package/modules/lifecycle/db-claims.js +934 -0
  7. package/modules/lifecycle/db-deps-criteria.js +441 -0
  8. package/modules/lifecycle/db-goals.js +763 -0
  9. package/modules/lifecycle/db-grade.js +233 -0
  10. package/modules/lifecycle/db-overrides.js +181 -0
  11. package/modules/lifecycle/db-rank-authz.js +446 -0
  12. package/modules/lifecycle/db-shared.js +115 -0
  13. package/modules/lifecycle/db-ship.js +994 -0
  14. package/modules/lifecycle/db-tasks.js +810 -0
  15. package/modules/lifecycle/db-versions.js +118 -0
  16. package/modules/lifecycle/db.js +167 -5564
  17. package/package-lock.json +2 -2
  18. package/package.json +1 -1
  19. package/scripts/gds/coreB-carve-proof.sh +18 -9
  20. package/scripts/gds/fitness.js +35 -27
  21. package/scripts/gds/identity-strip.js +75 -0
  22. package/scripts/gds/preflight.js +25 -2
  23. package/scripts/gds/provision.js +7 -3
  24. package/scripts/gds/skill-preflight.js +336 -0
  25. package/src/module-api.js +5 -1
  26. package/tests/auto_satisfy_criteria.mjs +3 -2
  27. package/tests/dependencies_migration.mjs +3 -2
  28. package/tests/fitness.mjs +43 -0
  29. package/tests/goal_abuse_matrix.mjs +3 -2
  30. package/tests/goal_advisory.mjs +3 -2
  31. package/tests/government_claim_eligibility.mjs +3 -1
  32. package/tests/government_rank_sync.mjs +9 -2
  33. package/tests/helpers.mjs +19 -0
  34. package/tests/migration_allocation.mjs +3 -1
  35. package/tests/publish_reconciler.mjs +3 -1
  36. package/tests/rank_gate.mjs +3 -1
  37. package/tests/reward_gate.mjs +3 -1
  38. package/tests/skill_preflight.mjs +251 -0
@@ -0,0 +1,233 @@
1
+ // modules/lifecycle/db-grade.js — applyGrade — writes the grader result and promotes completed→confirmed.
2
+ //
3
+ // Carved out of modules/lifecycle/db.js by task 1003204 (goal 1000079, the R03
4
+ // de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
5
+ // original db.js lines 4561-4773 unchanged. db.js remains the facade that
6
+ // re-exports every name, so every existing db call site is unchanged.
7
+ //
8
+ // THE BOUNDARY (ADR 0093 §1): this file reaches core ONLY through the published
9
+ // doorway ../../src/module-api.js — never a core internal, never a sibling module.
10
+
11
+ 'use strict';
12
+
13
+ const api = require('../../src/module-api');
14
+ const { pool, withTx } = api;
15
+ const seams = api;
16
+ const { logGradeAttempt } = require('./db-analytics.js');
17
+ const { mostRecentClaimHolderId } = require('./db-shared.js');
18
+
19
+ // applyGrade — V3.R18 (#209). Writes the subagent grader result to task_grades
20
+ // and, if the grade passed, promotes completed→confirmed + awards credits.
21
+ //
22
+ // Inputs (the route handler validates + scores the subagent JSON before
23
+ // calling this, so we accept a pre-built grade-result):
24
+ // taskId — number
25
+ // builderId — current authenticated builder (must own the task's most-
26
+ // recent claim; the route handler enforces this)
27
+ // gradeResult — { score, passed, threshold, grader_kind, grader_version,
28
+ // rubric_scores, signals, notes_md, issues }
29
+ //
30
+ // Returns: { taskId, taskTitle, taskStatus, creditsAwarded, justUnlocked,
31
+ // grade: {score, passed, ...} }.
32
+ //
33
+ // Idempotent: the task_grades row is upsert-on-task_id (a re-grade overwrites);
34
+ // credit_log is guarded by an existence check on (task_id, 'task.confirmed').
35
+
36
+ async function applyGrade({ taskId, builderId, gradeResult, committedFiles = null, diffHash = null }, deps = {}) {
37
+ // committedFiles (touches[] cleanup [3/8] / task 878): the real committed
38
+ // changed-files for this ship, computed client-side by ship.js. Threaded to
39
+ // evaluateAfterShip so the prefix-keyed shipper badges reflect the actual
40
+ // diff rather than the predicted touches[]. Optional — null/absent falls back
41
+ // to touches[] inside evaluateAfterShip.
42
+ // #871 test seam (mirrors confirmTask's #538 seam): a caller-supplied pool is
43
+ // honored ONLY under NODE_ENV==='test' so tests/grade_attribution.mjs can drive
44
+ // the txn with a SQL-aware fakeClient. Production callers omit deps → module pool.
45
+ const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
46
+ const result = await withTx(async (client) => {
47
+ const { rows: taskRows } = await client.query(
48
+ `SELECT id, title, status, credits_reward, kind FROM tasks WHERE id = $1 FOR UPDATE`,
49
+ [taskId]
50
+ );
51
+ if (taskRows.length === 0) {
52
+ throw Object.assign(new Error('task not found'), { code: 'TASK_NOT_FOUND' });
53
+ }
54
+ const task = taskRows[0];
55
+
56
+ // The grade can only land while the task is at `completed` (just shipped
57
+ // and awaiting grading) or `confirmed` (already promoted by a prior grade
58
+ // pass — re-grading is allowed and writes a fresh row, but it does not
59
+ // re-award credits or re-evaluate achievements). At `shipped`/`backlog`/
60
+ // `active`/`ready`/`blocked`/`abandoned` the grade has no lifecycle target,
61
+ // so we refuse.
62
+ if (!['completed', 'confirmed'].includes(task.status)) {
63
+ throw Object.assign(new Error(`task is at ${task.status}; grade only applies at completed/confirmed`), {
64
+ code: 'TASK_NOT_GRADABLE',
65
+ current_status: task.status,
66
+ });
67
+ }
68
+
69
+ // SECURITY (#871 — mirrors the #661 confirmTask fix): credit + attribute the
70
+ // builder who actually DID the work — the task's most-recent claim holder —
71
+ // NEVER the caller of this endpoint. Before this fix applyGrade set
72
+ // tasks.shipped_by and the credit_log row to `builderId` (req.builder.id from
73
+ // the route), so a builder who POSTed a fabricated passing grade on ANOTHER
74
+ // builder's `completed` task captured both the ship attribution AND the
75
+ // drachmae. POST /tasks/:id/grade now also gates to the claim holder/Archon,
76
+ // but we fix the attribution here too: defense-in-depth, and it is the
77
+ // CORRECT behaviour for the Archon `--regrade` path where the caller is not
78
+ // the worker. Fall back to the caller only if the task has no claim at all
79
+ // (degenerate — e.g. status set directly via SQL) so a legit grade is never
80
+ // refused.
81
+ const creditedBuilderId = (await mostRecentClaimHolderId(client, taskId)) ?? builderId;
82
+
83
+ // Upsert the grade row. The subagent's issues[] is embedded into the
84
+ // signals jsonb so /me + the public dashboard can surface structured
85
+ // failure detail without a schema change. (task_grades has no `issues`
86
+ // column.)
87
+ const signalsForRow = {
88
+ ...(gradeResult.signals || {}),
89
+ issues: gradeResult.issues || [],
90
+ };
91
+ await client.query(
92
+ `INSERT INTO task_grades
93
+ (task_id, score, passed, threshold, grader_kind, grader_version,
94
+ rubric_scores, signals, notes_md, diff_hash)
95
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, $9, $10)
96
+ ON CONFLICT (task_id) DO UPDATE SET
97
+ score = EXCLUDED.score,
98
+ passed = EXCLUDED.passed,
99
+ threshold = EXCLUDED.threshold,
100
+ grader_kind = EXCLUDED.grader_kind,
101
+ grader_version = EXCLUDED.grader_version,
102
+ rubric_scores = EXCLUDED.rubric_scores,
103
+ signals = EXCLUDED.signals,
104
+ notes_md = EXCLUDED.notes_md,
105
+ diff_hash = EXCLUDED.diff_hash,
106
+ graded_at = now()`,
107
+ [
108
+ taskId,
109
+ gradeResult.score,
110
+ gradeResult.passed,
111
+ gradeResult.threshold,
112
+ gradeResult.grader_kind,
113
+ gradeResult.grader_version,
114
+ JSON.stringify(gradeResult.rubric_scores || {}),
115
+ JSON.stringify(signalsForRow),
116
+ gradeResult.notes_md ?? null,
117
+ diffHash ?? null,
118
+ ]
119
+ );
120
+
121
+ let finalStatus = task.status;
122
+ let creditsAwarded = 0;
123
+ let creditsRaw = 0;
124
+ let creditsMultiplier = 1.0;
125
+ let ideaBonus = null; // { ideaBonusAwarded, bonusBuilderId, ideaId } | null
126
+ let netNegativeBonus = 0;
127
+ let subtractorUnlocks = [];
128
+ let justUnlocked = [];
129
+ // Credit allocation + gamification carved to modules/economy/ (BV1.R77 / ADR
130
+ // 0093 §1-2): resolve the `reward` kernel port ONCE; every credit/achievement
131
+ // write below runs inside this same txn via the resolved provider. The
132
+ // state-machine SQL (FOR UPDATE, task_grades upsert, UPDATE tasks status,
133
+ // claim-holder lookup, logGradeAttempt) stays here in the lifecycle.
134
+ const reward = seams.resolveOptional('reward');
135
+ if (gradeResult.passed && task.status === 'completed') {
136
+ await client.query(
137
+ `UPDATE tasks
138
+ SET status = 'confirmed',
139
+ shipped_by = COALESCE(shipped_by, $2),
140
+ updated_at = now()
141
+ WHERE id = $1 AND status = 'completed'`,
142
+ [taskId, creditedBuilderId]
143
+ );
144
+ finalStatus = 'confirmed';
145
+
146
+ // V3.R93 (#354): reward simplification. A ship whose diff removed more
147
+ // lines than it added earns a small flat bonus, written as its own
148
+ // credit_log row (reason='ship.net_negative') with a self-describing
149
+ // description. Idempotent on (task_id, reason) so a re-grade is a no-op.
150
+ // diff_stats arrives via gradeResult.signals (ship.js decorates it).
151
+ //
152
+ // Awarded BEFORE the achievement evaluators so the Subtractor achievement
153
+ // (which counts the builder's net-negative ships) sees the current ship's
154
+ // bonus row in this same transaction.
155
+ const nn = await reward.awardNetNegativeBonus(
156
+ client, creditedBuilderId, taskId, gradeResult.signals?.diff_stats
157
+ );
158
+ netNegativeBonus = nn.bonus;
159
+ subtractorUnlocks = nn.justUnlocked;
160
+
161
+ // ADR 0120 "pay on land": the per-task credit + idea-promotion bonus are
162
+ // NO LONGER awarded at grader-confirm. They land at the confirmed→shipped
163
+ // flip (shipTask → awardLandingCredit, keyed 'task.shipped'), so a task
164
+ // that strands at 'confirmed' pays nothing until its code is on `main`.
165
+ // The net-negative (simplification) bonus above STAYS here — it derives
166
+ // from grade-time diff_stats that only exist at grade time, so it cannot
167
+ // move to shipTask (and is outside ADR 0120's two named reward writes).
168
+ // creditsAwarded/creditsRaw/creditsMultiplier/ideaBonus stay 0/null at
169
+ // confirm for a stable return contract; the credit lands at ship. The
170
+ // credit-crossing achievement evaluator (evaluateAfterCreditAward) moves
171
+ // with the credit to shipTask's reconcile; here we still fire the
172
+ // ship/subtractor unlocks that DON'T depend on the per-task credit.
173
+ const shipUnlocks = await reward.evaluateAfterShip(client, creditedBuilderId, taskId, committedFiles);
174
+ justUnlocked = [...subtractorUnlocks, ...shipUnlocks];
175
+
176
+ // task 1679: state-based safety net. The granular evaluators above are
177
+ // boundary-/path-sensitive — breaking-the-100c-bar only fires when a
178
+ // per-task confirm credit crosses 100 (the session cost-plus reward never
179
+ // hits it), and streaks key off current_streak. reconcile re-checks every
180
+ // achievement against current state and unlocks any straggler. It is
181
+ // idempotent: anything the granular calls just unlocked is visible in this
182
+ // txn, so reconcile's alreadyUnlocked guard skips it (no dup awards).
183
+ // SAVEPOINT-wrapped so it can never fail the grade-confirm.
184
+ if (reward && reward.reconcileBuilderAchievements) {
185
+ await client.query('SAVEPOINT ach_reconcile');
186
+ try {
187
+ const reconciled = await reward.reconcileBuilderAchievements(client, creditedBuilderId, { taskId });
188
+ await client.query('RELEASE SAVEPOINT ach_reconcile');
189
+ if (reconciled.length) justUnlocked = [...justUnlocked, ...reconciled];
190
+ } catch (_) {
191
+ await client.query('ROLLBACK TO SAVEPOINT ach_reconcile');
192
+ }
193
+ }
194
+ }
195
+ // !passed → task_grades row written above; task stays at completed; no
196
+ // credits, no achievements. Builder can manual-confirm via /confirm if the
197
+ // grade was wrong.
198
+
199
+ return {
200
+ taskId,
201
+ taskTitle: task.title,
202
+ taskStatus: finalStatus,
203
+ creditedBuilderId, // #871: the builder credited/attributed (claim holder, not caller)
204
+ creditsAwarded,
205
+ creditsRaw,
206
+ creditsMultiplier,
207
+ ideaBonus, // V3.R27 (#247): { ideaBonusAwarded, bonusBuilderId, ideaId } | null
208
+ netNegativeBonus,
209
+ advanceToMerge: finalStatus === 'confirmed',
210
+ justUnlocked,
211
+ grade: {
212
+ score: gradeResult.score,
213
+ passed: gradeResult.passed,
214
+ threshold: gradeResult.threshold,
215
+ rubric_scores: gradeResult.rubric_scores || {},
216
+ notes_md: gradeResult.notes_md ?? null,
217
+ issues: gradeResult.issues || [],
218
+ },
219
+ };
220
+ }, { pool: activePool });
221
+
222
+ // 6D.6 (ADR 0027): record this grade ATTEMPT in the append-only log so
223
+ // re-grade thrash is countable (task_grades is upsert-unique). Post-commit +
224
+ // best-effort: logGradeAttempt uses a pooled connection and swallows its own
225
+ // errors, so it can never roll back or block the grade that just committed.
226
+ await logGradeAttempt({ taskId, builderId, passed: gradeResult.passed, score: gradeResult.score });
227
+
228
+ return result;
229
+ }
230
+
231
+ module.exports = {
232
+ applyGrade,
233
+ };
@@ -0,0 +1,181 @@
1
+ // modules/lifecycle/db-overrides.js — the grader override-request flow (builder asks an Archon to confirm anyway).
2
+ //
3
+ // Carved out of modules/lifecycle/db.js by task 1003204 (goal 1000079, the R03
4
+ // de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
5
+ // original db.js lines 4401-4560 unchanged. db.js remains the facade that
6
+ // re-exports every name, so every existing db call site is unchanged.
7
+ //
8
+ // THE BOUNDARY (ADR 0093 §1): this file reaches core ONLY through the published
9
+ // doorway ../../src/module-api.js — never a core internal, never a sibling module.
10
+
11
+ 'use strict';
12
+
13
+ const api = require('../../src/module-api');
14
+ const { pool, withTx } = api;
15
+ const { mostRecentClaimHolderId } = require('./db-shared.js');
16
+
17
+ // task override requests — V3.R74 (#293). Rare path: a builder whose ship was
18
+ // rejected by the subagent grader (task stays at 'completed', no credits)
19
+ // believes the grader was wrong and asks an Archon to confirm anyway.
20
+
21
+
22
+ async function createOverrideRequest({ taskId, requesterId, rationale }) {
23
+ const { rows: taskRows } = await pool.query(
24
+ `SELECT id, status FROM tasks WHERE id = $1`,
25
+ [taskId]
26
+ );
27
+ if (taskRows.length === 0) {
28
+ throw Object.assign(new Error('task not found'), { code: 'TASK_NOT_FOUND' });
29
+ }
30
+ if (taskRows[0].status !== 'completed') {
31
+ throw Object.assign(new Error('task not in completed state'), {
32
+ code: 'TASK_NOT_COMPLETED',
33
+ currentStatus: taskRows[0].status,
34
+ });
35
+ }
36
+ try {
37
+ const { rows } = await pool.query(
38
+ `INSERT INTO task_override_requests (task_id, requested_by_builder_id, rationale)
39
+ VALUES ($1, $2, $3)
40
+ RETURNING id, task_id, requested_by_builder_id, rationale, status, created_at`,
41
+ [taskId, requesterId, rationale]
42
+ );
43
+ return rows[0];
44
+ } catch (err) {
45
+ // uniq_task_override_requests_open_per_task — one open request per task.
46
+ if (err && err.code === '23505') {
47
+ throw Object.assign(new Error('open request already exists for this task'), {
48
+ code: 'OPEN_REQUEST_EXISTS',
49
+ });
50
+ }
51
+ throw err;
52
+ }
53
+ }
54
+
55
+
56
+ async function listOverrideRequests({ status = 'open', limit = 50 } = {}) {
57
+ const { rows } = await pool.query(
58
+ `SELECT r.id, r.task_id, r.requested_by_builder_id, r.rationale, r.status,
59
+ r.decided_by_builder_id, r.decided_at, r.decision_note, r.created_at,
60
+ t.title AS task_title, t.status AS task_status, t.version_id, t.credits_reward,
61
+ b.github_login AS requester_login, b.display_name AS requester_display_name
62
+ FROM task_override_requests r
63
+ JOIN tasks t ON t.id = r.task_id
64
+ JOIN builders b ON b.id = r.requested_by_builder_id
65
+ WHERE r.status = $1
66
+ ORDER BY r.created_at DESC
67
+ LIMIT $2`,
68
+ [status, limit]
69
+ );
70
+ return rows;
71
+ }
72
+
73
+
74
+ async function decideOverrideRequest({ requestId, archonId, decision, note }, deps = {}) {
75
+ if (decision !== 'approve' && decision !== 'deny') {
76
+ throw Object.assign(new Error('bad decision'), { code: 'BAD_DECISION' });
77
+ }
78
+ // `deps.pool` is a TEST-ONLY injection seam (#538) — honored ONLY when
79
+ // NODE_ENV==='test' (not merely "not production"), so an unset NODE_ENV in a
80
+ // prod-like box can't accidentally activate it, and a future route that
81
+ // naively forwarded a caller-supplied pool could never swap the DB connection
82
+ // in prod (hacker review #538). It lets tests drive the txn with a fakeClient
83
+ // (same pattern as credit_grant.mjs). Production callers omit it → module pool.
84
+ const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
85
+ return withTx(async (client) => {
86
+ // RETURNING requested_by_builder_id so the approve path doesn't re-read the
87
+ // row it just locked (#538): the work was the ORIGINAL REQUESTER's, and both
88
+ // the credit AND the ship attribution must go to them.
89
+ const { rows } = await client.query(
90
+ `UPDATE task_override_requests
91
+ SET status = $1,
92
+ decided_by_builder_id = $2,
93
+ decided_at = now(),
94
+ decision_note = $3
95
+ WHERE id = $4 AND status = 'open'
96
+ RETURNING id, task_id, status, requested_by_builder_id`,
97
+ [decision === 'approve' ? 'approved' : 'denied', archonId, note ?? null, requestId]
98
+ );
99
+ if (rows.length === 0) {
100
+ throw Object.assign(new Error('request not open'), { code: 'REQUEST_NOT_OPEN' });
101
+ }
102
+ const row = rows[0];
103
+
104
+ let confirmResult = null;
105
+ if (decision === 'approve') {
106
+ // BOTH the credit AND the ship attribution (shipped_by, which drives the
107
+ // builders' hall + peer-vote feed) go to the builder who actually DID the
108
+ // work — the task's most-recent claim holder — never the approving Archon.
109
+ //
110
+ // (#538: previously shipped_by fell to the Archon via
111
+ // COALESCE(shipped_by, archonId), misattributing the ship even though the
112
+ // credit was routed correctly. That fix attributed everything to the
113
+ // override REQUESTER. SR-16 / #996 tightens it once more: with the new
114
+ // ownership gate on POST /tasks/:id/override-request the requester is
115
+ // already the claim holder OR an Archon, so resolving the CLAIM HOLDER
116
+ // here — the same attribution source applyGrade/confirmTask use via
117
+ // getLastClaimHolder — keeps the credit on the worker even when an Archon
118
+ // files the request on a builder's behalf. Falls back to the requester
119
+ // only when the task has no claim on record at all, so a legitimate
120
+ // degenerate edge — e.g. status set directly via SQL — is never refused.)
121
+ const creditedBuilderId = (await mostRecentClaimHolderId(client, row.task_id)) ?? row.requested_by_builder_id;
122
+ // requested_by_builder_id is NOT NULL in the schema, so this can only be
123
+ // null in a degenerate state. Rather than silently fall back to archonId —
124
+ // which would re-create the exact misattribution #538 fixed — refuse the
125
+ // approval if there is neither a claim holder nor a requester to credit.
126
+ // Rolls back so the override stays open for inspection.
127
+ if (creditedBuilderId == null) {
128
+ throw Object.assign(
129
+ new Error(`override request ${requestId} has no claim holder or requester to credit`),
130
+ { code: 'OVERRIDE_NO_REQUESTER' }
131
+ );
132
+ }
133
+
134
+ // Flip the task completed→confirmed and attribute the ship to the
135
+ // requester. Inlined here (rather than calling confirmTask) because
136
+ // confirmTask opens its own client/transaction; we want one atomic step.
137
+ const { rows: taskRows } = await client.query(
138
+ `UPDATE tasks
139
+ SET status = 'confirmed',
140
+ shipped_by = COALESCE(shipped_by, $2),
141
+ updated_at = now()
142
+ WHERE id = $1 AND status = 'completed'
143
+ RETURNING id, title, credits_reward, kind`,
144
+ [row.task_id, creditedBuilderId]
145
+ );
146
+ if (taskRows.length === 0) {
147
+ // Task was raced to another terminal state (e.g. someone manually
148
+ // confirmed it between the request and the decision). The approval row
149
+ // still stands as the audit signal; we just don't double-flip the task.
150
+ // creditedBuilderId is included for a consistent return shape so callers
151
+ // can always destructure it from confirmResult without a guard (#538).
152
+ confirmResult = { taskFlipped: false, creditsAwarded: 0, ideaBonus: null, creditedBuilderId };
153
+ } else {
154
+ // ADR 0120 "pay on land": the override-approval path flips the task
155
+ // completed→confirmed and attributes shipped_by to the claim holder
156
+ // (above), but the per-task credit + idea bonus now wait for the land
157
+ // (shipTask → awardLandingCredit, keyed 'task.shipped'). A stranded
158
+ // override-confirmed task pays nothing until its code is on `main`.
159
+ // creditsAwarded/ideaBonus stay in the return shape (0 / null) for a
160
+ // stable contract; thetes graduation was already ship-time (#704).
161
+ confirmResult = { taskFlipped: true, creditsAwarded: 0, ideaBonus: null, creditedBuilderId };
162
+ }
163
+ }
164
+
165
+ return { request: row, confirmResult };
166
+ }, { pool: activePool });
167
+ }
168
+
169
+ // Credit allocation carved into modules/economy/credits.js (BV1.R77 / ADR 0093
170
+ // §§1-2): KIND_MULTIPLIERS / IDEA_BONUS_FRACTION / REWARD_MARGIN_PCT,
171
+ // computeSessionTokenReward, applyKindMultiplier, insertMultipliedConfirmCredit,
172
+ // maybeAwardIdeaPromotionBonus, awardSessionTokenReward, and the net-negative
173
+ // (Subtractor) bonus all left this monolith. The lifecycle above (applyGrade /
174
+ // confirmTask / decideOverrideRequest / upsertSessionRecordWithReward) resolves
175
+ // them through the `reward` kernel port, passing its own tx client.
176
+
177
+ module.exports = {
178
+ createOverrideRequest,
179
+ decideOverrideRequest,
180
+ listOverrideRequests,
181
+ };