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
package/README.md
CHANGED
|
@@ -6,13 +6,55 @@
|
|
|
6
6
|
|
|
7
7
|
## Latest release
|
|
8
8
|
|
|
9
|
-
**v11.9.
|
|
9
|
+
**v11.9.2** (2026-08-16) — Correctness: the config gate can now fail, the audit chain no longer
|
|
10
|
+
forks under concurrent writers, trace retrieval works, and the cost ledger has one record shape.
|
|
11
|
+
**Contains a breaking change to the dashboard HTTP surface** (loopback-only) — see the BREAKING
|
|
12
|
+
section in [CHANGELOG.md](./CHANGELOG.md), or [RELEASENOTES.md](./RELEASENOTES.md) for
|
|
13
|
+
human-readable notes.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What is actually enforced
|
|
18
|
+
|
|
19
|
+
Read this before the install instructions. MindForge ships a large corpus of agent
|
|
20
|
+
instructions — commands, skills, personas, protocols — and those are advisory: they work by
|
|
21
|
+
being in the model's context, and a model can decline them. The parts that would *block* an
|
|
22
|
+
action are hooks, and **no install channel currently registers them.**
|
|
23
|
+
|
|
24
|
+
| Capability | Plugin channel | `npx` channel |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Slash commands | Yes | Yes |
|
|
27
|
+
| Skills / personas / protocol docs | Yes | Yes |
|
|
28
|
+
| Subagents | Yes | Yes |
|
|
29
|
+
| Audit hash-chain (`bin/verify-audit.js`) | Yes | Yes |
|
|
30
|
+
| **Hooks enforced (can block a tool call)** | **No** | **No** |
|
|
31
|
+
|
|
32
|
+
Why, specifically:
|
|
33
|
+
|
|
34
|
+
- **No hook configuration ships, and nothing generates one.** `package.json` `files[]` has 48
|
|
35
|
+
entries and none contains `settings`, so neither `.claude/settings.json` nor
|
|
36
|
+
`.agent/settings.json` is published. All references to those paths in `bin/` are reads or
|
|
37
|
+
metadata strings — there is no code that writes or merges one. `bin/harness-audit.js:335`
|
|
38
|
+
even offers "wire trust-gate + block-no-verify into both …" as a *fix suggestion*, auditing a
|
|
39
|
+
wiring nothing creates.
|
|
40
|
+
- **The plugin channel's hooks additionally crash when fired.**
|
|
41
|
+
`plugins/mindforge/scripts/run-with-flags.js:24` requires `./lib/hook-flags`, and
|
|
42
|
+
`plugins/mindforge/scripts/lib/` does not exist in the published plugin. Running the
|
|
43
|
+
dispatcher gives `Error: Cannot find module './lib/hook-flags'` and exit 1. The module it
|
|
44
|
+
needs does exist at `.agent/hooks/lib/hook-flags.js`; it was never copied in.
|
|
45
|
+
|
|
46
|
+
So treat MindForge as **governance-by-convention plus a tamper-evident audit log**, not as a
|
|
47
|
+
policy enforcement point. Installing it also expands your repository's trust boundary by a large
|
|
48
|
+
volume of agent instructions — review what you install. Making hook registration real per
|
|
49
|
+
harness is the headline goal of v12; the audit chain is genuinely verifiable today
|
|
50
|
+
(`node bin/verify-audit.js`).
|
|
10
51
|
|
|
11
52
|
---
|
|
12
53
|
|
|
13
54
|
## Install
|
|
14
55
|
|
|
15
|
-
|
|
56
|
+
Claude Code plugin marketplace (no project files written). **Note:** the plugin's hooks do not
|
|
57
|
+
fire — see *What is actually enforced* above. Slash commands, skills and subagents do work.
|
|
16
58
|
|
|
17
59
|
```bash
|
|
18
60
|
/plugin marketplace add sairam0424/MindForge
|
|
@@ -27,6 +69,13 @@ npx mindforge-cc@latest --claude --local
|
|
|
27
69
|
|
|
28
70
|
All install channels (global, local, Antigravity, Cursor, Copilot, Gemini CLI, MCP server, combined runtimes, `--minimal`): see [docs/getting-started.md](docs/getting-started.md).
|
|
29
71
|
|
|
72
|
+
**Upgrading from 11.9.x?** The installer does not overwrite an existing
|
|
73
|
+
`.mindforge/MINDFORGE-SCHEMA.json`, so 11.9.2's armed config validator keeps the older
|
|
74
|
+
permissive schema on a plain upgrade. Run with `--force` if you want the stricter gate. The
|
|
75
|
+
daily cost cap declared as `[COST_HARD_LIMIT_USD]` in `MINDFORGE.md` was **not enforced** in
|
|
76
|
+
11.9.2; 11.9.3 arms it. An upgrade never rewrites an existing `MINDFORGE.md`, so if yours
|
|
77
|
+
predates the key the cap stays off — add `[COST_HARD_LIMIT_USD] = 25.00` to turn it on.
|
|
78
|
+
|
|
30
79
|
---
|
|
31
80
|
|
|
32
81
|
## Verify
|
package/RELEASENOTES.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## v11.9.2 — 2026-08-16 — Correctness: config gate, audit chain, retrieval, cost ledger
|
|
4
|
+
|
|
5
|
+
### What's New
|
|
6
|
+
Nothing. This is a correctness release: four things that were reported as working were measured
|
|
7
|
+
and found not to be, and a fifth — the plugin channel's hooks — is now documented as broken
|
|
8
|
+
instead of advertised as the fastest path.
|
|
9
|
+
|
|
10
|
+
### ⚠️ Breaking (under a patch bump)
|
|
11
|
+
The dashboard's HTTP surface changed shape. `detail` is gone from 5 endpoints and raw errno
|
|
12
|
+
strings from 10 more; `correlation_id` is added to 15; malformed bodies now return
|
|
13
|
+
`application/json` instead of an HTML error page. Those fields were leaking absolute filesystem
|
|
14
|
+
paths — and so the operator's username and home directory — into an unauthenticated response.
|
|
15
|
+
The dashboard also now **exits** on an unhandled rejection or uncaught exception where 11.9.1
|
|
16
|
+
logged and continued, so supervised deployments will see restarts instead of silent errors.
|
|
17
|
+
The surface is loopback-only, so this affects you only if you script against it.
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
- **`security-scan` could not fail.** It reported `MINDFORGE.md valid — 0 settings configured`
|
|
21
|
+
and exited 0 on every input, because both config parsers used a plain `KEY=value` regex while
|
|
22
|
+
the registry declares bracketed `[KEY] = value`. All 43 parameters were invisible. One shared
|
|
23
|
+
parser now reads both forms, and the schema has real `required` keys. Three CI gates go from
|
|
24
|
+
unfailable to failable — on fresh installs and `--force` reinstalls; a plain upgrade keeps
|
|
25
|
+
your existing schema.
|
|
26
|
+
- **The audit hash chain forked under concurrent writers.** No `.jsonl` append took a lock, and
|
|
27
|
+
the chain head was cached in-process and never invalidated — so a second process's append made
|
|
28
|
+
the first keep chaining from a superseded hash. Both halves are fixed: a fail-closed lock, and
|
|
29
|
+
a cached head that carries the file size witnessing it is still the tail. A lock alone was
|
|
30
|
+
measured insufficient. 8 concurrent appenders went from 199 broken links to 0.
|
|
31
|
+
- **44.4% of trace content was unsearchable and every multi-word query returned nothing.** The
|
|
32
|
+
index was keyed on `trace_id` rather than the row's primary key, so each span evicted the
|
|
33
|
+
previous one; and whole queries were wrapped as a single FTS phrase, so one absent term zeroed
|
|
34
|
+
the result. 2,270 of 5,117 rows are recovered losslessly and queries are now tokenised and
|
|
35
|
+
ranked. The shipped retrieval eval had zero callers and now runs as `npm run eval:retrieval`.
|
|
36
|
+
- **The cost ledger reported two different totals for one concept** ($13.73 vs $0.00) because
|
|
37
|
+
writer and reader used different field names — and the test wrote the reader's name, so the
|
|
38
|
+
mismatch passed. One record shape now, and the configured ledger path no longer points at a
|
|
39
|
+
file that never existed.
|
|
40
|
+
- **The daily cost cap is declared but not enforced**, and is now labelled that way in
|
|
41
|
+
`MINDFORGE.md` and the schema rather than described as non-overridable. Wiring it is COST-02.
|
|
42
|
+
|
|
43
|
+
### Honesty
|
|
44
|
+
`README.md` now carries a per-channel capability table with an explicit **Hooks enforced: No**
|
|
45
|
+
row for both install channels, because no hook configuration ships and nothing generates one.
|
|
46
|
+
The plugin channel's dispatcher additionally fails with `Cannot find module './lib/hook-flags'`
|
|
47
|
+
on every fire. Treat MindForge as governance-by-convention plus a verifiable audit log, not as a
|
|
48
|
+
policy enforcement point. Making enforcement real per harness is the goal of v12.
|
|
49
|
+
|
|
50
|
+
### Stats
|
|
51
|
+
105 test files, 103 pass, 2 environment-dependent skips. Four suites that could not report
|
|
52
|
+
failure now can. Audit chain verifies clean. Tarball 1,973 entries.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
3
56
|
## v11.9.0 — 2026-07-27 — Bedrock Provider + Full Dry-Run Audit
|
|
4
57
|
|
|
5
58
|
### What's New
|
|
@@ -569,7 +622,7 @@ You must address these before upgrading from v9.x:
|
|
|
569
622
|
|--------|--------|-----------|
|
|
570
623
|
| `better-sqlite3` removed, replaced by `sql.js` | If you imported or extended VectorHub internals, the constructor API has changed | Use `createVectorHub()` factory function instead of `new VectorHub()` |
|
|
571
624
|
| VectorHub uses FTS4 (not FTS5) | FTS5-specific syntax (e.g., `BM25`) no longer available | Use standard FTS4 `MATCH` queries; ranking is handled internally |
|
|
572
|
-
| SDK `memory.ts` rewritten | If you imported memory utilities from `../../bin/` paths, those imports no longer resolve | Import from
|
|
625
|
+
| SDK `memory.ts` rewritten | If you imported memory utilities from `../../bin/` paths, those imports no longer resolve | Import from `mindforge-sdk` directly — all public APIs are re-exported |
|
|
573
626
|
| Dashboard endpoints require auth | Unauthenticated requests to `/api/steering`, `/api/approve`, and SSE mutators now return 401 | Set `MINDFORGE_DASHBOARD_TOKEN` env var; pass as `Authorization: Bearer <token>` |
|
|
574
627
|
| `sync-jira` / `sync-confluence` CLI routes removed | These were stubs with no implementation | Remove any scripts that invoke these commands |
|
|
575
628
|
| Prompt injection blocklist removed | The regex-based `BLOCKED_PATTERNS` array no longer exists | Use the structured action allowlist in `bin/governance/action-allowlist.js` |
|
|
@@ -661,7 +714,7 @@ The database file format is compatible. No data migration is required.
|
|
|
661
714
|
import { MemoryStore } from '../../bin/memory/store';
|
|
662
715
|
|
|
663
716
|
// After (v10) — public SDK exports
|
|
664
|
-
import { MemoryStore } from '
|
|
717
|
+
import { MemoryStore } from 'mindforge-sdk';
|
|
665
718
|
```
|
|
666
719
|
|
|
667
720
|
### Step 5: Remove references to deleted commands
|
package/SECURITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Security Policy
|
|
2
2
|
|
|
3
|
-
> **Current version:** 11.9.
|
|
3
|
+
> **Current version:** 11.9.3 | **npm audit:** 0 vulnerabilities across root, sdk, mcp-server
|
|
4
4
|
|
|
5
5
|
## Supported Versions
|
|
6
6
|
|
|
@@ -66,7 +66,9 @@ We follow responsible disclosure practices. We will credit reporters in the rele
|
|
|
66
66
|
|
|
67
67
|
### Audit & Integrity
|
|
68
68
|
|
|
69
|
-
- **
|
|
69
|
+
- **Hash-chained audit log (SHA-256 back-links)** — Every entry in `AUDIT.jsonl` carries the SHA-256
|
|
70
|
+
hash of the previous entry. Not a Merkle tree: there is no hash tree and no inclusion proof, so
|
|
71
|
+
"Merkle" was the wrong word for it. What it detects, and does not, is measured below.
|
|
70
72
|
- **AuditWriter with buffered writes** — Atomic append operations prevent partial writes from corrupting the log.
|
|
71
73
|
- **Log rotation with archival** — AUDIT.jsonl auto-archives beyond 5000 lines with gzip compression, preventing unbounded disk growth.
|
|
72
74
|
- **npm provenance** — Published packages include SLSA Build Level 2 attestation via `--provenance`, proving the package was built from the stated source commit in CI.
|
|
@@ -143,7 +145,24 @@ For security questions that are not vulnerability reports, open a GitHub Discuss
|
|
|
143
145
|
Tier-3 trust in v11.x uses **in-process key simulation** (`bin/governance/ztai-manager.js` `SecureEnclaveProvider`). Key material resides in the Node.js heap — it is NOT hardware-isolated. A real TPM/HSM provider is planned for v12.x. **Do not use Tier-3 trust for production credential workflows in v11.x.**
|
|
144
146
|
|
|
145
147
|
## Audit Log Tamper Evidence
|
|
146
|
-
The
|
|
148
|
+
The hash chain (`bin/governance/audit-hash.js`) is real and independently verifiable — `node
|
|
149
|
+
bin/verify-audit.js` recomputes it and exits 1 on a break. Measured on a 200-entry chain taken from
|
|
150
|
+
the head of a live log:
|
|
151
|
+
|
|
152
|
+
| Attack | Detected? | Evidence |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| Mutate a middle entry | **YES** | `BROKEN at entry 79: hash mismatch (entry mutated)`, exit 1 |
|
|
155
|
+
| Delete a middle entry | **YES** | `BROKEN at entry 79: previous_hash mismatch`, exit 1 |
|
|
156
|
+
| **Truncate the tail** | **NO** | 40 entries removed from the end → `audit chain valid: 160 entries`, **exit 0** |
|
|
157
|
+
| Replay identical entries | **NO** | the chain constrains content and ordering, not multiplicity |
|
|
158
|
+
|
|
159
|
+
The truncation gap is inherent to a bare hash chain: each entry commits to its predecessor, so a
|
|
160
|
+
prefix of a valid chain is itself a valid chain. Closing it needs a commitment to LENGTH — an external
|
|
161
|
+
anchor, a signed length attestation, or a witnessed head — none of which ships today.
|
|
162
|
+
|
|
163
|
+
So the accurate claim is **"detects mutation and mid-file deletion"**, not "immutable" or
|
|
164
|
+
"non-repudiable". Restrict OS-level write access to `.planning/AUDIT.jsonl`; the chain does not
|
|
165
|
+
substitute for filesystem permissions.
|
|
147
166
|
|
|
148
167
|
## Agent Dispatch (spawn mode)
|
|
149
168
|
`bin/spawn-agent.js` spawn mode exits with an error in v11.x — real agent dispatch is not yet implemented at the shell level. Use Claude Code slash commands (`/mindforge:auto`, `/mindforge:next`) to dispatch agents.
|
|
@@ -20,6 +20,7 @@ const fs = require('fs');
|
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const crypto = require('crypto');
|
|
22
22
|
const { hashAuditEntry } = require('../governance/audit-hash');
|
|
23
|
+
const { withFileLock } = require('../utils/file-lock');
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Computes the SHA-256 hash of an entry chained to its predecessor (UC-04).
|
|
@@ -57,19 +58,20 @@ function readLastHash(auditPath) {
|
|
|
57
58
|
// sync write gives in-process consumers (e.g. StuckMonitor, which is fed the event
|
|
58
59
|
// object directly but may also re-read the file) immediate, durable data.
|
|
59
60
|
//
|
|
60
|
-
// Chain head caching: re-reading the file's tail on every append is O(file) — bad
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
61
|
+
// Chain head caching: re-reading the file's tail on every append is O(file) — bad on
|
|
62
|
+
// hot paths. We keep a per-path in-memory head (Map keyed by the RESOLVED absolute
|
|
63
|
+
// path) as { hash, size }, where `size` is the file size immediately after our own
|
|
64
|
+
// write. The size is the WITNESS that the cache is still the true tail: if the file is
|
|
65
|
+
// still exactly that long, nobody else appended and the cached hash is the tail (O(1));
|
|
66
|
+
// if it differs, another process appended and we MUST re-seed from disk.
|
|
66
67
|
//
|
|
67
|
-
// Concurrency:
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
68
|
+
// Concurrency (LOCK-01): the seed-through-write sequence below runs inside
|
|
69
|
+
// withFileLock, so no other process can read the head, or append, while we hold it.
|
|
70
|
+
// The size check is what makes the cache safe across processes — a lock alone is NOT
|
|
71
|
+
// enough, because a warm cache would still hand back a hash that a lock-respecting
|
|
72
|
+
// second writer has since superseded, forking the chain. Verified: lock-only leaves
|
|
73
|
+
// 4-6 link breaks per 8-process run; lock + size-validated reseed leaves 0.
|
|
74
|
+
const _lastHashCache = new Map(); // resolvedPath -> { hash, size }
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* Synchronously appends ONE hash-chained, durable entry to an audit JSONL file.
|
|
@@ -96,30 +98,43 @@ function appendAuditEntrySync(auditPath, event) {
|
|
|
96
98
|
timestamp: event.timestamp || new Date().toISOString(),
|
|
97
99
|
};
|
|
98
100
|
|
|
99
|
-
// 2.
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// 2-5. LOCK-01: seed-through-write is ONE critical section. The lock opens BEFORE
|
|
102
|
+
// the head is read and closes AFTER the fsync, so no other process can read the
|
|
103
|
+
// same head, or interleave an append, while we hold it. maxTries is raised above
|
|
104
|
+
// the module default because failing closed here DROPS an audit entry: at 16
|
|
105
|
+
// concurrent writers the default ~1s ceiling loses ~35% of entries, 150 tries
|
|
106
|
+
// loses none.
|
|
107
|
+
return withFileLock(resolved, () => {
|
|
108
|
+
// 2. Seed previous_hash from the warm cache ONLY if the file is still exactly as
|
|
109
|
+
// long as we left it (proof nobody else appended); otherwise re-seed from disk.
|
|
110
|
+
const cached = _lastHashCache.get(resolved);
|
|
111
|
+
let onDiskSize = -1;
|
|
112
|
+
try { onDiskSize = fs.statSync(resolved).size; } catch { onDiskSize = -1; }
|
|
113
|
+
const previous_hash = (cached && cached.size === onDiskSize)
|
|
114
|
+
? cached.hash
|
|
115
|
+
: readLastHash(resolved);
|
|
104
116
|
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
// 3. Compute _hash over {...stamped, previous_hash} WITHOUT _hash in the material.
|
|
118
|
+
const _hash = hashEntry(stamped, previous_hash);
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
// 4. Write {...stamped, previous_hash, _hash} as one JSON line, durably+synchronously
|
|
121
|
+
// (openSync('a') + writeSync + fsyncSync + closeSync — mirrors appendDurableSync).
|
|
122
|
+
const chained = { ...stamped, previous_hash, _hash };
|
|
123
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
124
|
+
const fd = fs.openSync(resolved, 'a');
|
|
125
|
+
let sizeAfter = -1;
|
|
126
|
+
try {
|
|
127
|
+
fs.writeSync(fd, JSON.stringify(chained) + '\n');
|
|
128
|
+
fs.fsyncSync(fd);
|
|
129
|
+
sizeAfter = fs.fstatSync(fd).size; // exact new length, one syscall, fd already open
|
|
130
|
+
} finally {
|
|
131
|
+
fs.closeSync(fd);
|
|
132
|
+
}
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
// 5. Advance the in-process chain head (hash + its witness) and return the entry.
|
|
135
|
+
_lastHashCache.set(resolved, { hash: _hash, size: sizeAfter });
|
|
136
|
+
return chained;
|
|
137
|
+
}, { maxTries: 150, label: 'audit' });
|
|
123
138
|
}
|
|
124
139
|
|
|
125
140
|
module.exports = { appendAuditEntrySync };
|
|
@@ -117,6 +117,12 @@ class AutoRunner {
|
|
|
117
117
|
this.phase = String(options.phase);
|
|
118
118
|
this.isHeadless = options.headless || false;
|
|
119
119
|
this.isPaused = false;
|
|
120
|
+
// Optional async function(task) that performs the actual work for one task. When absent,
|
|
121
|
+
// executeWave() aborts the wave rather than writing task_completed entries for work that would
|
|
122
|
+
// not run — see the comment there. Deliberately NOT defaulted to a no-op: bin/autonomous/
|
|
123
|
+
// wave-executor.js:133 defaults its own `executor` to `async () => {}`, and a silent no-op
|
|
124
|
+
// default is exactly how a wave came to report every task fulfilled while dispatching nothing.
|
|
125
|
+
this.taskExecutor = typeof options.taskExecutor === 'function' ? options.taskExecutor : null;
|
|
120
126
|
|
|
121
127
|
// Paths
|
|
122
128
|
const planningDir = path.join(process.cwd(), '.planning');
|
|
@@ -306,10 +312,30 @@ class AutoRunner {
|
|
|
306
312
|
runPreFlight() {
|
|
307
313
|
console.log('🔍 Running pre-flight checks...');
|
|
308
314
|
|
|
309
|
-
// UC-01: fail closed on version drift before any wave executes
|
|
315
|
+
// UC-01: fail closed on version drift before any wave executes.
|
|
316
|
+
//
|
|
317
|
+
// The root MUST be MindForge's own install root, NOT process.cwd(). This check compares
|
|
318
|
+
// package.json against .mindforge/config.json, sdk/package.json and MINDFORGE.md — all
|
|
319
|
+
// MindForge's own manifests. Pointed at a consumer's cwd it takes THEIR application
|
|
320
|
+
// version as canonical and reports MindForge's own config as drift. Measured, with an app
|
|
321
|
+
// at 1.0.0 and MindForge at 11.9.2:
|
|
322
|
+
// [".mindforge/config.json declares 11.9.2 but canonical (package.json) is 1.0.0"]
|
|
323
|
+
// and a consumer with no package.json at all fails closed on a missing canonical. Only a
|
|
324
|
+
// project whose app version coincidentally equalled MindForge's would have passed.
|
|
325
|
+
//
|
|
326
|
+
// This has never fired in a shipped build, because nothing constructs AutoRunner today
|
|
327
|
+
// (verified: no `new AutoRunner` anywhere in bin/; the only requirer is
|
|
328
|
+
// tests/wave-timeout-rollback.test.js, for the pure helpers). It is a landmine that arms
|
|
329
|
+
// the moment the runner is wired — which DEL-02 proposes doing — so it is disarmed here
|
|
330
|
+
// rather than left for whoever wires it to find in the field.
|
|
331
|
+
//
|
|
332
|
+
// __dirname/../.. is MindForge's root in both layouts: <repo> from a checkout, and
|
|
333
|
+
// <project>/node_modules/mindforge-cc when installed. In the installed shape
|
|
334
|
+
// .mindforge/config.json is absent (the installer writes that into the CONSUMER project),
|
|
335
|
+
// which reads as null and is SKIPPED rather than counted as drift — verified.
|
|
310
336
|
try {
|
|
311
337
|
const { assertVersionConsistency } = require('../utils/version-check');
|
|
312
|
-
assertVersionConsistency(
|
|
338
|
+
assertVersionConsistency(path.resolve(__dirname, '..', '..'));
|
|
313
339
|
} catch (e) {
|
|
314
340
|
throw new Error(`[pre-flight] ${e.message}`);
|
|
315
341
|
}
|
|
@@ -357,6 +383,40 @@ class AutoRunner {
|
|
|
357
383
|
|
|
358
384
|
console.log(`\n⚡ Wave ${waveNum}/${this.waves.length}: ${pending.length} tasks (concurrency: ${maxConcurrency})`);
|
|
359
385
|
if (idcStatus.action === 'UPGRADE_MIR') console.log(` [IDC-ACTIVE] MIR Override: ${idcStatus.new_mir}`);
|
|
386
|
+
|
|
387
|
+
// REFUSE TO FABRICATE. The task body below used to be, in full:
|
|
388
|
+
//
|
|
389
|
+
// try {
|
|
390
|
+
// this.writeAudit({ event: 'task_started', ... });
|
|
391
|
+
// this.writeAudit({ event: 'task_completed', ... duration_ms: Date.now() - taskStart });
|
|
392
|
+
// this.completedTasks.add(task.id);
|
|
393
|
+
//
|
|
394
|
+
// Nothing between the two writes. No dispatch. So every task was recorded as completed with a
|
|
395
|
+
// duration of ~0ms, the catch block was unreachable because nothing could throw, and the
|
|
396
|
+
// hash-chained audit log — the one component of this project that is genuinely production-grade
|
|
397
|
+
// and independently verifiable — became a tamper-evident record of statements that were false.
|
|
398
|
+
// A verifiable false record is worse than no record: it survives verify-audit and carries the
|
|
399
|
+
// authority of the chain.
|
|
400
|
+
//
|
|
401
|
+
// Measured before this change: task_completed = 0 in the live 3056-entry chain, and no module
|
|
402
|
+
// under bin/ requires auto-runner (every repo-wide match is a comment, a test, or an
|
|
403
|
+
// "extracted from" note). So the defect was LATENT — the code would have lied the first time
|
|
404
|
+
// anything invoked it, and nothing had. That is why this is a refusal rather than a cleanup:
|
|
405
|
+
// there is no contamination to repair, only a trap to disarm.
|
|
406
|
+
//
|
|
407
|
+
// A wave with no executor now writes ONE honest entry and throws, instead of N false ones.
|
|
408
|
+
if (typeof this.taskExecutor !== 'function') {
|
|
409
|
+
this.writeAudit({
|
|
410
|
+
event: 'wave_aborted', phase: this.phase, wave: waveNum, task_count: pending.length,
|
|
411
|
+
reason: 'no task executor is wired, so no task can run. Refusing to write task_completed '
|
|
412
|
+
+ 'entries for work that would not be performed — see auto-runner.executeWave.',
|
|
413
|
+
});
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Wave ${waveNum} cannot execute: no task executor wired. Pass { taskExecutor } to the `
|
|
416
|
+
+ 'AutoRunner constructor, or drive waves through bin/autonomous/wave-executor.js. '
|
|
417
|
+
+ 'Refusing to record completions for work that would not run.');
|
|
418
|
+
}
|
|
419
|
+
|
|
360
420
|
this.writeAudit({ event: 'wave_started', phase: this.phase, wave: waveNum, task_count: pending.length });
|
|
361
421
|
|
|
362
422
|
const semaphore = new Semaphore(maxConcurrency);
|
|
@@ -368,6 +428,9 @@ class AutoRunner {
|
|
|
368
428
|
console.log(` → Task: ${task.name || task.id}`);
|
|
369
429
|
try {
|
|
370
430
|
this.writeAudit({ event: 'task_started', phase: this.phase, wave: waveNum, task_id: task.id, task_name: task.name || task.id });
|
|
431
|
+
// The dispatch that was missing. Its absence is what made the catch below unreachable and
|
|
432
|
+
// every duration_ms ~0; task_completed is now written only after real work returns.
|
|
433
|
+
await this.taskExecutor(task);
|
|
371
434
|
this.writeAudit({ event: 'task_completed', phase: this.phase, wave: waveNum, task_id: task.id, task_name: task.name || task.id, duration_ms: Date.now() - taskStart });
|
|
372
435
|
this.completedTasks.add(task.id);
|
|
373
436
|
return { taskId: task.id, status: 'fulfilled' };
|
package/bin/change-classifier.js
CHANGED
|
@@ -9,16 +9,55 @@
|
|
|
9
9
|
const { execSync, execFileSync } = require('child_process');
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
|
|
12
|
+
// Matched with String.prototype.startsWith against repo-relative diff paths.
|
|
13
|
+
//
|
|
14
|
+
// Two groups, because this file SHIPS (package.json files[] includes bin/) and therefore runs
|
|
15
|
+
// against two different trees:
|
|
16
|
+
//
|
|
17
|
+
// (a) CONSUMER paths — a project that installed MindForge. Measured against MindForge's own
|
|
18
|
+
// tree these match ZERO tracked files, which is correct: they describe the consumer's
|
|
19
|
+
// layout, not the framework's. Do not remove them.
|
|
20
|
+
// (b) FRAMEWORK paths — MindForge's OWN trust surface, which control-plane.yml classifies on
|
|
21
|
+
// every push and PR to this repo. These were absent, so the detector protected the five
|
|
22
|
+
// markdown files under .mindforge/governance/ while leaving the 15 executable modules
|
|
23
|
+
// under bin/governance/ — the audit hasher, the verifier, RBAC, the policy engine and
|
|
24
|
+
// approve.js itself — unclassified. Editing the prose ABOUT governance tripped Tier 3;
|
|
25
|
+
// editing the code that enforces it did not.
|
|
12
26
|
const SENSITIVE_PATHS = [
|
|
27
|
+
// (a) consumer-project layout
|
|
13
28
|
'auth/',
|
|
14
29
|
'payment/',
|
|
15
30
|
'security/',
|
|
31
|
+
// (b) MindForge's own trust surface
|
|
32
|
+
'bin/governance/', // audit-hash, audit-verifier, rbac, policy-engine, approve
|
|
33
|
+
'bin/security/', // trust-gate-hook and friends — NOT matched by 'security/'
|
|
34
|
+
'bin/hooks/', // instinct-capture-hook, context-monitor
|
|
35
|
+
'bin/models/',
|
|
36
|
+
'.agent/hooks/', // the hook dispatcher the installer copies
|
|
37
|
+
'.claude/settings.json', // hook registration
|
|
38
|
+
'.agent/settings.json',
|
|
39
|
+
'.planning/approvals/', // the approval records themselves
|
|
16
40
|
'.github/workflows/',
|
|
17
|
-
'.mindforge/governance/'
|
|
18
|
-
'bin/models/'
|
|
41
|
+
'.mindforge/governance/'
|
|
19
42
|
];
|
|
20
43
|
|
|
44
|
+
// Scanned against the DIFF CONTENT, so security-critical code in an innocuously-named file still
|
|
45
|
+
// reaches Tier 3 — that is the stated purpose of "Signal B" in
|
|
46
|
+
// .mindforge/governance/change-classifier.md.
|
|
47
|
+
//
|
|
48
|
+
// The doc lists 19 patterns; measured, only 6 of them were detected. The second group below was
|
|
49
|
+
// specified and absent: argon2 (a password hasher), the jose signing pair, paypal.,
|
|
50
|
+
// createCipheriv/createDecipheriv and crypto.subtle (cipher construction), encrypt(/decrypt(,
|
|
51
|
+
// role.*permission and hasPermission (authorization), and SET ROLE / GRANT (SQL privilege
|
|
52
|
+
// changes). tests/governance.test.js pins every documented pattern against the live set, so the
|
|
53
|
+
// doc and this array cannot drift apart again.
|
|
54
|
+
//
|
|
55
|
+
// Adding them cost nothing measurable: over the last 40 commits they caught ZERO additional
|
|
56
|
+
// changes, because anything they would flag was already Tier 3 by path or by one of the broad
|
|
57
|
+
// substring patterns above. So this closes a documented gap without moving the Tier-3 rate, which
|
|
58
|
+
// sat at 50% (path OR pattern) before and after.
|
|
21
59
|
const SENSITIVE_PATTERNS = [
|
|
60
|
+
// broad substring signals
|
|
22
61
|
/jwt/i,
|
|
23
62
|
/bcrypt/i,
|
|
24
63
|
/stripe/i,
|
|
@@ -26,18 +65,89 @@ const SENSITIVE_PATTERNS = [
|
|
|
26
65
|
/password/i,
|
|
27
66
|
/secret/i,
|
|
28
67
|
/token/i,
|
|
29
|
-
/PII
|
|
68
|
+
/PII/,
|
|
69
|
+
// the specified-but-missing set
|
|
70
|
+
/argon2/i,
|
|
71
|
+
/jose\.(sign|verify)/i,
|
|
72
|
+
/paypal\./i,
|
|
73
|
+
/createCipheriv/,
|
|
74
|
+
/createDecipheriv/,
|
|
75
|
+
/crypto\.subtle/,
|
|
76
|
+
/\bencrypt\(/,
|
|
77
|
+
/\bdecrypt\(/,
|
|
78
|
+
/role.*permission/i,
|
|
79
|
+
/hasPermission/,
|
|
80
|
+
/\bSET ROLE\b/i,
|
|
81
|
+
/\bGRANT\b/
|
|
30
82
|
];
|
|
31
83
|
|
|
84
|
+
/** True if `rev` resolves to an object in this clone (false on a shallow/partial fetch). */
|
|
85
|
+
function revExists(rev) {
|
|
86
|
+
try {
|
|
87
|
+
execFileSync('git', ['rev-parse', '--verify', '--quiet', `${rev}^{commit}`], { stdio: 'pipe' });
|
|
88
|
+
return true;
|
|
89
|
+
} catch { return false; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The pre-push tip GitHub reports for a push event, or '' if unavailable. */
|
|
93
|
+
function pushBefore() {
|
|
94
|
+
if (process.env.MINDFORGE_PUSH_BEFORE) return process.env.MINDFORGE_PUSH_BEFORE;
|
|
95
|
+
// Read the event payload rather than requiring the workflow to thread a variable through.
|
|
96
|
+
// A fix that depends on someone remembering `env: BEFORE: ${{ github.event.before }}` is a
|
|
97
|
+
// fix that silently reverts the first time a workflow is copied.
|
|
98
|
+
const p = process.env.GITHUB_EVENT_PATH;
|
|
99
|
+
if (!p || !fs.existsSync(p)) return '';
|
|
100
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')).before || ''; } catch { return ''; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the commit range to classify.
|
|
105
|
+
*
|
|
106
|
+
* A push is a RANGE, not a commit. The old code used HEAD~1..HEAD whenever GITHUB_BASE_REF was
|
|
107
|
+
* unset, so on a push it inspected only the tip. Measured: two commits where the first adds
|
|
108
|
+
* auth/login.js with a hardcoded password and the second is docs-only classify as TIER=1 on a
|
|
109
|
+
* push and TIER=3 as a PR — the sensitive file rides in completely unclassified. control-plane.yml
|
|
110
|
+
* triggers on `push: [main, develop]`, and this repo has allow_rebase_merge enabled, so the
|
|
111
|
+
* multi-commit push is the normal case, not an edge case.
|
|
112
|
+
*
|
|
113
|
+
* @returns {{range:string, how:string}}
|
|
114
|
+
* @throws {Error} with .failClosed when a CI push range cannot be resolved.
|
|
115
|
+
*/
|
|
116
|
+
function resolveRange() {
|
|
117
|
+
// Pull request: three-dot diffs against the MERGE-BASE, so a branch that is merely behind
|
|
118
|
+
// its base does not pick up base-only changes. (Two-dot here caused Tier-3 false positives.)
|
|
119
|
+
if (process.env.GITHUB_BASE_REF) {
|
|
120
|
+
return { range: `origin/${process.env.GITHUB_BASE_REF}...HEAD`, how: 'pull_request' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const inCiPush = process.env.GITHUB_EVENT_NAME === 'push';
|
|
124
|
+
if (inCiPush) {
|
|
125
|
+
const before = pushBefore();
|
|
126
|
+
const isBranchCreation = /^0{40}$/.test(before);
|
|
127
|
+
if (before && !isBranchCreation && revExists(before)) {
|
|
128
|
+
return { range: `${before}..HEAD`, how: 'push' };
|
|
129
|
+
}
|
|
130
|
+
// Fail CLOSED. Every remaining case genuinely cannot be scoped: a new branch has no prior
|
|
131
|
+
// tip, and a shallow clone cannot reach `before`. Falling back to HEAD~1 here is what let
|
|
132
|
+
// a sensitive commit ride in behind a benign tip.
|
|
133
|
+
const why = isBranchCreation ? 'branch creation (before is all-zeros)'
|
|
134
|
+
: !before ? 'no `before` in the push event payload'
|
|
135
|
+
: `\`before\` (${before.slice(0, 12)}) is not present in this clone — shallow fetch?`;
|
|
136
|
+
const err = new Error(`cannot scope the push range: ${why}`);
|
|
137
|
+
err.failClosed = true;
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Local invocation (`mindforge classify`). HEAD~1..HEAD is a developer convenience, not a
|
|
142
|
+
// gate, and must never be reached in CI — the branch above owns every CI push.
|
|
143
|
+
return { range: 'HEAD~1..HEAD', how: 'local' };
|
|
144
|
+
}
|
|
145
|
+
|
|
32
146
|
function classify() {
|
|
33
147
|
try {
|
|
34
|
-
|
|
35
|
-
// Three-dot (...) diffs against the MERGE-BASE, so on a PR branch that is behind its base
|
|
36
|
-
// we see ONLY this branch's own changes — not unrelated commits already on the base.
|
|
37
|
-
// (Two-dot here caused Tier-3 false positives by pulling in base-only changes.)
|
|
38
|
-
const base = process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : 'HEAD~1';
|
|
39
|
-
const range = process.env.GITHUB_BASE_REF ? `${base}...HEAD` : `${base}..HEAD`;
|
|
148
|
+
const { range, how } = resolveRange();
|
|
40
149
|
const diffFiles = execFileSync('git', ['diff', '--name-only', range], { encoding: 'utf8' }).split('\n').filter(Boolean);
|
|
150
|
+
if (process.env.MINDFORGE_CLASSIFY_DEBUG) console.error(`[classify] ${how}: ${range} (${diffFiles.length} file(s))`);
|
|
41
151
|
|
|
42
152
|
// Test and documentation files are excluded from the sensitive-PATTERN scan below: a test
|
|
43
153
|
// asserting on "password"/key patterns, or a doc mentioning secrets, is not a sensitive
|
|
@@ -50,10 +160,16 @@ function classify() {
|
|
|
50
160
|
let reasons = [];
|
|
51
161
|
|
|
52
162
|
// 1. Path-based detection (Tier 3)
|
|
53
|
-
|
|
54
|
-
|
|
163
|
+
// Reports EVERY matched path, not just the first. `find` short-circuited, so a change
|
|
164
|
+
// touching both .github/workflows/ and bin/governance/ was attributed only to the workflow —
|
|
165
|
+
// which made the CI log unable to show that the newly-added framework paths were doing any
|
|
166
|
+
// work, and hid the more interesting of the two matches from the reviewer.
|
|
167
|
+
const matchedPaths = diffFiles.filter(file => SENSITIVE_PATHS.some(p => file.startsWith(p)));
|
|
168
|
+
if (matchedPaths.length) {
|
|
55
169
|
tier = 3;
|
|
56
|
-
|
|
170
|
+
const shown = matchedPaths.slice(0, 5).join(', ');
|
|
171
|
+
const more = matchedPaths.length > 5 ? ` (+${matchedPaths.length - 5} more)` : '';
|
|
172
|
+
reasons.push(`Sensitive path${matchedPaths.length > 1 ? 's' : ''} modified: ${shown}${more}`);
|
|
57
173
|
}
|
|
58
174
|
|
|
59
175
|
// 2. Pattern-based detection in diff (Tier 3) — non-test/doc files only
|
|
@@ -94,13 +210,32 @@ function classify() {
|
|
|
94
210
|
|
|
95
211
|
return tier;
|
|
96
212
|
} catch (err) {
|
|
97
|
-
|
|
98
|
-
//
|
|
213
|
+
// Fail closed: an unscopeable change is treated as maximum risk. Emit the SAME
|
|
214
|
+
// TIER=/REASONS= lines as the success path so every consumer — the workflow's stdout
|
|
215
|
+
// parsing, GITHUB_OUTPUT, and a human reading the log — sees tier 3 rather than nothing.
|
|
216
|
+
// Previously only GITHUB_OUTPUT was written, so anything reading stdout saw no tier at all.
|
|
217
|
+
const reason = err.failClosed
|
|
218
|
+
? `Fail-closed: ${err.message}`
|
|
219
|
+
: `Fail-closed: classification error — ${err.message}`;
|
|
220
|
+
console.log('TIER=3');
|
|
221
|
+
console.log(`REASONS=${reason}`);
|
|
222
|
+
console.error(`❌ Classification could not be scoped — defaulting to Tier 3. ${err.message}`);
|
|
223
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
224
|
+
console.error(`::error title=Change classification failed closed::${reason}`);
|
|
225
|
+
}
|
|
99
226
|
if (process.env.GITHUB_OUTPUT) {
|
|
100
227
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'tier=3\n');
|
|
101
228
|
}
|
|
102
|
-
|
|
229
|
+
// Exit 0 deliberately: this step's contract is to REPORT a tier, and the downstream gate
|
|
230
|
+
// is what blocks. Exiting non-zero here would fail the job before the gate can annotate
|
|
231
|
+
// why. That contract is only honest while a gate actually consumes tier 3 — see
|
|
232
|
+
// tests/change-classifier.test.js, which pins that the tier-3 consumer exists.
|
|
233
|
+
return 3;
|
|
103
234
|
}
|
|
104
235
|
}
|
|
105
236
|
|
|
106
|
-
classify
|
|
237
|
+
module.exports = { classify, resolveRange, revExists, pushBefore, SENSITIVE_PATHS, SENSITIVE_PATTERNS };
|
|
238
|
+
|
|
239
|
+
// Behind a require.main guard so tests can import the pure pieces without triggering a run.
|
|
240
|
+
// bin/mindforge-cli.js:51 spawns this file as a script, which still executes normally.
|
|
241
|
+
if (require.main === module) classify();
|