@bongos/core 1.19.1031 → 1.19.1033
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 +167 -57
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grader-health/SKILL.md +1 -1
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +26 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/0338-modules-travel-through-a-bongos-hosted-store.md +119 -0
- package/docs/adr/README.md +4 -3
- package/docs/architecture.md +1 -1
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +57 -9
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/db-claim-reads.js +8 -2
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7411 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +81 -1
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/provision-core-upgrade.js +128 -77
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-land.js +86 -7
- package/scripts/gds/ship-preflight-steps.js +56 -3
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +6 -4
- package/src/module-api.js +1 -1
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/core_upgrade_runner.mjs +89 -1
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/grade_diff_hash.mjs +77 -2
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_preflight.mjs +160 -3
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -31,7 +31,9 @@ DB="${PGDATABASE:-cloudbongos}"
|
|
|
31
31
|
CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
32
32
|
case "$CORE_DIR" in */node_modules/@bongos/core) INSTANCE_DIR="${INSTANCE_DIR:-${CORE_DIR%/node_modules/@bongos/core}}" ;; esac
|
|
33
33
|
[ -n "${INSTANCE_DIR:-}" ] || { echo "set INSTANCE_DIR to the control plane's instance checkout — this sweep is not running from one"; exit 1; }
|
|
34
|
-
|
|
34
|
+
# Owner by name: GNU stat spells it `-c %U`, BSD/macOS stat `-f %Su`, and each refuses the
|
|
35
|
+
# other's flag, so GNU is asked first and BSD answers when it refuses (task 1004170).
|
|
36
|
+
APP_USER="${PROVISION_APP_USER:-$(stat -c %U "$INSTANCE_DIR" 2>/dev/null || stat -f %Su "$INSTANCE_DIR" 2>/dev/null)}"
|
|
35
37
|
if [ "$APP_USER" = root ] || ! [[ "$APP_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then
|
|
36
38
|
echo "cannot use '$APP_USER' as the app account — set PROVISION_APP_USER to the unprivileged account that owns $INSTANCE_DIR"; exit 1
|
|
37
39
|
fi
|
|
@@ -94,6 +94,56 @@ function logPath() {
|
|
|
94
94
|
try { return require('../../src/instance-config.js').configPath('autobongos-runs.jsonl'); }
|
|
95
95
|
catch (_) { return path.join(REPO_ROOT, 'autobongos-runs.jsonl'); }
|
|
96
96
|
}
|
|
97
|
+
// countWorkedSince — how many tasks this runner has worked since an epoch-second
|
|
98
|
+
// mark. Reads the run log, because that is the only record that survives a
|
|
99
|
+
// restart, and the cap it feeds has to hold across one (a runner that forgets
|
|
100
|
+
// its count on every crash has no cap at all).
|
|
101
|
+
//
|
|
102
|
+
// BOUNDED ON BOTH AXES, because this runs every iteration of a loop that never
|
|
103
|
+
// exits and the log only grows. It reads the TAIL rather than the file, and it
|
|
104
|
+
// scans BACKWARDS and stops at the first row older than the mark — the rows that
|
|
105
|
+
// can possibly count are the newest ones, so the common case touches a handful
|
|
106
|
+
// of lines whatever the log's size.
|
|
107
|
+
//
|
|
108
|
+
// A torn or missing log counts as zero rather than refusing: the fence and the
|
|
109
|
+
// gauge are the gates, this is a bound on top of them, and a bound that bricks
|
|
110
|
+
// the runner when its own log is unreadable is worse than one that occasionally
|
|
111
|
+
// allows an extra task.
|
|
112
|
+
const WORKED_SCAN_TAIL_BYTES = 1024 * 1024;
|
|
113
|
+
const NEWLINE = String.fromCharCode(10);
|
|
114
|
+
|
|
115
|
+
function countWorkedSince(sinceEpochS) {
|
|
116
|
+
if (!Number.isFinite(sinceEpochS) || sinceEpochS <= 0) return 0;
|
|
117
|
+
let text;
|
|
118
|
+
try {
|
|
119
|
+
const p = logPath();
|
|
120
|
+
const { size } = fs.statSync(p);
|
|
121
|
+
const from = Math.max(0, size - WORKED_SCAN_TAIL_BYTES);
|
|
122
|
+
const fd = fs.openSync(p, 'r');
|
|
123
|
+
try {
|
|
124
|
+
const buf = Buffer.alloc(Math.min(size, WORKED_SCAN_TAIL_BYTES));
|
|
125
|
+
fs.readSync(fd, buf, 0, buf.length, from);
|
|
126
|
+
text = buf.toString('utf8');
|
|
127
|
+
} finally { fs.closeSync(fd); }
|
|
128
|
+
// A tail read can land mid-line; drop the first partial one.
|
|
129
|
+
if (from > 0) text = text.slice(text.indexOf(NEWLINE) + 1);
|
|
130
|
+
} catch (_) { return 0; }
|
|
131
|
+
|
|
132
|
+
const lines = text.split(NEWLINE);
|
|
133
|
+
let n = 0;
|
|
134
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
135
|
+
const line = lines[i];
|
|
136
|
+
if (!line.trim()) continue;
|
|
137
|
+
let row;
|
|
138
|
+
try { row = JSON.parse(line); } catch (_) { continue; } // a torn line costs one row, not the count
|
|
139
|
+
const at = Date.parse(row.at);
|
|
140
|
+
if (!Number.isFinite(at)) continue;
|
|
141
|
+
if (at / 1000 < sinceEpochS) break; // the log is append-ordered: nothing older can count
|
|
142
|
+
if (row.event === 'worked') n += 1;
|
|
143
|
+
}
|
|
144
|
+
return n;
|
|
145
|
+
}
|
|
146
|
+
|
|
97
147
|
function record(entry) {
|
|
98
148
|
const row = { at: new Date().toISOString(), ...entry };
|
|
99
149
|
try {
|
|
@@ -342,6 +392,36 @@ async function iteration(opts, deps = {}) {
|
|
|
342
392
|
return log({ event: 'hold', go: false, unknown: !!g.decision.unknown, reason: g.decision.reason, sleep_until: g.decision.sleepUntil ?? null });
|
|
343
393
|
}
|
|
344
394
|
|
|
395
|
+
// 1b. A DERIVED gauge buys a BOUNDED amount of work (task 1004178).
|
|
396
|
+
//
|
|
397
|
+
// `derived` means the last real reading's window had already reset, so the
|
|
398
|
+
// baseline is a retired observation rather than a measurement. That is sound
|
|
399
|
+
// for the FIVE-hour window, which turns over during a night. It does nothing
|
|
400
|
+
// for the SEVEN-day one, which does not — so the weekly pace check, the thing
|
|
401
|
+
// the module's notes call what actually binds a heavy week, is simply absent
|
|
402
|
+
// on a derived reading. A stale weekly figure cannot substitute: usage only
|
|
403
|
+
// grows, so an under-reported value makes the check more permissive, and that
|
|
404
|
+
// is the wrong direction to be wrong in.
|
|
405
|
+
//
|
|
406
|
+
// So the bound is a task count, not a percentage: on one derived window, do
|
|
407
|
+
// AUTOBONGOS_DERIVED_TASK_CAP tasks and then hold until a real reading exists.
|
|
408
|
+
// A terminal session writes one; the owner opening a terminal in the morning
|
|
409
|
+
// is what lifts it. Default 2 — each worker is wall-clock bounded at 90
|
|
410
|
+
// minutes, so two of them is well inside a five-hour window even at worst, and
|
|
411
|
+
// the cap only has to be small enough that being wrong is survivable.
|
|
412
|
+
if (g.decision.derived) {
|
|
413
|
+
const cap = Number(process.env.AUTOBONGOS_DERIVED_TASK_CAP ?? 2);
|
|
414
|
+
const since = Number(g.decision.derivedSince) || 0;
|
|
415
|
+
const done = countWorkedSince(since);
|
|
416
|
+
if (Number.isFinite(cap) && cap >= 0 && done >= cap) {
|
|
417
|
+
return log({
|
|
418
|
+
event: 'hold', go: false, unknown: false, derived: true, worked_since_derived: done,
|
|
419
|
+
reason: `derived gauge: ${done} task(s) already done since the window reset and the cap is ${cap} — holding for a real reading (a terminal session writes one)`,
|
|
420
|
+
sleep_until: null,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
345
425
|
// 2. Pick — from the ALLOWLISTED goals, never from what the command line asked
|
|
346
426
|
// for. gate.goals is already the intersection.
|
|
347
427
|
const picked = await pick(gate.goals, deps.pickDeps || {});
|
|
@@ -798,7 +878,7 @@ if (require.main === module) {
|
|
|
798
878
|
|
|
799
879
|
module.exports = {
|
|
800
880
|
readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW, resolveWorkerBin,
|
|
801
|
-
worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX,
|
|
881
|
+
worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX, countWorkedSince,
|
|
802
882
|
heartbeatPath, writeHeartbeat, readHeartbeat, pidAlive, anotherRunnerIsAlive, HEARTBEAT_STALE_MS,
|
|
803
883
|
isRunnerTree, RUNNER_TREE_RE, postHeartbeat, RUNNER_STARTED_AT,
|
|
804
884
|
};
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// anybody's diff. Hard-failing a PR because ambient debt turned eight weeks old
|
|
41
41
|
// punishes whoever pushed next and gets the gate disabled. The audit asked for
|
|
42
42
|
// "the repeatable, SCHEDULED version" of its own manual review, and the patrol
|
|
43
|
-
// is that: history-wide, time-based, filed for adjudication (ADR
|
|
43
|
+
// is that: history-wide, time-based, filed for adjudication (ADR 0158 §2).
|
|
44
44
|
//
|
|
45
45
|
// AGE COMES FROM GIT, NOT FROM A STORED TIMESTAMP. A `since:` field in the JSON
|
|
46
46
|
// would be faster and hermetic, and it would also be the thing that silently
|
|
@@ -131,7 +131,10 @@ pg_dump \
|
|
|
131
131
|
# A SIGKILL mid-stream leaves a .partial that the next run will overwrite.
|
|
132
132
|
mv "$OUT.partial" "$OUT"
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
# Size in bytes via POSIX `wc -c`, not `stat -c '%s'`: that is GNU-only, and BSD/macOS
|
|
135
|
+
# stat refuses it outright, so every run on a Mac died here (task 1004170). tr drops the
|
|
136
|
+
# padding BSD wc prints in front of the number.
|
|
137
|
+
BYTES=$(wc -c < "$OUT" | tr -d '[:space:]')
|
|
135
138
|
echo "[$(date -u +%FT%TZ)] gds-db-backup: wrote ${OUT} (${BYTES} bytes)"
|
|
136
139
|
|
|
137
140
|
# Completeness guard (incident 2026-07-02, task 1711; task 1003925). pipefail already
|
|
@@ -190,7 +193,9 @@ ATTACH_OUT="${BACKUP_DIR}/${DB_NAME}-attachments-${TS}.tar.gz"
|
|
|
190
193
|
ATTACH_GLOB="${DB_NAME}-attachments-${D}${D}${D}${D}-${D}${D}-${D}${D}T${D}${D}${D}${D}${D}${D}Z.tar.gz"
|
|
191
194
|
|
|
192
195
|
if [ -d "$ATTACH_DIR" ]; then
|
|
193
|
-
|
|
196
|
+
# Every count here goes through tr for the same reason as BYTES: BSD wc pads it, which
|
|
197
|
+
# put "pruned 1 dump(s)" in a Mac's log where Linux writes "pruned 1 dump(s)".
|
|
198
|
+
ATTACH_COUNT=$(find "$ATTACH_DIR" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')
|
|
194
199
|
if [ "$ATTACH_COUNT" -gt 0 ]; then
|
|
195
200
|
if tar -czf "$ATTACH_OUT.partial" -C "$ATTACH_DIR" . 2>/dev/null; then
|
|
196
201
|
mv "$ATTACH_OUT.partial" "$ATTACH_OUT"
|
|
@@ -215,7 +220,7 @@ fi
|
|
|
215
220
|
PRUNED=$(find "$BACKUP_DIR" -maxdepth 1 -type f \
|
|
216
221
|
\( -name "$DUMP_GLOB" -o -name "$ATTACH_GLOB" \) \
|
|
217
222
|
-mtime +"${RETENTION_DAYS}" \
|
|
218
|
-
-print -delete | wc -l)
|
|
223
|
+
-print -delete | wc -l | tr -d '[:space:]')
|
|
219
224
|
|
|
220
225
|
if [ "$PRUNED" -gt 0 ]; then
|
|
221
226
|
echo "[$(date -u +%FT%TZ)] gds-db-backup: pruned ${PRUNED} dump(s) older than ${RETENTION_DAYS}d"
|
|
@@ -223,5 +228,5 @@ fi
|
|
|
223
228
|
|
|
224
229
|
# ---- emit a one-line summary for journald + monitoring ---------------------
|
|
225
230
|
|
|
226
|
-
REMAINING=$(find "$BACKUP_DIR" -maxdepth 1 -type f -name "$DUMP_GLOB" | wc -l)
|
|
231
|
+
REMAINING=$(find "$BACKUP_DIR" -maxdepth 1 -type f -name "$DUMP_GLOB" | wc -l | tr -d '[:space:]')
|
|
227
232
|
echo "[$(date -u +%FT%TZ)] gds-db-backup: ok · ${REMAINING} dump(s) retained · ${BYTES} bytes latest"
|
|
@@ -169,7 +169,9 @@ if [ -z "$LATEST" ]; then
|
|
|
169
169
|
fi
|
|
170
170
|
|
|
171
171
|
BASENAME=$(basename "$LATEST")
|
|
172
|
-
|
|
172
|
+
# Sizes via POSIX `wc -c`, never GNU-only `stat -c '%s'`, which BSD/macOS stat refuses
|
|
173
|
+
# (task 1004170). tr drops the padding BSD wc prints in front of the number.
|
|
174
|
+
BYTES=$(wc -c < "$LATEST" | tr -d '[:space:]')
|
|
173
175
|
|
|
174
176
|
# ---- encrypt before it leaves the box ---------------------------------------
|
|
175
177
|
#
|
|
@@ -189,7 +191,7 @@ fi
|
|
|
189
191
|
|
|
190
192
|
# A zero-byte or missing ciphertext would upload happily and restore to nothing.
|
|
191
193
|
# Checked here rather than trusted, because the failure is silent for a month.
|
|
192
|
-
ENC_BYTES=$(
|
|
194
|
+
ENC_BYTES=$(wc -c < "$ENC" | tr -d '[:space:]')
|
|
193
195
|
if [ "$ENC_BYTES" -lt 1 ]; then
|
|
194
196
|
echo "ERROR: encryption produced an empty file — refusing to upload it as a backup." >&2
|
|
195
197
|
exit 2
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
// 3. Per file, IN MEMORY (processCoreFile): DOCS (.md) are genericized + secret-scrubbed
|
|
35
35
|
// like the mirror; FUNCTIONAL files are kept VERBATIM and scanned only for an exact
|
|
36
36
|
// live-credential leak (brand/IP hits are non-fatal ADVISORIES, surfaced for a human).
|
|
37
|
+
// 3b. RELEASE NOTES (task 1004218): release-notes.json — what each release brought in,
|
|
38
|
+
// generated from the ref's HISTORY (scripts/gds/release-notes.js), its prose redacted
|
|
39
|
+
// exactly like a doc, and pinned like every other file. Skipped, with a line saying
|
|
40
|
+
// so, where there is no history to read (--from-worktree, a shallow clone).
|
|
37
41
|
// 4. NO-LEAK GATE (fail-closed): if any DOC leaks post-redaction, or any FUNCTIONAL file
|
|
38
42
|
// carries a live credential verbatim, PRODUCE NOTHING — dist/ is never touched. The
|
|
39
43
|
// in-memory pass makes the abort atomic (same discipline as the mirror export).
|
|
@@ -105,6 +109,12 @@ const {
|
|
|
105
109
|
const { collectEnvSecrets } = require(path.join(REPO_ROOT, 'modules/security/secret-scrub.js'));
|
|
106
110
|
const { redactBlob, parseLsTree } = require(path.join(REPO_ROOT, 'scripts/public-mirror-export.js'));
|
|
107
111
|
const { partitionHits, unusedEntries } = require(path.join(REPO_ROOT, 'scripts/gds/leak-scan-allowlist.js'));
|
|
112
|
+
// The release notes shipped beside package.json (task 1004218). Required by a literal path:
|
|
113
|
+
// the module is pure and requires nothing, so it adds no computed require to this file's
|
|
114
|
+
// executed closure (tests/executed_closure.mjs counts those).
|
|
115
|
+
const {
|
|
116
|
+
RELEASE_NOTES_FILE, RELEASE_NOTES_MAX_BYTES, generateReleaseNotes, boundReleaseNotes, serializeReleaseNotes,
|
|
117
|
+
} = require('./release-notes.js');
|
|
108
118
|
|
|
109
119
|
const MANIFEST_SCHEMA = 1;
|
|
110
120
|
const ARTIFACT_NAME = 'bongos-core';
|
|
@@ -734,6 +744,42 @@ function scanArtifactForLeaks(files, { literals = PRIOR_INSTANCE_LITERALS } = {}
|
|
|
734
744
|
return { literalHits, secretWarnings };
|
|
735
745
|
}
|
|
736
746
|
|
|
747
|
+
// release-notes.json — the one file the core carries that is derived from HISTORY rather
|
|
748
|
+
// than from a committed file (task 1004218). It only exists where the history does, which
|
|
749
|
+
// is the CI pack (publish.yml checks out at fetch-depth 0). main() stages it into the SAME
|
|
750
|
+
// staged/filesMeta arrays as package.json, so the pin manifest covers it and --self-verify
|
|
751
|
+
// reproduces it; a file written into the stage dir behind the manifest's back would fail
|
|
752
|
+
// self-verify at publish time.
|
|
753
|
+
//
|
|
754
|
+
// ITS PROSE IS TREATED AS A DOC. These are merge summaries — builder-authored sentences — so
|
|
755
|
+
// the neutral floor applies exactly as it does to a shipped .md: redacted with the same rules
|
|
756
|
+
// (redactBlob), and any residue after redaction is a fatal no-leak failure. Not optional:
|
|
757
|
+
// ten of the 1,399 summaries in the history (measured 2026-09-25) name a prior instance or
|
|
758
|
+
// its owner, and history cannot be edited — shipped verbatim, they would fail the fail-closed
|
|
759
|
+
// post-build scan on EVERY pack from now on, and the lane would never publish again. That
|
|
760
|
+
// scan still reads the finished file, as it reads every staged one.
|
|
761
|
+
//
|
|
762
|
+
// Pure: notes + the rules main() already built in, `{ buf, notes, dropped, summaries,
|
|
763
|
+
// redacted, leaks }` out.
|
|
764
|
+
function releaseNotesArtifact(notes, { rules, sensitiveLiterals, envSecrets, knownDocNames, maxBytes = RELEASE_NOTES_MAX_BYTES }) {
|
|
765
|
+
const leaks = [];
|
|
766
|
+
let redacted = 0;
|
|
767
|
+
const clean = {};
|
|
768
|
+
for (const [version, entries] of Object.entries(notes || {})) {
|
|
769
|
+
clean[version] = entries.map((e) => {
|
|
770
|
+
const r = redactBlob({ buf: Buffer.from(e.text, 'utf8'), rules, sensitiveLiterals, envSecrets, knownDocNames });
|
|
771
|
+
for (const l of r.leaks) leaks.push({ label: `doc:${l.label}`, sample: l.sample });
|
|
772
|
+
if (r.secret) leaks.push({ label: 'doc:secret-shape' });
|
|
773
|
+
const text = r.buf.toString('utf8');
|
|
774
|
+
if (text !== e.text) redacted++;
|
|
775
|
+
return e.id ? { id: e.id, text } : { text };
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
const bounded = boundReleaseNotes(clean, maxBytes);
|
|
779
|
+
const summaries = Object.values(bounded.notes).reduce((n, list) => n + list.length, 0);
|
|
780
|
+
return { buf: serializeReleaseNotes(bounded.notes), notes: bounded.notes, dropped: bounded.dropped, summaries, redacted, leaks };
|
|
781
|
+
}
|
|
782
|
+
|
|
737
783
|
// Self-verify the built artifact (task 1002218 / audit H3, findings F1+F5). The packager
|
|
738
784
|
// claims a consumer "verifies an install by recomputing [the pin] from the unpacked tree" —
|
|
739
785
|
// this proves the packager's OWN output actually does, at build time, on the produce side:
|
|
@@ -964,6 +1010,29 @@ function main() {
|
|
|
964
1010
|
staged.push({ name: TAR_ROOT + relPath, mode: 0o644, buf });
|
|
965
1011
|
}
|
|
966
1012
|
|
|
1013
|
+
// 4b. The release notes (task 1004218): what each release brought in, generated from this
|
|
1014
|
+
// ref's history, redacted as a doc, and staged into the same arrays so the pin covers
|
|
1015
|
+
// them. A worktree build or a shallow clone has no history to read, so it ships without
|
|
1016
|
+
// — and an upgrade onto it says its notes are unavailable rather than listing nothing.
|
|
1017
|
+
const notesGen = fromWorktree
|
|
1018
|
+
? { skipped: 'built from the working tree, which carries no history' }
|
|
1019
|
+
: generateReleaseNotes({ git, ref: sha, version });
|
|
1020
|
+
let notesStaged = false;
|
|
1021
|
+
if (notesGen.skipped) {
|
|
1022
|
+
console.error(`[package-core] ! ${RELEASE_NOTES_FILE} not generated — ${notesGen.skipped}.`);
|
|
1023
|
+
} else {
|
|
1024
|
+
const art = releaseNotesArtifact(notesGen.notes, { rules, sensitiveLiterals, envSecrets, knownDocNames });
|
|
1025
|
+
for (const l of art.leaks) leaks.push({ path: RELEASE_NOTES_FILE, label: l.label, sample: l.sample });
|
|
1026
|
+
filesMeta.push({ path: RELEASE_NOTES_FILE, mode: '100644', sha256: sha256hex(art.buf) });
|
|
1027
|
+
staged.push({ name: TAR_ROOT + RELEASE_NOTES_FILE, mode: 0o644, buf: art.buf });
|
|
1028
|
+
notesStaged = true;
|
|
1029
|
+
const kept = Object.keys(art.notes);
|
|
1030
|
+
const trimmed = art.dropped.length
|
|
1031
|
+
? ` — the ${art.dropped.length} oldest release(s) DROPPED to stay under ${RELEASE_NOTES_MAX_BYTES / 1024} KiB`
|
|
1032
|
+
: '';
|
|
1033
|
+
console.error(`[package-core] ${RELEASE_NOTES_FILE}: ${kept.length} release(s) ${kept[0]} → ${kept[kept.length - 1]}, ${art.summaries} summaries (${art.redacted} redacted), ${(art.buf.length / 1024).toFixed(1)} KiB${trimmed}.`);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
967
1036
|
if (leaks.length > 0) {
|
|
968
1037
|
console.error(`\n[package-core] NO-LEAK GATE FAILED — ${leaks.length} real leak(s) (doc redaction residue or a live credential in a functional file). PRODUCING NOTHING (fail-closed):`);
|
|
969
1038
|
for (const l of leaks.slice(0, 50)) {
|
|
@@ -977,7 +1046,7 @@ function main() {
|
|
|
977
1046
|
console.error(' for a live-credential leak, remove the secret at SOURCE (it should never be committed). Then re-run. dist/ was NOT touched.');
|
|
978
1047
|
process.exit(1);
|
|
979
1048
|
}
|
|
980
|
-
console.error(`[package-core] no-leak gate PASSED — ${staged.length} files clean (${docCount} docs redacted, ${stubCount} agent-docs stubbed, ${funcCount} functional verbatim).`);
|
|
1049
|
+
console.error(`[package-core] no-leak gate PASSED — ${staged.length} files clean (${docCount} docs redacted, ${stubCount} agent-docs stubbed, ${funcCount} functional verbatim${notesStaged ? `, ${RELEASE_NOTES_FILE} generated` : ''}).`);
|
|
981
1050
|
|
|
982
1051
|
// Advisory: brand-identity + public-IP literals that ship VERBATIM inside functional
|
|
983
1052
|
// files. Not a leak (these are public, not secrets; a downstream rebrands via config) —
|
|
@@ -1139,6 +1208,8 @@ module.exports = {
|
|
|
1139
1208
|
PRIOR_INSTANCE_FLOOR,
|
|
1140
1209
|
// task 1003995 — --from-worktree's enumerator, for a core checkout with no git history
|
|
1141
1210
|
walkWorktreeEntries,
|
|
1211
|
+
// task 1004218 — release-notes.json: the history-derived file, redacted as a doc and bounded
|
|
1212
|
+
releaseNotesArtifact,
|
|
1142
1213
|
};
|
|
1143
1214
|
|
|
1144
1215
|
if (require.main === module) main();
|
|
@@ -28,10 +28,14 @@
|
|
|
28
28
|
// Its own file because scripts/gds/provision.js sits at the 1500-line oversized-file
|
|
29
29
|
// ratchet, the same reason provision-net.js and provision-units.js were carved out.
|
|
30
30
|
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const os = require('os');
|
|
31
33
|
const path = require('path');
|
|
34
|
+
const { spawnSync } = require('child_process');
|
|
32
35
|
const { CONFIG, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, REPO_ROOT } = require('./provision-config.js');
|
|
33
36
|
const { appUser, dbName, instanceUser, selfInstanceRoot, standaloneRoot } = require('./provision-repo.js');
|
|
34
37
|
const { isValidSlug } = require('../../modules/provisioning/provisioning.js');
|
|
38
|
+
const { RELEASE_NOTES_FILE, RELEASE_NOTES_MAX_BYTES, parseChangeRecords, notesBetween } = require('./release-notes.js');
|
|
35
39
|
|
|
36
40
|
// The shapes whose box this runner can actually reach. Mirrors UPGRADE_SHAPES in the
|
|
37
41
|
// web-tier half, and deliberately EXCLUDES 'dedicated': a dedicated droplet is stood up
|
|
@@ -452,74 +456,101 @@ function preflightUpgrade(inst, to, deps) {
|
|
|
452
456
|
// requires one and posts it to Discord. So this reads the merge summaries between the two
|
|
453
457
|
// releases rather than inventing a changelog nobody would maintain.
|
|
454
458
|
//
|
|
455
|
-
// IT READS THE
|
|
456
|
-
//
|
|
457
|
-
// the CORE repository, and
|
|
458
|
-
// repo with the core vendored in from npm
|
|
459
|
-
//
|
|
460
|
-
//
|
|
459
|
+
// IT READS THE NOTES THE CORE PACKAGE CARRIES, and a core checkout only as a fallback
|
|
460
|
+
// (task 1004218). "What changed in the core between 1.19.881 and 1.19.972" is a question
|
|
461
|
+
// about the CORE repository, and no tenant box will ever hold its history — every project
|
|
462
|
+
// is a checkout of its OWN repo with the core vendored in from npm. So the packer writes
|
|
463
|
+
// the summaries down once, from the full history CI packs with (release-notes.json, built
|
|
464
|
+
// by scripts/gds/release-notes.js), and any machine that can download a core version can
|
|
465
|
+
// read what it carries. The checkout path below is kept for a machine that has one.
|
|
461
466
|
const CHANGE_CAP = 20;
|
|
462
|
-
|
|
467
|
+
// One registry download of a core tarball. The preview's owner is waiting on the dry run
|
|
468
|
+
// beside it anyway, but a hung registry must not hold the runner forever.
|
|
469
|
+
const NOTES_FETCH_TIMEOUT_MS = 60000;
|
|
470
|
+
|
|
471
|
+
function runFault(r) {
|
|
472
|
+
if (!r) return 'no answer';
|
|
473
|
+
if (r.error) return String(r.error.message || r.error).slice(0, 160);
|
|
474
|
+
const tail = String(r.stderr || '').trim().split('\n').filter(Boolean).slice(-1)[0] || '';
|
|
475
|
+
return (tail || `exit ${r.status}`).slice(0, 160);
|
|
476
|
+
}
|
|
463
477
|
|
|
464
478
|
/**
|
|
465
|
-
* The
|
|
479
|
+
* The release notes the core THIS runner executes from carries, if any. Free to read, and
|
|
480
|
+
* enough on its own whenever the target is at or below this core's own version — a tenant
|
|
481
|
+
* catching up to where the platform already is.
|
|
482
|
+
*/
|
|
483
|
+
function readInstalledNotes(deps = {}) {
|
|
484
|
+
const fsImpl = deps.fs || fs;
|
|
485
|
+
let raw;
|
|
486
|
+
try { raw = fsImpl.readFileSync(path.join(REPO_ROOT, RELEASE_NOTES_FILE), 'utf8'); }
|
|
487
|
+
catch { return { missing: 'this core carries no release notes' }; }
|
|
488
|
+
try { return { notes: JSON.parse(raw) }; }
|
|
489
|
+
catch { return { error: "this core's release notes are not valid JSON" }; }
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The release notes inside ONE published core package — the TARGET's own (task 1004218).
|
|
466
494
|
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
495
|
+
* WHY THE TARGET'S. The installed core's notes stop at its own version, which is zero
|
|
496
|
+
* coverage for the move looked at most — the platform's own, onto a release newer than
|
|
497
|
+
* itself. The target's package is the one guaranteed to reach the target, because that is
|
|
498
|
+
* where the target was packed. `npm pack` downloads it (the core is public, so no
|
|
499
|
+
* credential — the same registry listAvailableVersions reads) and `tar -xzOf` reads the one
|
|
500
|
+
* file out of it rather than unpacking the rest.
|
|
473
501
|
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
* are the mechanics of landing work, not the work, and listing them would bury the
|
|
477
|
-
* eleven sentences the owner actually wants under a hundred they do not.
|
|
502
|
+
* Returns `{ notes }`, `{ missing }` when the package carries none (every release published
|
|
503
|
+
* before the notes were packaged), or `{ error }`. The download never outlives the call.
|
|
478
504
|
*/
|
|
479
|
-
function
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
505
|
+
function readPackagedNotes(version, deps = {}) {
|
|
506
|
+
if (!CORE_VERSION_RE.test(String(version || ''))) return { error: 'could not work out which package to read' };
|
|
507
|
+
const run = deps.spawn || spawnSync;
|
|
508
|
+
const fsImpl = deps.fs || fs;
|
|
509
|
+
let dir = null;
|
|
510
|
+
try {
|
|
511
|
+
dir = fsImpl.mkdtempSync(path.join(deps.tmpDir || os.tmpdir(), 'bongos-release-notes-'));
|
|
512
|
+
const packed = run('npm', ['pack', `@bongos/core@${version}`, '--pack-destination', dir, '--silent'],
|
|
513
|
+
{ cwd: dir, env: deps.env || process.env, encoding: 'utf8', timeout: NOTES_FETCH_TIMEOUT_MS });
|
|
514
|
+
if (!packed || packed.error || packed.status !== 0) {
|
|
515
|
+
return { error: `could not download the core ${version} package to read its release notes (${runFault(packed)})` };
|
|
516
|
+
}
|
|
517
|
+
// Whatever npm named it: a scoped package's tarball is <scope>-<name>-<version>.tgz today.
|
|
518
|
+
const tgz = fsImpl.readdirSync(dir).find((f) => f.endsWith('.tgz'));
|
|
519
|
+
if (!tgz) return { error: `npm reported the core ${version} package downloaded, but left no tarball` };
|
|
520
|
+
const x = run('tar', ['-xzOf', path.join(dir, tgz), `package/${RELEASE_NOTES_FILE}`],
|
|
521
|
+
{ encoding: 'utf8', maxBuffer: 4 * RELEASE_NOTES_MAX_BYTES, timeout: NOTES_FETCH_TIMEOUT_MS });
|
|
522
|
+
if (!x || x.error || x.status !== 0) {
|
|
523
|
+
if (/not found in archive/i.test(String((x && x.stderr) || ''))) {
|
|
524
|
+
return { missing: `the core ${version} package carries no release notes` };
|
|
525
|
+
}
|
|
526
|
+
return { error: `could not read the release notes out of the core ${version} package (${runFault(x)})` };
|
|
499
527
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
528
|
+
try { return { notes: JSON.parse(x.stdout) }; }
|
|
529
|
+
catch { return { error: `the release notes in the core ${version} package are not valid JSON` }; }
|
|
530
|
+
} catch (e) {
|
|
531
|
+
return { error: `could not fetch the core ${version} package (${String((e && e.message) || e).slice(0, 160)})` };
|
|
532
|
+
} finally {
|
|
533
|
+
if (dir) { try { fsImpl.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
506
534
|
}
|
|
507
|
-
return out;
|
|
508
535
|
}
|
|
509
536
|
|
|
510
537
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
* `entries` for a range it could not resolve. An empty list reads as "nothing changed",
|
|
515
|
-
* which on a page whose job is to say what a restart buys is the one wrong impression it
|
|
516
|
-
* must not give, and it is exactly what a missing checkout or a shallow clone produces.
|
|
517
|
-
*
|
|
518
|
-
* Every version reaching a shell is re-checked against the strict three-number form
|
|
519
|
-
* FIRST. They arrive from an intent row whose column has the same CHECK, but a value that
|
|
520
|
-
* is interpolated into a command is checked where it is interpolated — the rule
|
|
521
|
-
* upgradeInvocation states about the slug.
|
|
538
|
+
* The range, answered from release notes alone: `{ entries }`, or `{ why }` they cannot.
|
|
539
|
+
* The installed core first, when its notes already reach the target; the target's own
|
|
540
|
+
* package otherwise. No git, no SHAs, no checkout.
|
|
522
541
|
*/
|
|
542
|
+
function changesFromNotes(fromVersion, toVersion, deps) {
|
|
543
|
+
const installed = (deps.readInstalledNotes || readInstalledNotes)(deps);
|
|
544
|
+
if (installed.notes) {
|
|
545
|
+
const r = notesBetween(installed.notes, fromVersion, toVersion);
|
|
546
|
+
if (r.entries) return r;
|
|
547
|
+
}
|
|
548
|
+
const target = (deps.readPackagedNotes || readPackagedNotes)(toVersion, deps);
|
|
549
|
+
if (!target.notes) return { why: target.missing || target.error || `the core ${toVersion} package's release notes could not be read` };
|
|
550
|
+
const r = notesBetween(target.notes, fromVersion, toVersion);
|
|
551
|
+
return r.entries ? r : { why: `the release notes in the core ${toVersion} package ${r.uncovered}` };
|
|
552
|
+
}
|
|
553
|
+
|
|
523
554
|
/**
|
|
524
555
|
* Classify a git command that did not succeed. PURE.
|
|
525
556
|
*
|
|
@@ -598,21 +629,11 @@ function findCoreCheckout(deps, git) {
|
|
|
598
629
|
}
|
|
599
630
|
|
|
600
631
|
/**
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
* `entries` for a range it could not resolve. An empty list reads as "nothing changed",
|
|
605
|
-
* which on a page whose job is to say what a restart buys is the one wrong impression it
|
|
606
|
-
* must not give, and it is exactly what a missing checkout produces.
|
|
607
|
-
*
|
|
608
|
-
* Every version reaching a shell is re-checked against the strict three-number form
|
|
609
|
-
* FIRST. They arrive from an intent row whose column has the same CHECK, but a value that
|
|
610
|
-
* is interpolated into a command is checked where it is interpolated — the rule
|
|
611
|
-
* upgradeInvocation states about the slug.
|
|
632
|
+
* The range, answered from a core CHECKOUT's git history: `{ entries }` or `{ error }`.
|
|
633
|
+
* The fallback for a machine that has one, since the package's notes only exist for
|
|
634
|
+
* releases published after they were introduced (task 1004218).
|
|
612
635
|
*/
|
|
613
|
-
function
|
|
614
|
-
const bad = [fromVersion, toVersion].find((v) => !CORE_VERSION_RE.test(String(v || '')));
|
|
615
|
-
if (bad !== undefined) return { from: fromVersion || null, to: toVersion || null, error: 'could not work out which releases this covers' };
|
|
636
|
+
function changesFromCheckout(fromVersion, toVersion, deps) {
|
|
616
637
|
// The CONTROL PLANE's executor, never the previewed instance's: under
|
|
617
638
|
// PROVISION_REMOTE_HOST `exec` is the remote box, while the core history — and the whole
|
|
618
639
|
// question of what changed in the CORE — belongs to the machine that builds it.
|
|
@@ -635,7 +656,7 @@ function changesBetween(fromVersion, toVersion, deps) {
|
|
|
635
656
|
};
|
|
636
657
|
};
|
|
637
658
|
const found = findCoreCheckout(deps, git);
|
|
638
|
-
if (found.error) return {
|
|
659
|
+
if (found.error) return { error: found.error };
|
|
639
660
|
const dir = found.dir;
|
|
640
661
|
const shaOf = (v) => {
|
|
641
662
|
const r = git(dir, `log --format=%H --max-count=1 -F --grep='release: core ${v} '`);
|
|
@@ -645,18 +666,48 @@ function changesBetween(fromVersion, toVersion, deps) {
|
|
|
645
666
|
};
|
|
646
667
|
const from = shaOf(fromVersion);
|
|
647
668
|
const to = shaOf(toVersion);
|
|
648
|
-
if (from.fault || to.fault) return {
|
|
669
|
+
if (from.fault || to.fault) return { error: from.fault || to.fault };
|
|
649
670
|
// LOOKED, AND DID NOT FIND — a different sentence from "could not look", and it names
|
|
650
671
|
// WHICH version is missing so the reader can tell a not-yet-published target from a
|
|
651
672
|
// checkout that has not been fetched.
|
|
652
673
|
const missing = [!from.sha && fromVersion, !to.sha && toVersion].filter(Boolean);
|
|
653
674
|
if (missing.length) {
|
|
654
|
-
return {
|
|
675
|
+
return { error: `core ${missing.join(' and ')} ${missing.length > 1 ? 'are' : 'is'} not in the core history on this machine yet` };
|
|
655
676
|
}
|
|
656
677
|
const r = git(dir, `log --merges --format=%s%x1f%b%x1e ${from.sha}..${to.sha}`);
|
|
657
|
-
if (!r.ok) return {
|
|
658
|
-
|
|
659
|
-
|
|
678
|
+
if (!r.ok) return { error: gitFault(r, dir) };
|
|
679
|
+
return { entries: parseChangeRecords(r.stdout) };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* What landed between two core releases, as the summaries their ships wrote.
|
|
684
|
+
*
|
|
685
|
+
* Returns `{ from, to, entries, more, source }`, or `{ from, to, error }` — NEVER an empty
|
|
686
|
+
* `entries` for a range it could not resolve. An empty list reads as "nothing changed",
|
|
687
|
+
* which on a page whose job is to say what a restart buys is the one wrong impression it
|
|
688
|
+
* must not give, and it is exactly what a missing checkout produces.
|
|
689
|
+
*
|
|
690
|
+
* THE PACKAGE'S NOTES FIRST, THE CHECKOUT SECOND (task 1004218). The notes answer on every
|
|
691
|
+
* box; the checkout only where one exists, which is never a tenant. When neither can, the
|
|
692
|
+
* error carries BOTH reasons — the notes' and the checkout's own precise sentence (task
|
|
693
|
+
* 1004212) — so the reader is told everything that was tried and why each fell short.
|
|
694
|
+
*
|
|
695
|
+
* Every version reaching a shell is re-checked against the strict three-number form
|
|
696
|
+
* FIRST. They arrive from an intent row whose column has the same CHECK, but a value that
|
|
697
|
+
* is interpolated into a command is checked where it is interpolated — the rule
|
|
698
|
+
* upgradeInvocation states about the slug.
|
|
699
|
+
*/
|
|
700
|
+
function changesBetween(fromVersion, toVersion, deps) {
|
|
701
|
+
const bad = [fromVersion, toVersion].find((v) => !CORE_VERSION_RE.test(String(v || '')));
|
|
702
|
+
if (bad !== undefined) return { from: fromVersion || null, to: toVersion || null, error: 'could not work out which releases this covers' };
|
|
703
|
+
const listed = (all, source) => ({
|
|
704
|
+
from: fromVersion, to: toVersion, entries: all.slice(0, CHANGE_CAP), more: Math.max(0, all.length - CHANGE_CAP), source,
|
|
705
|
+
});
|
|
706
|
+
const notes = changesFromNotes(fromVersion, toVersion, deps);
|
|
707
|
+
if (notes.entries) return listed(notes.entries, 'release-notes');
|
|
708
|
+
const checkout = changesFromCheckout(fromVersion, toVersion, deps);
|
|
709
|
+
if (checkout.entries) return listed(checkout.entries, 'checkout');
|
|
710
|
+
return { from: fromVersion, to: toVersion, error: `${notes.why}; ${checkout.error}` };
|
|
660
711
|
}
|
|
661
712
|
|
|
662
713
|
// Run a dry run for one instance and cache its verdict — the ON-DEMAND leg, drained from
|
|
@@ -780,4 +831,4 @@ async function snapshotAllCoreVersions(deps) {
|
|
|
780
831
|
return { seen };
|
|
781
832
|
}
|
|
782
833
|
|
|
783
|
-
module.exports = { UPGRADE_SHAPES, CHANGE_CAP, parseChangeRecords, changesBetween, coreCheckoutCandidates, findCoreCheckout, gitFault, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|
|
834
|
+
module.exports = { UPGRADE_SHAPES, CHANGE_CAP, parseChangeRecords, readInstalledNotes, readPackagedNotes, changesBetween, coreCheckoutCandidates, findCoreCheckout, gitFault, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// The patrol is the third enforcement layer (prevent → gate → PATROL): checks
|
|
6
6
|
// too slow, too networked, or too history-wide for the merge gate, run weekly
|
|
7
7
|
// by the .claude/scheduled-tasks/redteam-weekly routine. It is ADVISORY by
|
|
8
|
-
// doctrine (ADR
|
|
8
|
+
// doctrine (ADR 0158 §2 — a verdict never self-executes): findings are FILED as
|
|
9
9
|
// security reports for Archon adjudication, never turned into gates here.
|
|
10
10
|
//
|
|
11
11
|
// Checks (all deterministic — no model reads anything):
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// log. Instead --file preflights the session and, when there is none, follows the
|
|
41
41
|
// main-audit.js --require-roster doctrine: report LOUDLY rather than green-and-
|
|
42
42
|
// vacuous. The exit code says whether a finding was LOST, not whether findings
|
|
43
|
-
// exist — findings are advisory (ADR
|
|
43
|
+
// exist — findings are advisory (ADR 0158 §2) and a patrol that goes red merely for
|
|
44
44
|
// doing its job trains everyone to ignore it:
|
|
45
45
|
//
|
|
46
46
|
// ran + filed → 0
|
|
@@ -271,7 +271,7 @@ function buildClassReport(className, findings, notes = []) {
|
|
|
271
271
|
target: `redteam-patrol/${className}`,
|
|
272
272
|
severity: maxSeverity(findings),
|
|
273
273
|
title: `[patrol] ${className}: ${findings.length} finding(s)`,
|
|
274
|
-
description: `Weekly deterministic red-team patrol (task 1003128; advisory per ADR
|
|
274
|
+
description: `Weekly deterministic red-team patrol (task 1003128; advisory per ADR 0158 §2 — adjudicate, then fix via ordinary tasks).`,
|
|
275
275
|
repro_steps_md: body,
|
|
276
276
|
};
|
|
277
277
|
}
|
|
@@ -289,7 +289,7 @@ function stepSummary(md) {
|
|
|
289
289
|
// that disposition deserves. Extracted so the honest-exit contract is a table a
|
|
290
290
|
// test can enumerate rather than branching buried in main().
|
|
291
291
|
//
|
|
292
|
-
// The load-bearing row is the last one. Findings are ADVISORY (ADR
|
|
292
|
+
// The load-bearing row is the last one. Findings are ADVISORY (ADR 0158 §2), so a
|
|
293
293
|
// run that finds three things and files them is a SUCCESS — going red merely for
|
|
294
294
|
// doing its job is how a weekly lane becomes noise everyone filters. Red is
|
|
295
295
|
// reserved for the single state where evidence EVAPORATES: findings exist,
|