mindforge-cc 11.9.1 → 11.9.3
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/.agent/CLAUDE.md +37 -13
- package/.agent/hooks/mindforge-block-no-verify.js +61 -13
- package/.agent/hooks/mindforge-config-protection.js +82 -3
- package/.agent/hooks/mindforge-context-monitor.js +1 -1
- package/.agent/hooks/mindforge-workflow-guard.js +2 -2
- package/.agent/hooks/run-with-flags.js +190 -20
- package/.agent/mindforge/browse.md +2 -2
- package/.agent/mindforge/checkpoint.md +1 -1
- package/.agent/mindforge/consult.md +1 -1
- package/.agent/mindforge/cost-report.md +1 -1
- package/.agent/mindforge/harness-audit.md +1 -1
- package/.agent/mindforge/orch-add-feature.md +1 -1
- package/.agent/mindforge/orch-build-mvp.md +1 -1
- package/.agent/mindforge/orch-change-feature.md +1 -1
- package/.agent/mindforge/orch-fix-defect.md +1 -1
- package/.agent/mindforge/orch-refine-code.md +1 -1
- package/.agent/mindforge/qa.md +2 -2
- package/.claude/CLAUDE.md +37 -13
- package/.claude/commands/mindforge/browse.md +2 -2
- package/.claude/commands/mindforge/checkpoint.md +1 -1
- package/.claude/commands/mindforge/consult.md +1 -1
- package/.claude/commands/mindforge/cost-report.md +1 -1
- package/.claude/commands/mindforge/harness-audit.md +1 -1
- package/.claude/commands/mindforge/orch-add-feature.md +1 -1
- package/.claude/commands/mindforge/orch-build-mvp.md +1 -1
- package/.claude/commands/mindforge/orch-change-feature.md +1 -1
- package/.claude/commands/mindforge/orch-fix-defect.md +1 -1
- package/.claude/commands/mindforge/orch-refine-code.md +1 -1
- package/.claude/commands/mindforge/qa.md +2 -2
- package/.mindforge/MINDFORGE-SCHEMA.json +126 -13
- package/.mindforge/config.json +4 -4
- package/.mindforge/engine/autonomous/headless-adapter.md +2 -2
- package/.mindforge/engine/cost-tracking/router.md +1 -1
- package/.mindforge/engine/cost-tracking/token-ledger.md +21 -24
- package/.mindforge/engine/temporal-protocol.md +2 -2
- package/.mindforge/governance/change-classifier.md +20 -4
- package/.mindforge/memory/sync-manifest.json +1 -1
- package/.mindforge/metrics/METRICS-SCHEMA.md +13 -4
- package/.mindforge/personas/cost-optimizer.md +2 -2
- package/.mindforge/personas/multi-model-bridge.md +1 -1
- package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
- package/.mindforge/skills/cost-aware-routing/SKILL.md +3 -3
- package/.mindforge/skills/multi-llm-consult/SKILL.md +2 -2
- package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
- package/CHANGELOG.md +402 -0
- package/MINDFORGE.md +13 -6
- package/README.md +51 -2
- package/RELEASENOTES.md +55 -2
- package/SECURITY.md +22 -3
- package/bin/autonomous/audit-writer.js +48 -33
- package/bin/autonomous/auto-runner.js +65 -2
- package/bin/change-classifier.js +151 -16
- package/bin/dashboard/api-router.js +28 -47
- package/bin/dashboard/error-response.js +44 -0
- package/bin/dashboard/frontend/app.js +429 -0
- package/bin/dashboard/frontend/index.html +14 -390
- package/bin/dashboard/metrics-aggregator.js +75 -30
- package/bin/dashboard/revops-api.js +12 -2
- package/bin/dashboard/server.js +245 -6
- package/bin/dashboard/sse-bridge.js +11 -8
- package/bin/dashboard/temporal-api.js +11 -5
- package/bin/engine/remediation-engine.js +12 -1
- package/bin/engine/sre-manager.js +1 -1
- package/bin/engine/temporal-cli.js +56 -6
- package/bin/engine/temporal-hub.js +41 -9
- package/bin/engine/verification-runner.js +134 -17
- package/bin/engine/verify-cli.js +25 -7
- package/bin/eval/eval-harness.js +212 -1
- package/bin/eval/golden-set-retrieval.json +9 -0
- package/bin/governance/approval-record.js +147 -0
- package/bin/governance/approve.js +12 -7
- package/bin/governance/policy-engine.js +41 -3
- package/bin/governance/policy-gate-hardened.js +36 -1
- package/bin/governance/verify-approvals.js +163 -0
- package/bin/harness-audit.js +224 -10
- package/bin/hindsight-injector.js +8 -2
- package/bin/hooks/instinct-capture-hook.js +19 -5
- package/bin/install.js +63 -3
- package/bin/installer/harness-adapter-compliance.js +339 -28
- package/bin/installer/hook-registration.js +504 -0
- package/bin/installer-core.js +451 -63
- package/bin/learning/instinct-cli.js +14 -24
- package/bin/memory/knowledge-capture.js +23 -3
- package/bin/memory/knowledge-graph.js +70 -31
- package/bin/memory/vector-hub.js +500 -44
- package/bin/migrations/0.6.0-to-1.0.0.js +30 -25
- package/bin/migrations/1.0.0-to-2.0.0.js +22 -23
- package/bin/mindforge-cli.js +110 -17
- package/bin/models/cost-tracker.js +126 -29
- package/bin/models/model-client.js +6 -1
- package/bin/models/model-router.js +28 -7
- package/bin/models/usage-record.js +71 -0
- package/bin/revops/debt-monitor.js +57 -13
- package/bin/security/trust-gate-hook.js +50 -6
- package/bin/skill-validator.js +6 -1
- package/bin/skills-builder/skill-scorer.js +46 -6
- package/bin/updater/self-update.js +6 -1
- package/bin/updater/version-comparator.js +21 -1
- package/bin/utils/file-lock.js +106 -0
- package/bin/utils/mindforge-params.js +124 -0
- package/bin/utils/mindforge-version.js +99 -0
- package/bin/utils/redact-secrets.js +106 -0
- package/bin/validate-config.js +75 -17
- package/bin/wizard/setup-wizard.js +4 -1
- package/bin/wizard/theme.js +9 -1
- package/changelogs/index.json +11 -9
- package/changelogs/v11.9.2.md +209 -0
- package/changelogs/v11.9.3.md +195 -0
- package/docs/References/config-reference.md +76 -14
- package/docs/References/sdk-api.md +1 -1
- package/docs/Templates/Codebase/architecture.md +1 -1
- package/docs/commands-reference.md +4 -5
- package/docs/faq.md +25 -5
- package/docs/getting-started.md +3 -3
- package/docs/sdk-reference.md +15 -7
- package/docs/troubleshooting.md +10 -6
- package/docs/user-guide.md +14 -14
- package/examples/sdk-integration/README.md +1 -1
- package/package.json +10 -4
- package/subagents/.claude-plugin/marketplace.json +1 -1
- package/bin/dashboard/approval-handler.js +0 -136
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
const crypto = require('crypto');
|
|
13
11
|
const { execFileSync } = require('child_process');
|
|
12
|
+
const { checksumRecord, expiryFrom, SCHEMA } = require('./approval-record');
|
|
14
13
|
|
|
15
14
|
const ROOT = path.resolve(__dirname, '../../');
|
|
16
15
|
const APPROVALS_DIR = path.join(ROOT, '.planning/approvals');
|
|
@@ -90,21 +89,27 @@ function approve(opts = {}) {
|
|
|
90
89
|
// Verify approver identity — THROWS fail-closed if unverifiable (before any write).
|
|
91
90
|
const identityVerification = verifyApproverIdentity(approver);
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
// The old `signature` field was sha256(id:reason:timestamp:os.hostname()). It was not a
|
|
93
|
+
// signature (nothing signs it), it could not be verified by anyone holding the record
|
|
94
|
+
// (hostname is not a field of it), and the only consumer asserted its PRESENCE. Replaced by a
|
|
95
|
+
// recomputable checksum over the record's own fields — see bin/governance/approval-record.js.
|
|
96
|
+
//
|
|
97
|
+
// `expires_at` is new and load-bearing: without it, the record committed on 2026-06-11 for
|
|
98
|
+
// version 11.5.1 still satisfied the Tier-3 gate 67 days and 286 commits later, against
|
|
99
|
+
// 11.9.2. An acknowledgement now stops applying, and it does not carry to a later release.
|
|
97
100
|
const record = {
|
|
101
|
+
schema: SCHEMA,
|
|
98
102
|
id,
|
|
99
103
|
project: pkg.name,
|
|
100
104
|
version: pkg.version,
|
|
101
105
|
tier: 3,
|
|
102
106
|
approved_by: approver,
|
|
103
107
|
timestamp,
|
|
108
|
+
expires_at: expiryFrom(timestamp),
|
|
104
109
|
reason,
|
|
105
|
-
signature: `sha256:${signature}`,
|
|
106
110
|
identity_verification: identityVerification
|
|
107
111
|
};
|
|
112
|
+
record.record_checksum = checksumRecord(record);
|
|
108
113
|
|
|
109
114
|
if (!fs.existsSync(approvalsDir)) fs.mkdirSync(approvalsDir, { recursive: true });
|
|
110
115
|
const filename = `approval-${id.toLowerCase()}.json`;
|
|
@@ -74,7 +74,17 @@ class PolicyEngine {
|
|
|
74
74
|
|
|
75
75
|
// [PQAS] v7: Hardened Biometric Bypass for Risk > 95
|
|
76
76
|
if (impactScore > 95) {
|
|
77
|
-
|
|
77
|
+
// The gate looks up the attestation by `intent.requestId`, which this method never
|
|
78
|
+
// set — it generated `requestId` as a local and passed the bare intent, so the lookup
|
|
79
|
+
// always received undefined. Measured: orbital-guardian returns
|
|
80
|
+
// {verified:false, reason:"missing requestId"}, so the attestation path could NEVER
|
|
81
|
+
// approve, even immediately after a successful recordBypass(). The gate was stuck
|
|
82
|
+
// closed, which is safe but means the whole hardware-attestation feature only ever
|
|
83
|
+
// denied. tests/v8-orbital-governance.test.js passes an object that DOES carry
|
|
84
|
+
// requestId, which is why the contract looked satisfied.
|
|
85
|
+
//
|
|
86
|
+
// A new object rather than a mutation: the caller's intent is not ours to modify.
|
|
87
|
+
const gateResult = await policyGate.evaluateBypass({ ...intent, requestId }, impactScore);
|
|
78
88
|
if (gateResult.status === 'WAIT_FOR_BIOMETRIC' || gateResult.status === 'WAIT_FOR_ORBITAL') {
|
|
79
89
|
verdict = {
|
|
80
90
|
verdict: 'DENY',
|
|
@@ -86,8 +96,28 @@ class PolicyEngine {
|
|
|
86
96
|
this.logAudit(intent, impactScore, verdict);
|
|
87
97
|
return verdict;
|
|
88
98
|
}
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
// Only claim an attestation when one was actually verified. evaluateBypass has TWO
|
|
100
|
+
// non-WAIT returns, and they mean opposite things: an attestation verified in the
|
|
101
|
+
// enclave (carries attestation_id), or "Impact within standard threshold" — which
|
|
102
|
+
// states that no attestation was required. The old code logged "Biometric signature
|
|
103
|
+
// verified" and recorded the audit reason "Authorized via Biometric Bypass
|
|
104
|
+
// [WEB-AUTHN-DEX]" for BOTH, so the audit trail would assert a biometric verification
|
|
105
|
+
// for an operation the gate had explicitly waved through unchecked. That is the audit
|
|
106
|
+
// equivalent of a gate printing success without running.
|
|
107
|
+
const attested = Boolean(gateResult.attestation_id);
|
|
108
|
+
if (attested) {
|
|
109
|
+
console.log(`[PQAS-GATE] [${requestId}] Hardware attestation ${gateResult.attestation_id} verified. Proceeding with high-risk mutation.`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(`[PQAS-GATE] [${requestId}] Gate returned ${gateResult.status} without an attestation: ${gateResult.reason}. NOT recording this as attested.`);
|
|
112
|
+
}
|
|
113
|
+
verdict = {
|
|
114
|
+
verdict: 'PERMIT',
|
|
115
|
+
reason: attested
|
|
116
|
+
? `Authorized via hardware attestation [${gateResult.attestation_id}]`
|
|
117
|
+
: `Permitted without attestation: ${gateResult.reason || gateResult.status}`,
|
|
118
|
+
requestId,
|
|
119
|
+
attested,
|
|
120
|
+
};
|
|
91
121
|
this.logAudit(intent, impactScore, verdict);
|
|
92
122
|
return verdict;
|
|
93
123
|
}
|
|
@@ -165,6 +195,12 @@ class PolicyEngine {
|
|
|
165
195
|
}
|
|
166
196
|
|
|
167
197
|
logAudit(intent, impactScore, verdict) {
|
|
198
|
+
// LOCK-01: AuditWriter.write -> appendAuditEntrySync now takes a FAIL-CLOSED lock,
|
|
199
|
+
// so this can reject under contention. It is intentionally not awaited (the verdict
|
|
200
|
+
// path is synchronous), so catch here — otherwise the rejection escapes to a global
|
|
201
|
+
// unhandledRejection handler and the lost audit record is invisible at this site.
|
|
202
|
+
// The verdict is still returned: promoting an audit-write failure to an implicit
|
|
203
|
+
// DENY is a behaviour change for v12, not a patch.
|
|
168
204
|
this._auditWriter.write({
|
|
169
205
|
timestamp: new Date().toISOString(),
|
|
170
206
|
requestId: verdict.requestId,
|
|
@@ -175,6 +211,8 @@ class PolicyEngine {
|
|
|
175
211
|
impactScore,
|
|
176
212
|
verdict: verdict.verdict,
|
|
177
213
|
reason: verdict.reason
|
|
214
|
+
}).catch(err => {
|
|
215
|
+
console.error(`[APO-AUDIT-FAIL] [${verdict.requestId}] RISK-AUDIT append failed — decision NOT recorded: ${err.message}`);
|
|
178
216
|
});
|
|
179
217
|
}
|
|
180
218
|
|
|
@@ -12,7 +12,42 @@ const configManager = require('../governance/config-manager');
|
|
|
12
12
|
class PolicyGateHardened {
|
|
13
13
|
constructor() {
|
|
14
14
|
// bypasses.json deprecated in favor of orbital.attestations table (v8)
|
|
15
|
-
|
|
15
|
+
// Impact scores are 0-100. This used to read `governance.critical_drift_threshold` with a
|
|
16
|
+
// default of 95 — but that key is a DRIFT RATIO, read by bin/engine/logic-drift-detector.js:23
|
|
17
|
+
// with a default of 0.50, and the shipped config sets it to 0.5. One key, two incompatible
|
|
18
|
+
// scales: the drift detector was correct and this gate silently received 0.5 where it expected
|
|
19
|
+
// ~95, making the "within standard threshold" branch below permanently dead.
|
|
20
|
+
//
|
|
21
|
+
// Dead was the SAFE direction, but it is one plausible edit from the unsafe one: raising that
|
|
22
|
+
// key to 95 to "fix" this gate would return ALLOWED for every high-risk score AND break drift
|
|
23
|
+
// detection, and policy-engine.js would then log "Biometric signature verified" for an
|
|
24
|
+
// operation where no attestation was ever attempted. Hence a dedicated key, plus a range guard
|
|
25
|
+
// so a ratio can never be mistaken for a score again.
|
|
26
|
+
this.criticalThreshold = this.resolveImpactThreshold();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The impact score (0-100) at or below which no hardware attestation is demanded.
|
|
31
|
+
*
|
|
32
|
+
* Rejects anything that is not on the 0-100 scale rather than trusting it. A value of 0.5 is a
|
|
33
|
+
* ratio that somebody has pointed at the wrong knob, and silently accepting it is how this gate
|
|
34
|
+
* came to compare a drift ratio against an impact score.
|
|
35
|
+
*
|
|
36
|
+
* @returns {number}
|
|
37
|
+
*/
|
|
38
|
+
resolveImpactThreshold(fallback = 95) {
|
|
39
|
+
const raw = configManager.get('governance.critical_impact_threshold', fallback);
|
|
40
|
+
const value = Number(raw);
|
|
41
|
+
if (!Number.isFinite(value) || value < 1 || value > 100) {
|
|
42
|
+
// Deliberately loud: a mis-scaled threshold changes whether high-risk mutations demand
|
|
43
|
+
// attestation at all, so it must not be absorbed quietly.
|
|
44
|
+
console.warn(
|
|
45
|
+
`[ORBITAL-GATE] governance.critical_impact_threshold=${JSON.stringify(raw)} is not an ` +
|
|
46
|
+
'impact score on the 0-100 scale (a value below 1 is usually a drift RATIO pointed at the ' +
|
|
47
|
+
`wrong key). Using ${fallback}.`);
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
16
51
|
}
|
|
17
52
|
|
|
18
53
|
/**
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* Verify the integrity of every committed approval record, and disclose Tier-3 changes.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node bin/governance/verify-approvals.js [--tier N] [--dir <path>] [--json]
|
|
8
|
+
*
|
|
9
|
+
* Exit 0 = every record is intact and current (or there are none).
|
|
10
|
+
* Exit 1 = at least one record is malformed, tampered with, or stale.
|
|
11
|
+
*
|
|
12
|
+
* WHAT REPLACED WHAT, and why the contract changed.
|
|
13
|
+
*
|
|
14
|
+
* The previous gate lived inline in control-plane.yml and asked: "does .planning/approvals/
|
|
15
|
+
* contain at least one file that clears an identity bar?" Measured, that question has three
|
|
16
|
+
* fatal properties:
|
|
17
|
+
*
|
|
18
|
+
* * It is a pure function of the DIRECTORY, not of the change. Running the identical script in
|
|
19
|
+
* a temp dir containing only the committed approval — with no git repository at all — exited
|
|
20
|
+
* 0 with the same success message.
|
|
21
|
+
* * It therefore granted approval in perpetuity. One record, committed 2026-06-11 for version
|
|
22
|
+
* 11.5.1, satisfied it for 67 days and 286 commits, against 11.9.2.
|
|
23
|
+
* * It was committed in c6ec9a9 — the SAME commit that relaxed the gate to accept it.
|
|
24
|
+
*
|
|
25
|
+
* So the old step could not distinguish an approved change from an unapproved one. This script
|
|
26
|
+
* does not try to fix that by making the file mean more, because a git-tracked file cannot carry
|
|
27
|
+
* authorization: anyone who can push can write one, and MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1
|
|
28
|
+
* makes minting a one-liner. Verified live: before branch protection was enabled,
|
|
29
|
+
* `branches/{main,develop}/protection` returned 404 and `rulesets` was empty, so nothing stood
|
|
30
|
+
* behind the gate at all.
|
|
31
|
+
*
|
|
32
|
+
* The honest split, and what each half is worth:
|
|
33
|
+
*
|
|
34
|
+
* INTEGRITY (enforced here, can fail). Records must be well-formed, checksum-consistent,
|
|
35
|
+
* honestly marked as to identity, bound to the release being built, and unexpired. This is
|
|
36
|
+
* what stops a record being edited after the fact or recycled across releases, and it runs on
|
|
37
|
+
* EVERY build rather than only Tier-3 ones — a corrupt record is corrupt regardless of tier.
|
|
38
|
+
*
|
|
39
|
+
* AUTHORIZATION (not enforced here, deliberately). Enforced by branch protection: a required
|
|
40
|
+
* pull request plus required status checks on main and develop. On a Tier-3 change with no
|
|
41
|
+
* current acknowledgement this script DISCLOSES loudly and exits 0, rather than blocking. That
|
|
42
|
+
* is a deliberate choice: 28% of recent commits touch the trust surface, and blocking them
|
|
43
|
+
* behind a self-issued file would add friction proportional to nothing. Overstating a check is
|
|
44
|
+
* how the previous gate came to be trusted for two months while proving nothing.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const path = require('path');
|
|
49
|
+
const { verifyRecord } = require('./approval-record');
|
|
50
|
+
|
|
51
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const out = { tier: null, dir: path.join(ROOT, '.planning', 'approvals'), json: false };
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
if (argv[i] === '--tier') out.tier = String(argv[++i] ?? '').trim();
|
|
57
|
+
else if (argv[i] === '--dir') out.dir = argv[++i];
|
|
58
|
+
else if (argv[i] === '--json') out.json = true;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** GitHub Actions annotations when running there; plain text otherwise. */
|
|
64
|
+
const inActions = () => Boolean(process.env.GITHUB_ACTIONS);
|
|
65
|
+
function notice(msg) { console.log(inActions() ? `::notice::${msg}` : msg); }
|
|
66
|
+
function error(msg) { console.error(inActions() ? `::error::${msg}` : msg); }
|
|
67
|
+
function summary(lines) {
|
|
68
|
+
const f = process.env.GITHUB_STEP_SUMMARY;
|
|
69
|
+
if (!f) return;
|
|
70
|
+
try { fs.appendFileSync(f, `${lines.join('\n')}\n`); } catch { /* summary is best-effort */ }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function main(argv = process.argv.slice(2)) {
|
|
74
|
+
const args = parseArgs(argv);
|
|
75
|
+
const currentVersion = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
|
|
76
|
+
|
|
77
|
+
let files = [];
|
|
78
|
+
try {
|
|
79
|
+
files = fs.readdirSync(args.dir).filter((f) => f.endsWith('.json'));
|
|
80
|
+
} catch {
|
|
81
|
+
files = []; // an absent directory is not an error — it means no acknowledgements exist
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const f of files) {
|
|
86
|
+
const p = path.join(args.dir, f);
|
|
87
|
+
let rec;
|
|
88
|
+
try { rec = JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
89
|
+
catch (e) {
|
|
90
|
+
results.push({ file: f, ok: false, stale: false, problems: [`not valid JSON: ${e.message}`] });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
results.push({ file: f, ...verifyRecord(rec, { currentVersion }) });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const broken = results.filter((r) => !r.ok);
|
|
97
|
+
const valid = results.filter((r) => r.ok);
|
|
98
|
+
|
|
99
|
+
if (args.json) {
|
|
100
|
+
console.log(JSON.stringify({ currentVersion, tier: args.tier, results }, null, 2));
|
|
101
|
+
} else {
|
|
102
|
+
for (const r of broken) {
|
|
103
|
+
error(`approval ${r.file} is not usable:`);
|
|
104
|
+
for (const p of r.problems) console.error(` - ${p}`);
|
|
105
|
+
}
|
|
106
|
+
for (const r of valid) notice(`approval ${r.file} is intact and current (${currentVersion}).`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (broken.length) {
|
|
110
|
+
error(`${broken.length} approval record(s) rejected. Remove the stale file, or re-mint with ` +
|
|
111
|
+
'`node bin/governance/approve.js "<reason>"`. A record does not carry forward to a later release.');
|
|
112
|
+
summary([
|
|
113
|
+
'## 🔴 Approval record integrity failed',
|
|
114
|
+
'',
|
|
115
|
+
...broken.flatMap((r) => [`**${r.file}**`, ...r.problems.map((p) => `- ${p}`), '']),
|
|
116
|
+
]);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!results.length) notice('No approval records present — nothing to verify.');
|
|
121
|
+
|
|
122
|
+
// Tier-3 disclosure. Never blocks — see the header.
|
|
123
|
+
if (args.tier === '3') {
|
|
124
|
+
if (valid.length) {
|
|
125
|
+
notice(`Tier 3 change with ${valid.length} current acknowledgement(s) on record.`);
|
|
126
|
+
summary([
|
|
127
|
+
'## ⚖️ Tier 3 change — acknowledged',
|
|
128
|
+
'',
|
|
129
|
+
...valid.map((r) => `- \`${r.file}\``),
|
|
130
|
+
'',
|
|
131
|
+
'Recorded acknowledgement only. Authorization is enforced by branch protection ' +
|
|
132
|
+
'(required PR + required checks), not by this file.',
|
|
133
|
+
]);
|
|
134
|
+
} else {
|
|
135
|
+
notice('Tier 3 change with NO recorded acknowledgement. Not blocking — see the summary.');
|
|
136
|
+
summary([
|
|
137
|
+
'## ⚖️ Tier 3 change — touches the trust surface',
|
|
138
|
+
'',
|
|
139
|
+
'This change modifies a sensitive path (auth/payment/security, `bin/governance/`, ',
|
|
140
|
+
'`bin/security/`, hook registration, workflows or the approval records themselves) ',
|
|
141
|
+
'and there is **no current acknowledgement** in `.planning/approvals/`.',
|
|
142
|
+
'',
|
|
143
|
+
'**This check does not block it, on purpose.** A git-tracked approval file cannot carry ',
|
|
144
|
+
'authorization: anyone who can push can write one.',
|
|
145
|
+
'',
|
|
146
|
+
'Authorization comes from branch protection — a required pull request plus required ',
|
|
147
|
+
'status checks on `main` and `develop`.',
|
|
148
|
+
'',
|
|
149
|
+
'To record a human acknowledgement (integrity-checked, 72h, bound to this release):',
|
|
150
|
+
'',
|
|
151
|
+
'```bash',
|
|
152
|
+
'node bin/governance/approve.js "what you reviewed and why it is safe"',
|
|
153
|
+
'```',
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { main, parseArgs };
|
|
162
|
+
|
|
163
|
+
if (require.main === module) process.exit(main());
|