@bongos/core 1.19.319 → 1.19.320
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/.bongos-core.json +121 -36
- package/.claude/skills/merge-mode/SKILL.md +3 -1
- package/.devcontainer/init-firewall.sh +41 -5
- package/clients/bongos-client/examples/hello-world.mjs +11 -2
- package/docs/copy-inventory.md +20 -20
- package/docs/copy-registry.json +22 -22
- package/modules/hall-ui/public/builders.js +0 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/audit-replay.sh +20 -3
- package/scripts/gds/box.js +79 -86
- package/scripts/gds/db-backup-nightly.sh +17 -4
- package/scripts/gds/db-backup-offsite.sh +23 -6
- package/scripts/gds/discipline-modes.json +1 -1
- package/scripts/gds/doc-cli-guard.js +34 -2
- package/scripts/gds/fitness.js +8 -4
- package/scripts/gds/gds-db-backup.service +15 -4
- package/scripts/gds/gds-db-backup.timer +12 -1
- package/scripts/gds/gds-db-offsite.service +16 -5
- package/scripts/gds/gds-db-offsite.timer +12 -1
- package/scripts/gds/instance-apex.js +28 -0
- package/scripts/gds/newcomer-floor.templates.json +2 -2
- package/scripts/gds/provision.js +7 -3
- package/scripts/gds/ship-deploy-target.js +388 -0
- package/scripts/gds/ship-finish.js +377 -0
- package/scripts/gds/ship-flow.js +814 -0
- package/scripts/gds/ship-git.js +100 -0
- package/scripts/gds/ship-grade-steps.js +376 -0
- package/scripts/gds/ship-grade.js +319 -0
- package/scripts/gds/ship-io.js +100 -0
- package/scripts/gds/ship-land.js +539 -0
- package/scripts/gds/ship-memory-sync.js +192 -0
- package/scripts/gds/ship-merge.js +297 -0
- package/scripts/gds/ship-preflight-steps.js +323 -0
- package/scripts/gds/ship-regen.js +286 -0
- package/scripts/gds/ship-resume.js +151 -0
- package/scripts/gds/ship-session-upload.js +216 -0
- package/scripts/gds/ship-state.js +73 -0
- package/scripts/gds/ship-surprise.js +84 -0
- package/scripts/gds/ship.js +105 -4195
- package/scripts/status-probe.mjs +30 -2
- package/src/module-api.js +3 -1
- package/tests/helpers.mjs +13 -0
- package/tests/provision.mjs +7 -1
- package/tests/regrade_eligibility.mjs +43 -0
- package/tests/ship_assurance.mjs +4 -2
- package/tests/ship_publish_error.mjs +43 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// scripts/gds/ship-git.js — the shell/git/argv runners and the injection-safe merge-argv builders.
|
|
2
|
+
//
|
|
3
|
+
// Carved out of scripts/gds/ship.js by task 1003205 (goal 1000079, the R03
|
|
4
|
+
// de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
|
|
5
|
+
// original ship.js lines 560-638 unchanged. ship.js remains the facade that
|
|
6
|
+
// re-exports every name and owns the CLI entry point, so the ship contract and
|
|
7
|
+
// every flag are untouched, and the other scripts that import from it are too.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { execSync, execFileSync } = require('node:child_process');
|
|
12
|
+
|
|
13
|
+
function gitOk(cmd, opts = {}) {
|
|
14
|
+
try {
|
|
15
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...opts }).trim();
|
|
16
|
+
} catch (_) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function shellOk(cmd, opts = {}) {
|
|
22
|
+
try {
|
|
23
|
+
execSync(cmd, { stdio: 'inherit', ...opts });
|
|
24
|
+
return true;
|
|
25
|
+
} catch (_) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// argvOk / argvOut — the two no-shell exec primitives behind the git/gh wrappers.
|
|
31
|
+
// Run `<cmd> <args...>` so each arg reaches the program as a literal argv element
|
|
32
|
+
// (never interpolated into a shell string) — the discipline that closes the
|
|
33
|
+
// autoMerge injection (#681), since branch names + DB-sourced value_summary flow
|
|
34
|
+
// through here. `shell: false` is forced AFTER the opts spread so a caller can
|
|
35
|
+
// never pass `{ shell: true }` and silently re-enable shell interpretation — the
|
|
36
|
+
// no-shell guarantee is the whole point and must not be overridable.
|
|
37
|
+
// argvOk — exit status only → boolean (gitArgvOk / ghArgvOk). stdio 'inherit'.
|
|
38
|
+
// argvOut — capture trimmed stdout → string|null (ghJson / fetchDeployedVersion).
|
|
39
|
+
// Default stdio captures BOTH stdout and stderr; a caller that must
|
|
40
|
+
// DISCARD stderr passes its own `stdio: ['ignore','pipe','ignore']`.
|
|
41
|
+
function argvOk(cmd, args, opts = {}) {
|
|
42
|
+
try {
|
|
43
|
+
execFileSync(cmd, args, { stdio: 'inherit', ...opts, shell: false });
|
|
44
|
+
return true;
|
|
45
|
+
} catch (_) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function argvOut(cmd, args, opts = {}) {
|
|
51
|
+
try {
|
|
52
|
+
return execFileSync(cmd, args, {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
...opts,
|
|
56
|
+
shell: false,
|
|
57
|
+
}).trim();
|
|
58
|
+
} catch (_) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// gitArgvOk — the git boolean runner. The boolean/`stdio:'inherit'` contract
|
|
64
|
+
// matches shellOk so callers can swap one for the other; see buildMergeArgv + the
|
|
65
|
+
// #681 regression test.
|
|
66
|
+
function gitArgvOk(args, opts = {}) {
|
|
67
|
+
return argvOk('git', args, opts);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// sanitizeGitHubRefs — rewrite "#NNN" → "task NNN" in any text that lands on a
|
|
71
|
+
// GitHub-rendered surface that can't carry a markdown link (commit messages,
|
|
72
|
+
// PR titles). GitHub autolinks every bare "#NNN" there to its OWN PR/issue
|
|
73
|
+
// number space, which our GDS task ids long since diverged from — so a value
|
|
74
|
+
// summary like "#914 player straw hat" rendered a dead link to issues/914 on
|
|
75
|
+
// every merge commit (task 916). Pure + exported for the regression test.
|
|
76
|
+
function sanitizeGitHubRefs(text) {
|
|
77
|
+
return String(text == null ? '' : text).replace(/#(\d{1,5})\b/g, 'task $1');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// buildMergeArgv — construct the argv for the auto-merge commit. Pure +
|
|
81
|
+
// exported so the regression test can assert the untrusted value_summary lands
|
|
82
|
+
// as a single literal argv element (never spliced into a shell string). The
|
|
83
|
+
// merge message is `Merge <branch>: <summary>` with task refs de-#-ed (commit
|
|
84
|
+
// messages can't carry markdown links, so a bare "#NNN" would autolink to a
|
|
85
|
+
// dead GitHub issue — task 916); transport stays argv, never a shell string.
|
|
86
|
+
function buildMergeArgv(branch, valueSummary) {
|
|
87
|
+
const summary = sanitizeGitHubRefs(valueSummary) || `(no value summary)`;
|
|
88
|
+
const mergeMsg = `Merge ${branch}: ${summary}`;
|
|
89
|
+
return ['merge', '--no-ff', branch, '-m', mergeMsg];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
argvOk,
|
|
94
|
+
argvOut,
|
|
95
|
+
buildMergeArgv,
|
|
96
|
+
gitArgvOk,
|
|
97
|
+
gitOk,
|
|
98
|
+
sanitizeGitHubRefs,
|
|
99
|
+
shellOk,
|
|
100
|
+
};
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// scripts/gds/ship-grade-steps.js — the grade step shared by shipMain and regradeMain: payload, reuse, scoring, printing.
|
|
2
|
+
//
|
|
3
|
+
// Carved out of scripts/gds/ship.js by task 1003205 (goal 1000079, the R03
|
|
4
|
+
// de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
|
|
5
|
+
// original ship.js lines 2580-2927 unchanged. ship.js remains the facade that
|
|
6
|
+
// re-exports every name and owns the CLI entry point, so the ship contract and
|
|
7
|
+
// every flag are untouched, and the other scripts that import from it are too.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
const { getServerReward } = require('./ship-state.js');
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const { cliClient, rewardPolicy } = require('./cli-lib');
|
|
15
|
+
const { gitOk } = require('./ship-git.js');
|
|
16
|
+
const { resolveInstanceBase } = require('./ship-deploy-target.js');
|
|
17
|
+
const { computeRawDiffHash, reconstructPanelIssues, runSubagentGrader, shouldReuseGrade } = require('./ship-grade.js');
|
|
18
|
+
|
|
19
|
+
// ---------- Shared grade-step helpers (used by shipMain AND regradeMain) ----------
|
|
20
|
+
//
|
|
21
|
+
// #678: the grade step is factored out of shipMain so the `--regrade` recovery
|
|
22
|
+
// path can reuse it verbatim instead of replicating ~50 lines (the trap that
|
|
23
|
+
// learning #43 warned about — "runSubagentGrader in ship.js is NOT exported, so
|
|
24
|
+
// replicate its ~10 lines"). These helpers are behavior-preserving lifts of the
|
|
25
|
+
// inline blocks shipMain used before; both paths now share one implementation.
|
|
26
|
+
|
|
27
|
+
// Map a grader result into the POST /tasks/:id/grade request body. Forwards the
|
|
28
|
+
// advisory sub-scores (economy/security/efficiency) only when present so the
|
|
29
|
+
// server's validateAndScore re-records them (back-compat: omitted when null).
|
|
30
|
+
function buildGradePayload(gradeResult, committedFiles = null) {
|
|
31
|
+
return {
|
|
32
|
+
// touches[] cleanup [3/8] (task 878): pass the real committed changed-files
|
|
33
|
+
// so the server keys the shipper badges off the diff, not touches[].
|
|
34
|
+
...(Array.isArray(committedFiles) && committedFiles.length > 0
|
|
35
|
+
? { committed_files: committedFiles }
|
|
36
|
+
: {}),
|
|
37
|
+
subagent_output: {
|
|
38
|
+
functional_fidelity: gradeResult.rubric_scores?.functional_fidelity,
|
|
39
|
+
code_quality: gradeResult.rubric_scores?.code_quality,
|
|
40
|
+
...(gradeResult.rubric_scores?.economy != null
|
|
41
|
+
? { economy: gradeResult.rubric_scores.economy }
|
|
42
|
+
: {}),
|
|
43
|
+
...(gradeResult.rubric_scores?.security_score != null
|
|
44
|
+
? { security_score: gradeResult.rubric_scores.security_score }
|
|
45
|
+
: {}),
|
|
46
|
+
...(gradeResult.rubric_scores?.efficiency_score != null
|
|
47
|
+
? { efficiency_score: gradeResult.rubric_scores.efficiency_score }
|
|
48
|
+
: {}),
|
|
49
|
+
notes_md: gradeResult.notes_md,
|
|
50
|
+
confidence: gradeResult.signals?.confidence ?? null,
|
|
51
|
+
issues: gradeResult.issues,
|
|
52
|
+
},
|
|
53
|
+
signals: gradeResult.signals,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Log the grader panel's own API spend (#486 / ADR 0028 path 1). Best-effort +
|
|
58
|
+
// idempotent on (source, source_ref): a cost-log failure must never block a
|
|
59
|
+
// ship/re-grade, and re-grading at the same HEAD is a no-op.
|
|
60
|
+
async function logGraderPanelCost(taskId, gradeResult) {
|
|
61
|
+
const panelCostUsd = Number(gradeResult.panel_cost_usd) || 0;
|
|
62
|
+
if (panelCostUsd <= 0) return;
|
|
63
|
+
const headSha = gitOk('git rev-parse --short HEAD') || 'nohead';
|
|
64
|
+
const costPost = await (await cliClient()).cost.postCost({
|
|
65
|
+
body: {
|
|
66
|
+
task_id: Number(taskId),
|
|
67
|
+
amount_usd: panelCostUsd,
|
|
68
|
+
category: 'api',
|
|
69
|
+
source: 'grader',
|
|
70
|
+
source_ref: `grade-${taskId}-${headSha}`,
|
|
71
|
+
description: `grader panel (${gradeResult.grader_kind || 'subagent'}) on #${taskId}`,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
// An idempotent conflict returns HTTP 200 {skipped:true} (cost.js), so
|
|
75
|
+
// costPost.ok is TRUE on a skip — check `skipped` first.
|
|
76
|
+
if (costPost.data && costPost.data.skipped) {
|
|
77
|
+
console.log(` grader cost already logged for ${headSha} (idempotent skip)`);
|
|
78
|
+
} else if (costPost.ok) {
|
|
79
|
+
console.log(` grader cost logged: $${panelCostUsd.toFixed(4)} → cost_log (category=api, source=grader)`);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(` grader cost log skipped (non-fatal): HTTP ${costPost.status}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// task 1002662: assemble the delta-aware prior-round context for a --regrade.
|
|
86
|
+
// Pure (no git/network) so the payload shape is unit-tested. Returns null when
|
|
87
|
+
// there is no prior grade worth being delta-aware of; otherwise:
|
|
88
|
+
// forPanel — { round, findings_md, response_md, interim_md } for the worker
|
|
89
|
+
// prompts (fenced as untrusted by the shared scaffold);
|
|
90
|
+
// audit — the compact regrade block recorded into the new grade's signals
|
|
91
|
+
// so grade_attempts rounds read as remediation history (the
|
|
92
|
+
// task-1002661 reader consumes this).
|
|
93
|
+
const PRIOR_ROUND_TEXT_CAP = 4000;
|
|
94
|
+
function buildPriorRoundContext({ storedGrade, responseText = '', interimCommits = '', interimDiffStat = '' }) {
|
|
95
|
+
if (!storedGrade || typeof storedGrade !== 'object') return null;
|
|
96
|
+
const issues = Array.isArray(storedGrade.issues) ? storedGrade.issues : [];
|
|
97
|
+
const notes = typeof storedGrade.notes_md === 'string' ? storedGrade.notes_md.trim() : '';
|
|
98
|
+
if (issues.length === 0 && !notes) return null;
|
|
99
|
+
const verdictLine = `Prior verdict: ${storedGrade.passed ? 'PASS' : 'FAIL'} · score ${storedGrade.score ?? '?'}/10 · ${issues.length} finding(s)`;
|
|
100
|
+
const findingLines = issues.map((it) => `- [${it.severity || '?'}] ${it.worker || 'panel'} · ${it.file || '?'} — ${it.summary || ''}`);
|
|
101
|
+
const findings_md = [
|
|
102
|
+
verdictLine,
|
|
103
|
+
'',
|
|
104
|
+
...(findingLines.length ? findingLines : ['(no per-finding rows — see the prior notes below)']),
|
|
105
|
+
...(notes ? ['', 'Prior panel notes:', notes.slice(0, PRIOR_ROUND_TEXT_CAP)] : []),
|
|
106
|
+
].join('\n');
|
|
107
|
+
const interimParts = [];
|
|
108
|
+
if (interimCommits) interimParts.push('Interim commits since the prior grade:', String(interimCommits).slice(0, PRIOR_ROUND_TEXT_CAP));
|
|
109
|
+
if (interimDiffStat) interimParts.push('', 'Interim diffstat:', String(interimDiffStat).slice(0, PRIOR_ROUND_TEXT_CAP));
|
|
110
|
+
const response = String(responseText || '').trim();
|
|
111
|
+
// Round numbering is self-consistent across regrades: the first grade is
|
|
112
|
+
// round 1; each regrade reads the prior signals.regrade.round when present.
|
|
113
|
+
const round = (Number(storedGrade.signals?.regrade?.round) || 1) + 1;
|
|
114
|
+
return {
|
|
115
|
+
forPanel: {
|
|
116
|
+
round,
|
|
117
|
+
findings_md,
|
|
118
|
+
response_md: response ? response.slice(0, PRIOR_ROUND_TEXT_CAP) : null,
|
|
119
|
+
interim_md: interimParts.length ? interimParts.join('\n') : null,
|
|
120
|
+
},
|
|
121
|
+
audit: {
|
|
122
|
+
round,
|
|
123
|
+
prior_passed: storedGrade.passed === true,
|
|
124
|
+
prior_score: storedGrade.score ?? null,
|
|
125
|
+
prior_findings_count: issues.length,
|
|
126
|
+
response_present: Boolean(response),
|
|
127
|
+
interim_present: Boolean(interimCommits || interimDiffStat),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Run the subagent grader against the committed diff, POST the result to the
|
|
133
|
+
// rank-open /tasks/:id/grade endpoint, and log the panel cost on success.
|
|
134
|
+
// Returns the raw apiCall response so the caller can branch on .ok / read the
|
|
135
|
+
// grade. Shared by the ship path and the --regrade path.
|
|
136
|
+
//
|
|
137
|
+
// V4.R08: before spawning the adversarial panel we compute the raw diff hash
|
|
138
|
+
// and check whether a stored PASS grade exists for the same hash. On a match
|
|
139
|
+
// we re-POST the stored grade (marking it reused in signals) and skip the
|
|
140
|
+
// panel entirely — cheap re-ships with an unchanged diff stay fast.
|
|
141
|
+
// task 1002660: the COMPUTE half — the reuse lookup + the panel run. No server
|
|
142
|
+
// writes, so shipMain can start it before the smoke gate and overlap the
|
|
143
|
+
// multi-minute panel with smoke + the claim resolve. Returns
|
|
144
|
+
// { kind: 'reuse', currentHash, reusedResult } | { kind: 'fresh', currentHash, gradeResult }.
|
|
145
|
+
async function computeGradeResult({ taskId, task, summary, baseline, committedFiles, priorRound = null }) {
|
|
146
|
+
// Compute the fingerprint of the raw (untruncated) diff so we can compare
|
|
147
|
+
// against any stored grade, independent of the display-truncation cap.
|
|
148
|
+
const currentHash = computeRawDiffHash(baseline);
|
|
149
|
+
|
|
150
|
+
if (currentHash) {
|
|
151
|
+
const storedGradeRes = await (await cliClient()).tasks.getTasksId({ id: taskId, query: { include: 'grade' } });
|
|
152
|
+
const storedGrade = storedGradeRes.ok ? (storedGradeRes.data.grade ?? null) : null;
|
|
153
|
+
|
|
154
|
+
if (shouldReuseGrade(storedGrade, currentHash)) {
|
|
155
|
+
console.log(' Grade reuse: stored PASS grade matches current diff hash — skipping panel.');
|
|
156
|
+
// Reuse the stored result with a reused marker so the audit trail is
|
|
157
|
+
// complete. We keep the original grader_kind and inject reuse metadata
|
|
158
|
+
// into signals so a bare table scan can spot reused grades.
|
|
159
|
+
const reusedSignals = {
|
|
160
|
+
...(storedGrade.signals || {}),
|
|
161
|
+
reused_from_diff_hash: storedGrade.diff_hash,
|
|
162
|
+
reused_at: new Date().toISOString(),
|
|
163
|
+
};
|
|
164
|
+
const reusedResult = {
|
|
165
|
+
passed: storedGrade.passed,
|
|
166
|
+
score: storedGrade.score,
|
|
167
|
+
threshold: storedGrade.threshold,
|
|
168
|
+
grader_kind: storedGrade.grader_kind,
|
|
169
|
+
grader_version: storedGrade.grader_version,
|
|
170
|
+
rubric_scores: storedGrade.rubric_scores,
|
|
171
|
+
notes_md: storedGrade.notes_md,
|
|
172
|
+
signals: reusedSignals,
|
|
173
|
+
// task 1002660: carry the original panel's issues (rebuilt from
|
|
174
|
+
// signals.workers[].issues — the row stores them nowhere else) so the
|
|
175
|
+
// reused PASS stands on the same substance/evidence it passed with.
|
|
176
|
+
issues: reconstructPanelIssues(storedGrade.signals),
|
|
177
|
+
};
|
|
178
|
+
return { kind: 'reuse', currentHash, reusedResult };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const gradeResult = await runSubagentGrader({
|
|
183
|
+
task,
|
|
184
|
+
valueSummary: summary,
|
|
185
|
+
baseline,
|
|
186
|
+
changedFiles: committedFiles,
|
|
187
|
+
priorRound: priorRound?.forPanel ?? null,
|
|
188
|
+
});
|
|
189
|
+
// task 1002662: record the regrade audit block on the new grade's signals so
|
|
190
|
+
// this round is readable as remediation history (grade_attempts keeps one row
|
|
191
|
+
// per round; the latest grade's signals.regrade names what it responded to).
|
|
192
|
+
if (priorRound?.audit) {
|
|
193
|
+
gradeResult.signals = { ...(gradeResult.signals || {}), regrade: priorRound.audit };
|
|
194
|
+
}
|
|
195
|
+
return { kind: 'fresh', currentHash, gradeResult };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The RECORD half: POST the computed grade + log the panel cost. `precomputed`
|
|
199
|
+
// (a promise from computeGradeResult, task 1002660) lets the ship path hand in
|
|
200
|
+
// the overlapped computation; the --regrade path passes nothing and computes
|
|
201
|
+
// inline — behavior identical to the pre-split gradeAndRecord either way.
|
|
202
|
+
async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles, priorRound = null, precomputed = null }) {
|
|
203
|
+
const computed = await (precomputed
|
|
204
|
+
|| computeGradeResult({ taskId, task, summary, baseline, committedFiles, priorRound }));
|
|
205
|
+
|
|
206
|
+
if (computed.kind === 'reuse') {
|
|
207
|
+
const payload = { ...buildGradePayload(computed.reusedResult, committedFiles), diff_hash: computed.currentHash };
|
|
208
|
+
return (await cliClient()).tasks.postTasksIdGrade({ id: taskId, body: payload });
|
|
209
|
+
// No logGraderPanelCost — the panel didn't run, there's no cost to log.
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const { gradeResult } = computed;
|
|
213
|
+
const payload = { ...buildGradePayload(gradeResult, committedFiles), diff_hash: computed.currentHash ?? undefined };
|
|
214
|
+
const gradePost = await (await cliClient()).tasks.postTasksIdGrade({ id: taskId, body: payload });
|
|
215
|
+
// Log the panel's cost once the grade is recorded — independent of the
|
|
216
|
+
// pass/fail VERDICT (the panel ran, and cost money, either way), but only
|
|
217
|
+
// when the POST itself succeeded (a failed POST means nothing was recorded).
|
|
218
|
+
if (gradePost.ok) await logGraderPanelCost(taskId, gradeResult);
|
|
219
|
+
return gradePost;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Build the grade verdict display block as a string (per-axis scores + notes +
|
|
223
|
+
// issues + next-step hints). Pure + exported so the #943 grader-unavailable vs
|
|
224
|
+
// FAIL branch is unit-tested without capturing stdout. printGradeOutcome just
|
|
225
|
+
// prints what this returns.
|
|
226
|
+
function formatGradeOutcome(g, taskId) {
|
|
227
|
+
const out = [];
|
|
228
|
+
// #943: a TOTAL grader outage (every gating worker failed to spawn / the
|
|
229
|
+
// grader was bypassed pre-spawn) is NOT a quality rejection — it's "the grader
|
|
230
|
+
// could not run". Present it distinctly so a worker-infra failure never reaches
|
|
231
|
+
// the builder as a misleading "FAIL · 0/10 — fix the issues" (the session-1659
|
|
232
|
+
// false-FAIL). There are no issues to fix; the path forward is re-grade when the
|
|
233
|
+
// grader is reachable, or manual-confirm.
|
|
234
|
+
if (g.signals?.panel_outcome === 'unavailable') {
|
|
235
|
+
const reason = g.signals?.grader_unavailable_reason;
|
|
236
|
+
out.push('');
|
|
237
|
+
out.push(`Subagent grade: GRADER UNAVAILABLE${reason ? ` (${reason})` : ''} · no quality signal produced — NOT a quality rejection`);
|
|
238
|
+
if (g.notes_md) out.push(g.notes_md.split('\n').map((l) => ' ' + l).join('\n'));
|
|
239
|
+
out.push('');
|
|
240
|
+
out.push('The grader could not run (infra/outage/bypass) — there are no findings to fix. Task stays at completed.');
|
|
241
|
+
out.push('When the grader is reachable again, re-grade:');
|
|
242
|
+
out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
|
|
243
|
+
out.push('Or land it now via manual-confirm:');
|
|
244
|
+
out.push(` curl -X POST -H "Authorization: Bearer <token>" ${resolveInstanceBase() || '<instance-origin>'}/api/gds/tasks/${taskId}/confirm`);
|
|
245
|
+
return out.join('\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// task 1002614 (map P-2): a protected-path refusal used to render as an
|
|
249
|
+
// ordinary quality FAIL — "fix the issues and --regrade" — although no rank of
|
|
250
|
+
// the builder's can land that diff. Present it as the authorization wall it is
|
|
251
|
+
// (the #943 grader-unavailable branch is the precedent for a distinct shape).
|
|
252
|
+
// Keyed off the guard name so it also reads grades from servers predating the
|
|
253
|
+
// authorization_refusal marker.
|
|
254
|
+
const guardTripped = g.signals?.server_guard?.tripped || [];
|
|
255
|
+
const permRefusal = g.signals?.server_guard?.authorization_refusal === true
|
|
256
|
+
|| guardTripped.some((t) => t.guard === 'permission_path');
|
|
257
|
+
if (!g.passed && permRefusal) {
|
|
258
|
+
out.push('');
|
|
259
|
+
out.push('Subagent grade: REFUSED (authorization) · not a quality verdict — your work was not judged and rejected');
|
|
260
|
+
out.push('');
|
|
261
|
+
out.push('The committed diff touches PROTECTED surfaces above your rank, so the server');
|
|
262
|
+
out.push('refused to accept it — no re-attempt at your rank can land this diff as-is.');
|
|
263
|
+
if ((g.issues || []).length > 0) {
|
|
264
|
+
out.push('');
|
|
265
|
+
out.push('What was refused (file · rule):');
|
|
266
|
+
for (const it of g.issues.filter((i) => String(i.worker || '').includes('permission-path'))) {
|
|
267
|
+
out.push(` ${it.file || '?'} — ${it.summary || ''}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
out.push('');
|
|
271
|
+
out.push('Ways forward:');
|
|
272
|
+
out.push(' - Split the protected files out of your diff and re-ship the rest, or');
|
|
273
|
+
out.push(' - hand this task to a builder at/above the floor the refusal names.');
|
|
274
|
+
out.push(`(--regrade re-runs the same server check and will refuse again unless the diff changes.)`);
|
|
275
|
+
return out.join('\n');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const verdict = g.passed ? 'PASS' : 'FAIL';
|
|
279
|
+
out.push('');
|
|
280
|
+
out.push(`Subagent grade: ${verdict} · blended ${g.score}/10 · threshold ${g.threshold} (both axes)`);
|
|
281
|
+
if (g.rubric_scores) {
|
|
282
|
+
const ff = g.rubric_scores.functional_fidelity;
|
|
283
|
+
const cq = g.rubric_scores.code_quality;
|
|
284
|
+
if (ff != null) out.push(` functional_fidelity: ${ff}/10`);
|
|
285
|
+
if (cq != null) out.push(` code_quality: ${cq}/10`);
|
|
286
|
+
}
|
|
287
|
+
if (g.notes_md) out.push(g.notes_md.split('\n').map((l) => ' ' + l).join('\n'));
|
|
288
|
+
if ((g.issues || []).length > 0) {
|
|
289
|
+
out.push('');
|
|
290
|
+
out.push(`Issues (${g.issues.length}):`);
|
|
291
|
+
for (const it of g.issues) {
|
|
292
|
+
const sev = String(it.severity || '?').padEnd(7);
|
|
293
|
+
out.push(` [${sev}] ${it.file || '?'} — ${it.summary || ''}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!g.passed) {
|
|
297
|
+
out.push('');
|
|
298
|
+
out.push('Grade failed — task stays at completed. Fix the issues above, commit, then re-grade:');
|
|
299
|
+
out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
|
|
300
|
+
out.push('Last resort (only if the grade was wrong):');
|
|
301
|
+
out.push(` curl -X POST -H "Authorization: Bearer <token>" ${resolveInstanceBase() || '<instance-origin>'}/api/gds/tasks/${taskId}/confirm`);
|
|
302
|
+
}
|
|
303
|
+
return out.join('\n');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Print the grade verdict + per-axis scores + notes + issues. On a FAIL, print
|
|
307
|
+
// both the re-grade hint (the cheap default) and the Archon override curl (the
|
|
308
|
+
// last resort — see feedback_prefer_regrade_over_override). #943: a total grader
|
|
309
|
+
// outage prints as "GRADER UNAVAILABLE", not a 0/10 FAIL — see formatGradeOutcome.
|
|
310
|
+
function printGradeOutcome(g, taskId) {
|
|
311
|
+
console.log(formatGradeOutcome(g, taskId));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Print the credits line + multiplier breakdown + idea bonus + unlocked
|
|
315
|
+
// achievements from a grade/confirm response.
|
|
316
|
+
function printCreditsAndAchievements({ postGradeStatus, credits, creditsRaw, creditsMultiplier, ideaBonusInfo, justUnlocked }) {
|
|
317
|
+
console.log('');
|
|
318
|
+
if (postGradeStatus === 'completed') {
|
|
319
|
+
console.log(`Credits awarded: ${credits} (none — task at completed, grade not passed)`);
|
|
320
|
+
} else if (postGradeStatus === 'confirmed') {
|
|
321
|
+
console.log(`Credits awarded: ${credits} (none yet — credits land when the task merges to main)`);
|
|
322
|
+
} else if (!rewardPolicy(getServerReward()).perTaskCredit) {
|
|
323
|
+
// cost-plus-only (ADR 0146): the per-task estimate is never paid, so this is
|
|
324
|
+
// 0 BY DESIGN. Left unexplained it reads as a broken payout — it sent a real
|
|
325
|
+
// session diagnosing the reward gate on 2026-07-26 (task 1002469).
|
|
326
|
+
console.log(`Credits awarded: ${credits} — expected: this instance pays cost-plus only.`);
|
|
327
|
+
console.log(" The task's estimate is a guide, never a payout. You're paid for this session's");
|
|
328
|
+
console.log(' real cost + 20%, booked separately from your token spend.');
|
|
329
|
+
} else {
|
|
330
|
+
console.log(`Credits awarded: ${credits}`);
|
|
331
|
+
}
|
|
332
|
+
// Write the credits flag so the NEXT Claude turn plays the credits-awarded
|
|
333
|
+
// sound via the Stop hook (task 582). Only when credits were actually awarded.
|
|
334
|
+
if (typeof credits === 'number' && credits > 0) {
|
|
335
|
+
const tmpDir = process.platform === 'win32'
|
|
336
|
+
? (process.env.TEMP || process.env.TMP || os.tmpdir()) : '/tmp';
|
|
337
|
+
try { fs.writeFileSync(require('path').join(tmpDir, 'otb-credits'), ''); } catch (_) {}
|
|
338
|
+
}
|
|
339
|
+
if (creditsRaw != null && creditsMultiplier != null && Number(creditsMultiplier) !== 1.0) {
|
|
340
|
+
const mult = Number(creditsMultiplier).toFixed(2).replace(/0$/, '').replace(/\.$/, '.0');
|
|
341
|
+
console.log(` Kind multiplier: ${creditsRaw}c × ${mult}× → ${credits}c`);
|
|
342
|
+
}
|
|
343
|
+
if (ideaBonusInfo && ideaBonusInfo.ideaBonusAwarded > 0) {
|
|
344
|
+
console.log(
|
|
345
|
+
` Idea-promotion bonus: +${ideaBonusInfo.ideaBonusAwarded}c paid to builder #${ideaBonusInfo.bonusBuilderId} (idea #${ideaBonusInfo.ideaId})`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
for (const ach of justUnlocked || []) {
|
|
349
|
+
const bonus = ach.credit_bonus > 0 ? ` (+${ach.credit_bonus}c bonus)` : '';
|
|
350
|
+
console.log(`✨ Achievement unlocked: ${ach.name}${bonus}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// chooseBroadcastTargets — the ship-broadcast channel router (task 990) now lives
|
|
355
|
+
// in src/bongos/ship-broadcast.js (task 1045) so the server-mediated publish path
|
|
356
|
+
// shares the exact same security-sensitive routing. Imported at the top of this
|
|
357
|
+
// file; still re-exported below for tests/ship_broadcast_routing.mjs.
|
|
358
|
+
|
|
359
|
+
// task 1442: play the task-shipped "ship bell". ship.js runs as its own process
|
|
360
|
+
// (not inside a Claude turn), so it plays the sound directly rather than via the
|
|
361
|
+
// Stop hook. Called at the confirmed/shipped success milestone so it rings on
|
|
362
|
+
// the ci/async landing path too — there ship.js exits at 'confirmed' and the
|
|
363
|
+
// deploy finishes later server-side, which is exactly why the old post-deploy
|
|
364
|
+
// call site never fired (bug #1442). Resolve the builder's chosen variant from
|
|
365
|
+
// the local prefs file; fall back to 'ship'. Best-effort: playSound swallows all
|
|
366
|
+
// errors and is a silent no-op with no audio player, so this can never fail a ship.
|
|
367
|
+
|
|
368
|
+
module.exports = {
|
|
369
|
+
buildGradePayload,
|
|
370
|
+
buildPriorRoundContext,
|
|
371
|
+
computeGradeResult,
|
|
372
|
+
formatGradeOutcome,
|
|
373
|
+
gradeAndRecord,
|
|
374
|
+
printCreditsAndAchievements,
|
|
375
|
+
printGradeOutcome,
|
|
376
|
+
};
|