mindforge-cc 11.4.0 → 11.5.0
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 +13 -0
- package/.agent/hooks/lib/hook-flags.js +78 -0
- package/.agent/hooks/lib/pretooluse-visible-output.js +46 -0
- package/.agent/hooks/mindforge-block-no-verify.js +552 -0
- package/.agent/hooks/mindforge-config-protection.js +144 -0
- package/.agent/hooks/run-with-flags.js +207 -0
- package/.agent/mindforge/checkpoint.md +76 -0
- package/.agent/mindforge/harness-audit.md +59 -0
- package/.agent/mindforge/instinct.md +46 -0
- package/.agent/mindforge/orch-add-feature.md +43 -0
- package/.agent/mindforge/orch-build-mvp.md +48 -0
- package/.agent/mindforge/orch-change-feature.md +45 -0
- package/.agent/mindforge/orch-fix-defect.md +43 -0
- package/.agent/mindforge/orch-refine-code.md +43 -0
- package/.claude/CLAUDE.md +13 -0
- package/.claude/commands/mindforge/checkpoint.md +76 -0
- package/.claude/commands/mindforge/execute-phase.md +47 -6
- package/.claude/commands/mindforge/harness-audit.md +59 -0
- package/.claude/commands/mindforge/instinct.md +46 -0
- package/.claude/commands/mindforge/orch-add-feature.md +43 -0
- package/.claude/commands/mindforge/orch-build-mvp.md +48 -0
- package/.claude/commands/mindforge/orch-change-feature.md +45 -0
- package/.claude/commands/mindforge/orch-fix-defect.md +43 -0
- package/.claude/commands/mindforge/orch-refine-code.md +43 -0
- package/.claude/commands/mindforge/plan-write.md +11 -0
- package/.claude/commands/mindforge/product-spec.md +76 -0
- package/.mindforge/config.json +2 -2
- package/.mindforge/engine/instincts/instinct-schema.md +17 -9
- package/.mindforge/imported-agents.jsonl +10 -0
- package/.mindforge/manifests/install-components.json +36 -0
- package/.mindforge/manifests/install-modules.json +193 -0
- package/.mindforge/manifests/install-profiles.json +57 -0
- package/.mindforge/memory/sync-manifest.json +1 -1
- package/.mindforge/personas/gan-evaluator.md +226 -0
- package/.mindforge/personas/gan-generator.md +151 -0
- package/.mindforge/personas/gan-planner.md +118 -0
- package/.mindforge/personas/harness-optimizer.md +55 -0
- package/.mindforge/personas/loop-operator.md +58 -0
- package/.mindforge/schemas/hooks.schema.json +199 -0
- package/.mindforge/schemas/install-modules.schema.json +44 -0
- package/.mindforge/schemas/install-state.schema.json +95 -0
- package/.mindforge/schemas/plugin.schema.json +75 -0
- package/.mindforge/schemas/provenance.schema.json +31 -0
- package/.mindforge/skills/agent-architecture-audit/SKILL.md +272 -0
- package/.mindforge/skills/continuous-learning/SKILL.md +16 -0
- package/.mindforge/skills/orch-pipeline/SKILL.md +284 -0
- package/.mindforge/skills/writing-plans/SKILL.md +76 -0
- package/CHANGELOG.md +75 -0
- package/MINDFORGE.md +3 -3
- package/RELEASENOTES.md +86 -0
- package/SECURITY.md +16 -0
- package/bin/autonomous/auto-runner.js +46 -5
- package/bin/autonomous/handoff-schema.js +114 -0
- package/bin/autonomous/session-guardian.sh +138 -0
- package/bin/autonomous/supervisor.js +98 -0
- package/bin/change-classifier.js +19 -5
- package/bin/governance/approve.js +61 -28
- package/bin/governance/config-manager.js +3 -1
- package/bin/governance/rbac-manager.js +14 -6
- package/bin/harness-audit.js +520 -0
- package/bin/hooks/instinct-capture-hook.js +16 -1
- package/bin/hooks/lib/detect-project.js +72 -0
- package/bin/installer/harness-adapter-compliance.js +321 -0
- package/bin/installer/install-manifests.js +200 -0
- package/bin/installer/install-state.js +243 -0
- package/bin/installer-core.js +1 -1
- package/bin/learning/instinct-cli.js +359 -0
- package/bin/learning/lib/ssrf-guard.js +252 -0
- package/bin/memory/eis-client.js +31 -10
- package/bin/models/llm-errors.js +79 -0
- package/bin/models/model-client.js +39 -4
- package/bin/models/ollama-provider.js +115 -0
- package/bin/models/openai-provider.js +40 -9
- package/bin/models/profiles-loader.js +147 -0
- package/bin/models/provider-registry.js +59 -0
- package/bin/revops/market-evaluator.js +23 -2
- package/bin/revops/router-steering-v2.js +17 -2
- package/bin/security/trust-boundaries.js +15 -3
- package/bin/utils/readiness-gate.js +169 -0
- package/bin/worktree/engine.js +497 -0
- package/package.json +8 -2
- package/subagents/categories/04-quality-security/.claude-plugin/plugin.json +10 -0
- package/subagents/categories/04-quality-security/go-build-resolver.md +105 -0
- package/subagents/categories/04-quality-security/go-reviewer.md +87 -0
- package/subagents/categories/04-quality-security/python-reviewer.md +109 -0
- package/subagents/categories/04-quality-security/react-build-resolver.md +215 -0
- package/subagents/categories/04-quality-security/react-reviewer.md +167 -0
- package/subagents/categories/04-quality-security/rust-build-resolver.md +159 -0
- package/subagents/categories/04-quality-security/rust-reviewer.md +105 -0
- package/subagents/categories/04-quality-security/silent-failure-hunter.md +67 -0
- package/subagents/categories/04-quality-security/type-design-analyzer.md +58 -0
- package/subagents/categories/04-quality-security/typescript-reviewer.md +126 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [11.5.0] - 2026-06-11 — Governance hardening + autonomous-engine repair (Waves 4–7)
|
|
4
|
+
|
|
5
|
+
This release bundles four waves of work: orchestration primitives, an **inert** manifest
|
|
6
|
+
engine + the new instinct CLI + GAN harness personas, governance/security hardening, and a
|
|
7
|
+
repair that makes the autonomous engine (`/mindforge:auto`) actually functional. Several
|
|
8
|
+
items ship deliberately **inert** (scaffolding present, not wired into any live path) and
|
|
9
|
+
are flagged as such below — they introduce **no behavior change yet**.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Instinct CLI** (`/mindforge:instinct`, `bin/learning/instinct-cli.js`) — deterministic,
|
|
14
|
+
no-LLM management of the JSONL instinct store: `list`, `export`, `import`,
|
|
15
|
+
`promote-candidates` (list + flag), and `prune`.
|
|
16
|
+
- **Typed Inter-Agent Message Protocol** (`bin/autonomous/handoff-schema.js`) — five message
|
|
17
|
+
kinds (`task_handoff`, `query`, `response`, `completed`, `conflict`) with priority levels
|
|
18
|
+
(`low`/`normal`/`high`/`critical`) and message validation. Internal orchestration primitive.
|
|
19
|
+
- **Manifest-driven install resolver** (`bin/installer/install-manifests.js`,
|
|
20
|
+
`.mindforge/manifests/install-*.json`) — profile-to-module expansion and dependency
|
|
21
|
+
detection. Ships **INERT**: the adapter wiring it into `bin/installer-core.js`'s live
|
|
22
|
+
`install()` path is deferred to a future PR. No behavior change.
|
|
23
|
+
- **GAN-style harness personas** (`.mindforge/personas/gan-evaluator.md`, `gan-generator.md`,
|
|
24
|
+
`gan-planner.md`) — fully scoped and documented. Ships **INERT**: not wired to any live
|
|
25
|
+
command or automated workflow. No behavior change.
|
|
26
|
+
- **Governance test coverage** — `tests/trust-verifier.test.js` (7 tests) and
|
|
27
|
+
`tests/rbac-manager.test.js` (8 tests) lock the fail-closed contracts for future
|
|
28
|
+
safety-critical governance changes.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **SSRF import-URL guard** (`bin/learning/lib/ssrf-guard.js`) — closed an IPv6 link-local
|
|
33
|
+
bypass (`fe81`–`fe8f` reachable via string-prefix check) and a symlink path-traversal
|
|
34
|
+
bypass, using numeric bitmask validation and canonical-path checking before system-dir
|
|
35
|
+
validation.
|
|
36
|
+
- **Federated EIS sync** (`bin/memory/eis-client.js`) — `getAuthHeader` no longer throws on
|
|
37
|
+
every call; it now registers a node identity via `ZTAI.registerAgent` and signs with
|
|
38
|
+
`ZTAI.signData`, restoring sync to non-localhost EIS endpoints.
|
|
39
|
+
- **RBAC tier elevation** (`bin/governance/rbac-manager.js`) — `getRolesByTier` now fails
|
|
40
|
+
safely for unregistered agents (no thrown exception) and resolves agent tier via the
|
|
41
|
+
correct ZTAI API.
|
|
42
|
+
- **Autonomous engine — wave crash** (`bin/autonomous/auto-runner.js`) — replaced ZTAI
|
|
43
|
+
singleton misuse (`getIdentity()`, non-existent) with `_getRunnerIdentity()` using the
|
|
44
|
+
real `registerAgent` API; `/mindforge:auto` no longer crashes on every wave.
|
|
45
|
+
- **Autonomous policy gate — fail-open** (`bin/autonomous/auto-runner.js`) — the async
|
|
46
|
+
policy verdict is now `await`ed (`this.policyEngine.evaluate(intent)`); the per-wave gate
|
|
47
|
+
previously always allowed.
|
|
48
|
+
- **Autonomous engine — fail-closed identity** (`bin/autonomous/auto-runner.js`) — if runner
|
|
49
|
+
identity cannot be established, the policy gate now denies and audits (`auto_mode_denied`)
|
|
50
|
+
instead of proceeding ungoverned.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- **Instinct store schema** (`.mindforge/engine/instincts/instinct-schema.md`) — added
|
|
55
|
+
`project_id` (stable scope key) and `source` (`auto-capture`/`manual`/`imported`/
|
|
56
|
+
`observer`), plus origin-based confidence scoring: auto-capture starts at `0.3`, manual
|
|
57
|
+
at `0.7`.
|
|
58
|
+
- **Cost-routing shadow mode** (`bin/revops/router-steering-v2.js`) — arbitrage steering now
|
|
59
|
+
respects `cost_routing.shadow_mode` (default `true`); in observe-only mode `steer()`
|
|
60
|
+
returns `{ shadow, authoritative: false }` with SHADOW vs LIVE logging.
|
|
61
|
+
|
|
62
|
+
### Security
|
|
63
|
+
|
|
64
|
+
- **Tier-3 approvals now fail closed** (`bin/governance/approve.js`) — approvals require GPG
|
|
65
|
+
verification and **throw before writing any record** when no GPG key is configured, unless
|
|
66
|
+
`MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1` is set; unverified approvals are marked
|
|
67
|
+
`verified: false`.
|
|
68
|
+
- **Persona supply-chain scan** (`scripts/ci/validate-assets.js`) — persona asset validation
|
|
69
|
+
now detects dangerous invisible unicode (zero-width, bidi overrides, Unicode tags) across
|
|
70
|
+
`.mindforge/personas` to prevent ASCII-smuggling injection.
|
|
71
|
+
- **Destructive-command detector** (`bin/security/trust-boundaries.js`) — `normalizeShell`
|
|
72
|
+
now strips bare `#` tokens after quote-stripping, blocking quoted-hash evasion (e.g.
|
|
73
|
+
`rm "#" -rf /`).
|
|
74
|
+
- **Instinct import port allowlist** (`bin/learning/lib/ssrf-guard.js`) — `validateImportUrl`
|
|
75
|
+
enforces an `ALLOWED_IMPORT_PORTS` allowlist (`{'', '443'}`), blocking attempts to reach
|
|
76
|
+
internal services (Redis `:6379`, Mongo `:27017`, etc.) on otherwise-allowed public hosts.
|
|
77
|
+
|
|
3
78
|
## [11.4.0] - 2026-06-06 — Claude Code plugin distribution
|
|
4
79
|
|
|
5
80
|
MindForge is now installable as a native **Claude Code plugin** from a marketplace, in
|
package/MINDFORGE.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# MINDFORGE.md — Parameter Registry (v11.
|
|
1
|
+
# MINDFORGE.md — Parameter Registry (v11.5.0)
|
|
2
2
|
|
|
3
3
|
## 1. IDENTITY & VERSIONING
|
|
4
4
|
|
|
5
5
|
[NAME] = MindForge
|
|
6
|
-
[VERSION] = 11.
|
|
6
|
+
[VERSION] = 11.5.0
|
|
7
7
|
[STABLE] = true
|
|
8
8
|
[MODE] = "Platform Sovereign"
|
|
9
|
-
[REQUIRED_CORE_VERSION] = 11.
|
|
9
|
+
[REQUIRED_CORE_VERSION] = 11.5.0
|
|
10
10
|
[SOVEREIGN_IDENTITY] = true
|
|
11
11
|
[SRE_LAYER_ENABLED] = true
|
|
12
12
|
|
package/RELEASENOTES.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## v11.5.0 — Governance hardening + autonomous-engine repair
|
|
4
|
+
|
|
5
|
+
**Release Date**: 2026-06-11
|
|
6
|
+
**Type**: Minor (one behavior change — see "Heads-up" below)
|
|
7
|
+
**Upgrade Path**: `npx mindforge-cc@latest`
|
|
8
|
+
|
|
9
|
+
This release consolidates four waves of work into a single minor: new orchestration and
|
|
10
|
+
learning primitives, a broad governance/security hardening pass, and — most importantly —
|
|
11
|
+
a repair that takes the autonomous engine from "crashes on every wave" to actually
|
|
12
|
+
functional. Some new pieces ship deliberately **inert** (present but not wired in); those
|
|
13
|
+
are called out so you know not to expect new behavior from them yet.
|
|
14
|
+
|
|
15
|
+
### Heads-up — Tier-3 approvals now fail closed (the one behavior change)
|
|
16
|
+
|
|
17
|
+
`bin/governance/approve.js` no longer rubber-stamps approvals. Tier-3 approvals now
|
|
18
|
+
**require GPG verification** and will **fail before writing any record** if no GPG key is
|
|
19
|
+
configured. If you relied on the old, unverified path, you have two choices:
|
|
20
|
+
|
|
21
|
+
- Configure a GPG key (recommended), or
|
|
22
|
+
- Set `MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1` to keep the old behavior — in which case the
|
|
23
|
+
approval record is written with `verified: false` so the gap is auditable.
|
|
24
|
+
|
|
25
|
+
This is the only item in v11.5.0 that can change an existing workflow's outcome.
|
|
26
|
+
|
|
27
|
+
### The autonomous engine is functional again
|
|
28
|
+
|
|
29
|
+
`/mindforge:auto` was effectively broken. Three fixes in `bin/autonomous/auto-runner.js`
|
|
30
|
+
bring it back:
|
|
31
|
+
|
|
32
|
+
- **No more per-wave crash.** The runner was calling a non-existent `getIdentity()` on the
|
|
33
|
+
ZTAI singleton and dying on every wave. It now establishes identity through the real
|
|
34
|
+
`registerAgent` API (`_getRunnerIdentity()`).
|
|
35
|
+
- **The policy gate actually enforces now.** The async policy verdict was never `await`ed,
|
|
36
|
+
so the gate silently allowed everything. It now awaits `policyEngine.evaluate(intent)` on
|
|
37
|
+
every wave.
|
|
38
|
+
- **It fails closed.** If runner identity can't be established, the gate now **denies and
|
|
39
|
+
audits** (`auto_mode_denied`) rather than running ungoverned.
|
|
40
|
+
|
|
41
|
+
### Security & governance hardening
|
|
42
|
+
|
|
43
|
+
- **SSRF guard hardened (twice).** The import-URL guard now closes an IPv6 link-local
|
|
44
|
+
bypass and a symlink path-traversal bypass via numeric bitmask + canonical-path checks.
|
|
45
|
+
Separately, remote instinct imports now enforce a port allowlist (`443`/none only), so an
|
|
46
|
+
attacker can no longer pivot through an allowed public host to reach internal services
|
|
47
|
+
like Redis (`:6379`) or Mongo (`:27017`).
|
|
48
|
+
- **Persona supply-chain scan.** Persona asset validation now flags dangerous invisible
|
|
49
|
+
unicode (zero-width, bidi overrides, Unicode tags) across `.mindforge/personas`, closing
|
|
50
|
+
an ASCII-smuggling injection vector.
|
|
51
|
+
- **Destructive-command detector closed an evasion.** Quoted-hash tricks like
|
|
52
|
+
`rm "#" -rf /` are now caught.
|
|
53
|
+
- **Federated memory sync works again.** `eis-client`'s `getAuthHeader` was throwing on
|
|
54
|
+
every call; it now correctly registers a node identity and signs requests, so sync to
|
|
55
|
+
non-localhost EIS endpoints functions.
|
|
56
|
+
- **RBAC fails safe.** Tier elevation no longer throws for unregistered agents and resolves
|
|
57
|
+
tiers through the correct ZTAI API.
|
|
58
|
+
- **Fail-closed contracts are now tested.** New `trust-verifier` and `rbac-manager` test
|
|
59
|
+
suites lock in identity-verification and tier-authorization behavior so future governance
|
|
60
|
+
changes can't quietly regress them.
|
|
61
|
+
|
|
62
|
+
### Learning & cost routing
|
|
63
|
+
|
|
64
|
+
- **New instinct CLI** — `/mindforge:instinct` manages the JSONL instinct store
|
|
65
|
+
deterministically (no LLM spawn): `list`, `export`, `import`, `promote-candidates`, and
|
|
66
|
+
`prune`.
|
|
67
|
+
- **Instinct store schema** gains `project_id` (stable scoping) and a `source` field
|
|
68
|
+
(`auto-capture`/`manual`/`imported`/`observer`), with origin-weighted confidence —
|
|
69
|
+
auto-captured instincts start at `0.3`, manually added ones at `0.7`.
|
|
70
|
+
- **Cost-routing shadow mode is now real.** Arbitrage steering respects
|
|
71
|
+
`cost_routing.shadow_mode` (default on); in observe-only mode, selections are returned as
|
|
72
|
+
`authoritative: false` and logged as SHADOW, so you can watch the router's
|
|
73
|
+
recommendations without it taking the wheel.
|
|
74
|
+
|
|
75
|
+
### Shipped inert (no behavior change yet)
|
|
76
|
+
|
|
77
|
+
These landed as scaffolding and are **not** wired into any live path — they do nothing
|
|
78
|
+
until a follow-up enables them:
|
|
79
|
+
|
|
80
|
+
- **Manifest-driven install resolver** (`install-manifests.js`) — profile-to-module
|
|
81
|
+
expansion and dependency detection are implemented, but the adapter into the installer's
|
|
82
|
+
live `install()` path is deferred.
|
|
83
|
+
- **GAN-style harness personas** (`gan-evaluator`, `gan-generator`, `gan-planner`) — fully
|
|
84
|
+
scoped and documented, not yet attached to any command or workflow.
|
|
85
|
+
- **Typed Inter-Agent Message Protocol** (`handoff-schema.js`) — an internal orchestration
|
|
86
|
+
primitive (five message kinds, four priority levels, validation) for upcoming
|
|
87
|
+
agent-handoff work.
|
|
88
|
+
|
|
3
89
|
## v11.3.1 — Packaging hotfix
|
|
4
90
|
|
|
5
91
|
**Release Date**: 2026-06-05
|
package/SECURITY.md
CHANGED
|
@@ -108,6 +108,22 @@ Before submitting code that touches security-sensitive paths:
|
|
|
108
108
|
|
|
109
109
|
---
|
|
110
110
|
|
|
111
|
+
## Agentic-Harness Threat Model
|
|
112
|
+
|
|
113
|
+
This document covers application/code vulnerabilities. The **outward** harness threat
|
|
114
|
+
model — prompt injection, poisoned project config / hooks / MCP, supply-chain risk in
|
|
115
|
+
skills/agents, the lethal trifecta, sandboxing, and the autonomous-agent minimum-bar
|
|
116
|
+
checklist — lives in **[MINDFORGE-AGENTIC-SECURITY.md](./MINDFORGE-AGENTIC-SECURITY.md)**.
|
|
117
|
+
Both are required reading before running MindForge autonomously.
|
|
118
|
+
|
|
119
|
+
Minimum bar (see that doc for detail): separate agent identities · short-lived scoped
|
|
120
|
+
creds · sandbox untrusted work · deny egress by default · `permissions.deny` on
|
|
121
|
+
secret-bearing paths · sanitize foreign content · human approval for shell/egress/deploy
|
|
122
|
+
(TrustGate + Tier-3) · log tool calls (AUDIT.jsonl) · process-group kill + heartbeat ·
|
|
123
|
+
narrow disposable memory · scan skills/hooks/MCP/agents as supply-chain artifacts.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
111
127
|
## Known Mitigations & Limitations
|
|
112
128
|
|
|
113
129
|
- **ZK-proofs are simulated** — The Dilithium-5 / ZK-proof layer uses cryptographic simulation, not hardware-backed TEEs. It provides logical governance enforcement, not hardware-grade isolation.
|
|
@@ -687,12 +687,53 @@ class AutoRunner {
|
|
|
687
687
|
fs.writeFileSync(this.statePath, JSON.stringify(state, null, 2));
|
|
688
688
|
}
|
|
689
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Lazily registers (once) the autonomous runner's own ZTAI identity and
|
|
692
|
+
* returns { did, tier }. ztai-manager is a SINGLETON (not a constructor) and
|
|
693
|
+
* exposes no getIdentity() — the runner must register a DID to obtain one.
|
|
694
|
+
* Cached on the instance so every wave evaluates under one stable identity.
|
|
695
|
+
* Tier 3: autonomous phase processing is a high-trust operation; the policy
|
|
696
|
+
* engine still runs its own blast-radius analysis on top, so this is an INPUT
|
|
697
|
+
* to evaluation, not a self-granted bypass.
|
|
698
|
+
*/
|
|
699
|
+
async _getRunnerIdentity() {
|
|
700
|
+
if (!this._runnerIdentity) {
|
|
701
|
+
_ZTAIManager = lazyRequire(_ZTAIManager, '../governance/ztai-manager');
|
|
702
|
+
const did = await _ZTAIManager.registerAgent(
|
|
703
|
+
`auto-runner:${process.env.MF_PROJECT_ID || 'MF-ALPHA'}:phase-${this.phase}`,
|
|
704
|
+
3,
|
|
705
|
+
this._sessionId
|
|
706
|
+
);
|
|
707
|
+
const agent = _ZTAIManager.getAgent(did);
|
|
708
|
+
this._runnerIdentity = { did, tier: agent && typeof agent.tier === 'number' ? agent.tier : 3 };
|
|
709
|
+
}
|
|
710
|
+
return this._runnerIdentity;
|
|
711
|
+
}
|
|
712
|
+
|
|
690
713
|
async evaluateWavePolicy() {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
714
|
+
let identity;
|
|
715
|
+
try {
|
|
716
|
+
identity = await this._getRunnerIdentity();
|
|
717
|
+
} catch (err) {
|
|
718
|
+
// Fail CLOSED: if the runner cannot establish a verifiable identity, deny
|
|
719
|
+
// the wave rather than proceeding ungoverned.
|
|
720
|
+
console.warn(`[APO-DENY] Could not establish runner identity: ${err.message}`);
|
|
721
|
+
this.writeAudit({ event: 'auto_mode_denied', reason: `identity unavailable: ${err.message}`, phase: this.phase });
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const intent = {
|
|
726
|
+
did: identity.did,
|
|
727
|
+
action: 'process_phase_wave',
|
|
728
|
+
resource: `projects/${process.env.MF_PROJECT_ID || 'MF-ALPHA'}/phases/${this.phase}/*`,
|
|
729
|
+
tier: identity.tier,
|
|
730
|
+
sessionId: this._sessionId,
|
|
731
|
+
metadata: { engine: 'Nimbus-S4', mode: 'autonomous', wave_timestamp: new Date().toISOString() }
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// policyEngine.evaluate is ASYNC — must be awaited, or `result` is a Promise
|
|
735
|
+
// and `result.verdict === 'DENY'` is always false (the gate never fires).
|
|
736
|
+
const result = await this.policyEngine.evaluate(intent);
|
|
696
737
|
if (result.verdict === 'DENY') { console.warn(`[APO-DENY] ${result.reason}`); return false; }
|
|
697
738
|
return true;
|
|
698
739
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MindForge — Typed inter-agent message protocol.
|
|
5
|
+
*
|
|
6
|
+
* Ports ECC's ecc2/src/comms/mod.rs MessageType + TaskPriority as a JSON-schema
|
|
7
|
+
* validator. Complements state-manager.js validateHandoff (which validates the
|
|
8
|
+
* HANDOFF.json envelope) by typing the individual messages agents exchange:
|
|
9
|
+
*
|
|
10
|
+
* kinds: task_handoff | query | response | completed | conflict
|
|
11
|
+
* priority: low | normal | high | critical (default normal; legacy fallback)
|
|
12
|
+
*
|
|
13
|
+
* The Conflict kind pairs with the worktree engine's merge-readiness output
|
|
14
|
+
* (bin/worktree/engine.js). This is discipline/typing, not new runtime behavior.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MESSAGE_KINDS = ['task_handoff', 'query', 'response', 'completed', 'conflict'];
|
|
18
|
+
const PRIORITIES = ['low', 'normal', 'high', 'critical'];
|
|
19
|
+
const DEFAULT_PRIORITY = 'normal';
|
|
20
|
+
|
|
21
|
+
// Required fields per kind (mirrors ECC's MessageType variants).
|
|
22
|
+
const REQUIRED_FIELDS = {
|
|
23
|
+
task_handoff: ['task', 'context'],
|
|
24
|
+
query: ['question'],
|
|
25
|
+
response: ['answer'],
|
|
26
|
+
completed: ['summary'], // files_changed optional, defaults []
|
|
27
|
+
conflict: ['file', 'description'],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate a typed message object. Returns { valid, warnings }.
|
|
32
|
+
* Fail-open style (matches validateHandoff): collects warnings, never throws.
|
|
33
|
+
*/
|
|
34
|
+
function validateMessage(msg) {
|
|
35
|
+
const warnings = [];
|
|
36
|
+
if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {
|
|
37
|
+
return { valid: false, warnings: ['message is not an object'] };
|
|
38
|
+
}
|
|
39
|
+
if (!MESSAGE_KINDS.includes(msg.kind)) {
|
|
40
|
+
warnings.push(`invalid kind: "${msg.kind}". Expected one of: ${MESSAGE_KINDS.join(', ')}`);
|
|
41
|
+
return { valid: false, warnings };
|
|
42
|
+
}
|
|
43
|
+
for (const field of REQUIRED_FIELDS[msg.kind]) {
|
|
44
|
+
if (typeof msg[field] !== 'string' || msg[field].length === 0) {
|
|
45
|
+
warnings.push(`${msg.kind} missing required string field: ${field}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (msg.kind === 'completed' && msg.files_changed !== undefined && !Array.isArray(msg.files_changed)) {
|
|
49
|
+
warnings.push('completed.files_changed must be an array');
|
|
50
|
+
}
|
|
51
|
+
if (msg.kind === 'task_handoff' && msg.priority !== undefined && !PRIORITIES.includes(msg.priority)) {
|
|
52
|
+
warnings.push(`invalid priority: "${msg.priority}". Expected one of: ${PRIORITIES.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
return { valid: warnings.length === 0, warnings };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a handoff message's priority, defaulting to "normal" and tolerating
|
|
59
|
+
* legacy entries that lack a typed priority (ECC's legacy fallback).
|
|
60
|
+
*/
|
|
61
|
+
function handoffPriority(msg) {
|
|
62
|
+
if (!msg || typeof msg !== 'object') return DEFAULT_PRIORITY;
|
|
63
|
+
const p = msg.priority;
|
|
64
|
+
return PRIORITIES.includes(p) ? p : DEFAULT_PRIORITY;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One-line human preview of a typed message (for status/log surfaces).
|
|
69
|
+
*/
|
|
70
|
+
function preview(msg) {
|
|
71
|
+
const trunc = (s, n) => {
|
|
72
|
+
const t = String(s || '').trim();
|
|
73
|
+
return t.length <= n ? t : `${t.slice(0, n - 1)}…`;
|
|
74
|
+
};
|
|
75
|
+
switch (msg && msg.kind) {
|
|
76
|
+
case 'task_handoff': {
|
|
77
|
+
const p = handoffPriority(msg);
|
|
78
|
+
return p === DEFAULT_PRIORITY
|
|
79
|
+
? `handoff ${trunc(msg.task, 56)}`
|
|
80
|
+
: `handoff [${p}] ${trunc(msg.task, 48)}`;
|
|
81
|
+
}
|
|
82
|
+
case 'query': return `query ${trunc(msg.question, 56)}`;
|
|
83
|
+
case 'response': return `response ${trunc(msg.answer, 56)}`;
|
|
84
|
+
case 'completed': {
|
|
85
|
+
const n = Array.isArray(msg.files_changed) ? msg.files_changed.length : 0;
|
|
86
|
+
return n === 0 ? `completed ${trunc(msg.summary, 48)}` : `completed ${trunc(msg.summary, 40)} | ${n} files`;
|
|
87
|
+
}
|
|
88
|
+
case 'conflict': return `conflict ${msg.file} | ${trunc(msg.description, 40)}`;
|
|
89
|
+
default: return `unknown ${trunc(JSON.stringify(msg), 56)}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build a well-formed message of a given kind (convenience + normalization).
|
|
95
|
+
* Throws if the result fails validation, so callers can't emit a bad message.
|
|
96
|
+
*/
|
|
97
|
+
function makeMessage(kind, fields = {}) {
|
|
98
|
+
const msg = Object.assign({ kind }, fields);
|
|
99
|
+
if (kind === 'task_handoff' && msg.priority === undefined) msg.priority = DEFAULT_PRIORITY;
|
|
100
|
+
if (kind === 'completed' && msg.files_changed === undefined) msg.files_changed = [];
|
|
101
|
+
const { valid, warnings } = validateMessage(msg);
|
|
102
|
+
if (!valid) throw new Error(`invalid ${kind} message: ${warnings.join('; ')}`);
|
|
103
|
+
return msg;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
MESSAGE_KINDS,
|
|
108
|
+
PRIORITIES,
|
|
109
|
+
DEFAULT_PRIORITY,
|
|
110
|
+
validateMessage,
|
|
111
|
+
handoffPriority,
|
|
112
|
+
preview,
|
|
113
|
+
makeMessage,
|
|
114
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# session-guardian.sh — Autonomous-loop session guard (MindForge)
|
|
3
|
+
# Exit 0 = proceed. Exit 1 = skip this loop cycle.
|
|
4
|
+
# Called by any bin/autonomous loop (and the deferred background observer)
|
|
5
|
+
# BEFORE spawning a model session, so the loop cannot burn tokens overnight,
|
|
6
|
+
# AFK, or faster than its cooldown.
|
|
7
|
+
#
|
|
8
|
+
# Ported near-verbatim from ECC (continuous-learning-v2/agents/session-guardian.sh).
|
|
9
|
+
# Env defaults map to a new instincts.observer config block (default-off posture):
|
|
10
|
+
# OBSERVER_INTERVAL_SECONDS default: 300 (per-project cooldown)
|
|
11
|
+
# OBSERVER_LAST_RUN_LOG default: ~/.mindforge/observer-last-run.log
|
|
12
|
+
# OBSERVER_ACTIVE_HOURS_START default: 800 (8:00 AM local, 0 to disable)
|
|
13
|
+
# OBSERVER_ACTIVE_HOURS_END default: 2300 (11:00 PM local, 0 to disable)
|
|
14
|
+
# OBSERVER_MAX_IDLE_SECONDS default: 1800 (30 min; 0 to disable)
|
|
15
|
+
#
|
|
16
|
+
# Gate order (cheapest first):
|
|
17
|
+
# Gate 1: Time window check (~0ms)
|
|
18
|
+
# Gate 2: Project cooldown log (~1ms, mkdir lock)
|
|
19
|
+
# Gate 3: Idle detection (~5-50ms, OS syscall; fail open)
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
INTERVAL="${OBSERVER_INTERVAL_SECONDS:-300}"
|
|
24
|
+
LOG_PATH="${OBSERVER_LAST_RUN_LOG:-$HOME/.mindforge/observer-last-run.log}"
|
|
25
|
+
ACTIVE_START="${OBSERVER_ACTIVE_HOURS_START:-800}"
|
|
26
|
+
ACTIVE_END="${OBSERVER_ACTIVE_HOURS_END:-2300}"
|
|
27
|
+
MAX_IDLE="${OBSERVER_MAX_IDLE_SECONDS:-1800}"
|
|
28
|
+
|
|
29
|
+
# ── Gate 1: Time Window ───────────────────────────────────────────────────────
|
|
30
|
+
if [ "$ACTIVE_START" -ne 0 ] || [ "$ACTIVE_END" -ne 0 ]; then
|
|
31
|
+
current_hhmm=$(date +%k%M | tr -d ' ')
|
|
32
|
+
current_hhmm_num=$(( 10#${current_hhmm:-0} ))
|
|
33
|
+
active_start_num=$(( 10#${ACTIVE_START:-800} ))
|
|
34
|
+
active_end_num=$(( 10#${ACTIVE_END:-2300} ))
|
|
35
|
+
|
|
36
|
+
within_active_hours=0
|
|
37
|
+
if [ "$active_start_num" -lt "$active_end_num" ]; then
|
|
38
|
+
if [ "$current_hhmm_num" -ge "$active_start_num" ] && [ "$current_hhmm_num" -lt "$active_end_num" ]; then
|
|
39
|
+
within_active_hours=1
|
|
40
|
+
fi
|
|
41
|
+
else
|
|
42
|
+
if [ "$current_hhmm_num" -ge "$active_start_num" ] || [ "$current_hhmm_num" -lt "$active_end_num" ]; then
|
|
43
|
+
within_active_hours=1
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [ "$within_active_hours" -ne 1 ]; then
|
|
48
|
+
echo "session-guardian: outside active hours (${current_hhmm}, window ${ACTIVE_START}-${ACTIVE_END})" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# ── Gate 2: Project Cooldown Log ─────────────────────────────────────────────
|
|
54
|
+
project_root="${PROJECT_DIR:-}"
|
|
55
|
+
if [ -z "$project_root" ] || [ ! -d "$project_root" ]; then
|
|
56
|
+
project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
|
|
57
|
+
fi
|
|
58
|
+
project_name="$(basename "$project_root")"
|
|
59
|
+
now="$(date +%s)"
|
|
60
|
+
|
|
61
|
+
mkdir -p "$(dirname "$LOG_PATH")" || {
|
|
62
|
+
echo "session-guardian: cannot create log dir, proceeding" >&2
|
|
63
|
+
exit 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_lock_dir="${LOG_PATH}.lock"
|
|
67
|
+
if ! mkdir "$_lock_dir" 2>/dev/null; then
|
|
68
|
+
echo "session-guardian: log locked by concurrent process, skipping cycle" >&2
|
|
69
|
+
exit 1
|
|
70
|
+
else
|
|
71
|
+
trap 'rm -rf "$_lock_dir"' EXIT INT TERM
|
|
72
|
+
|
|
73
|
+
last_spawn=0
|
|
74
|
+
last_spawn=$(awk -F '\t' -v key="$project_root" '$1 == key { value = $2 } END { if (value != "") print value }' "$LOG_PATH" 2>/dev/null) || true
|
|
75
|
+
last_spawn="${last_spawn:-0}"
|
|
76
|
+
[[ "$last_spawn" =~ ^[0-9]+$ ]] || last_spawn=0
|
|
77
|
+
|
|
78
|
+
elapsed=$(( now - last_spawn ))
|
|
79
|
+
if [ "$elapsed" -lt "$INTERVAL" ]; then
|
|
80
|
+
rm -rf "$_lock_dir"
|
|
81
|
+
trap - EXIT INT TERM
|
|
82
|
+
echo "session-guardian: cooldown active for '${project_name}' (last spawn ${elapsed}s ago, interval ${INTERVAL}s)" >&2
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
tmp_log="$(mktemp "$(dirname "$LOG_PATH")/observer-last-run.XXXXXX")"
|
|
87
|
+
awk -F '\t' -v key="$project_root" '$1 != key' "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
|
|
88
|
+
printf '%s\t%s\n' "$project_root" "$now" >> "$tmp_log"
|
|
89
|
+
mv "$tmp_log" "$LOG_PATH"
|
|
90
|
+
|
|
91
|
+
rm -rf "$_lock_dir"
|
|
92
|
+
trap - EXIT INT TERM
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# ── Gate 3: Idle Detection ────────────────────────────────────────────────────
|
|
96
|
+
get_idle_seconds() {
|
|
97
|
+
local _raw
|
|
98
|
+
case "$(uname -s)" in
|
|
99
|
+
Darwin)
|
|
100
|
+
_raw=$( { /usr/sbin/ioreg -c IOHIDSystem \
|
|
101
|
+
| /usr/bin/awk '/HIDIdleTime/ {print int($NF/1000000000); exit}'; } \
|
|
102
|
+
2>/dev/null ) || true
|
|
103
|
+
printf '%s\n' "${_raw:-0}" | head -n1
|
|
104
|
+
;;
|
|
105
|
+
Linux)
|
|
106
|
+
if command -v xprintidle >/dev/null 2>&1; then
|
|
107
|
+
_raw=$(xprintidle 2>/dev/null) || true
|
|
108
|
+
echo $(( ${_raw:-0} / 1000 ))
|
|
109
|
+
else
|
|
110
|
+
echo 0 # fail open: xprintidle not installed
|
|
111
|
+
fi
|
|
112
|
+
;;
|
|
113
|
+
*MINGW*|*MSYS*|*CYGWIN*)
|
|
114
|
+
_raw=$(powershell.exe -NoProfile -NonInteractive -Command \
|
|
115
|
+
"try { \
|
|
116
|
+
Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO p); [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public int dwTime; }' -Name WinAPI -Namespace PInvoke; \
|
|
117
|
+
\$l = New-Object PInvoke.WinAPI+LASTINPUTINFO; \$l.cbSize = 8; \
|
|
118
|
+
[PInvoke.WinAPI]::GetLastInputInfo([ref]\$l) | Out-Null; \
|
|
119
|
+
[int][Math]::Max(0, [long]([Environment]::TickCount - [long]\$l.dwTime) / 1000) \
|
|
120
|
+
} catch { 0 }" \
|
|
121
|
+
2>/dev/null | tr -d '\r') || true
|
|
122
|
+
printf '%s\n' "${_raw:-0}" | head -n1
|
|
123
|
+
;;
|
|
124
|
+
*)
|
|
125
|
+
echo 0 # fail open: unknown platform
|
|
126
|
+
;;
|
|
127
|
+
esac
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if [ "$MAX_IDLE" -gt 0 ]; then
|
|
131
|
+
idle_seconds=$(get_idle_seconds)
|
|
132
|
+
if [ "$idle_seconds" -gt "$MAX_IDLE" ]; then
|
|
133
|
+
echo "session-guardian: user idle ${idle_seconds}s (threshold ${MAX_IDLE}s), skipping" >&2
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
exit 0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MindForge — Autonomous session supervisor (PID-liveness crash recovery).
|
|
5
|
+
*
|
|
6
|
+
* Adapted from ECC's Rust session daemon (ecc2/src/session/daemon.rs). Ports
|
|
7
|
+
* ONLY the two pure, high-value functions — NOT the tokio daemon or ECC's
|
|
8
|
+
* dispatch/merge/rebalance machinery (which overlaps MindForge's existing
|
|
9
|
+
* task-dispatcher / wave-executor / mesh-self-healer):
|
|
10
|
+
*
|
|
11
|
+
* 1. pidIsAlive(pid) — process.kill(pid, 0): ESRCH=dead, EPERM=alive.
|
|
12
|
+
* 2. resumeCrashedSessions — sweep sessions left "running" whose pid is dead
|
|
13
|
+
* and mark them "failed" (stale-pid recovery).
|
|
14
|
+
*
|
|
15
|
+
* Plus a heartbeat() that stamps auto-state.json so a supervisor can detect a
|
|
16
|
+
* wedged/abandoned session. Layered OVER state-manager.js — it does not replace
|
|
17
|
+
* it. Default-inert: nothing runs unless explicitly invoked.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const SUPERVISOR_STATUSES = ['idle', 'running', 'paused', 'completed', 'escalated', 'timeout', 'failed'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Probe whether a process is alive. Cross-platform via Node's process.kill with
|
|
24
|
+
* signal 0 (no signal delivered — existence check only):
|
|
25
|
+
* - kill succeeds -> alive
|
|
26
|
+
* - throws EPERM -> alive (exists, owned by another user)
|
|
27
|
+
* - throws ESRCH (or other) -> dead
|
|
28
|
+
* A null/0/invalid pid is treated as dead.
|
|
29
|
+
*/
|
|
30
|
+
function pidIsAlive(pid) {
|
|
31
|
+
const n = Number(pid);
|
|
32
|
+
if (!Number.isInteger(n) || n <= 0) return false;
|
|
33
|
+
try {
|
|
34
|
+
process.kill(n, 0);
|
|
35
|
+
return true;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return err && err.code === 'EPERM';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sweep a sessions list and mark any that are "running" with a dead pid as
|
|
43
|
+
* "failed". Pure over its inputs: takes the sessions array + an isAlive probe
|
|
44
|
+
* (injectable for tests, matching ECC's resume_crashed_sessions_with) and
|
|
45
|
+
* returns { failed: [...ids], sessions: [...updated] } without side effects.
|
|
46
|
+
*
|
|
47
|
+
* @param {Array<{id:string,status:string,pid?:number}>} sessions
|
|
48
|
+
* @param {(pid:number)=>boolean} [isAlive] defaults to pidIsAlive
|
|
49
|
+
*/
|
|
50
|
+
function resumeCrashedSessions(sessions, isAlive = pidIsAlive) {
|
|
51
|
+
const failed = [];
|
|
52
|
+
const updated = (Array.isArray(sessions) ? sessions : []).map(session => {
|
|
53
|
+
if (session.status !== 'running') return session;
|
|
54
|
+
if (session.pid != null && isAlive(session.pid)) return session;
|
|
55
|
+
failed.push(session.id);
|
|
56
|
+
// Immutable: new object with failed status, pid cleared.
|
|
57
|
+
return Object.assign({}, session, { status: 'failed', pid: null });
|
|
58
|
+
});
|
|
59
|
+
return { failed, sessions: updated };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Stamp a heartbeat onto auto-state.json via a state manager. A supervisor in a
|
|
64
|
+
* separate process can compare heartbeatAt against now to detect a stalled loop
|
|
65
|
+
* (pairs with bin/autonomous/session-guardian.sh + loop-operator escalation).
|
|
66
|
+
*
|
|
67
|
+
* @param {{updateState:Function}} stateManager from createStateManager(planningDir)
|
|
68
|
+
* @param {number} [pid] the live worker pid to record (defaults to current pid)
|
|
69
|
+
*/
|
|
70
|
+
function heartbeat(stateManager, pid = process.pid) {
|
|
71
|
+
return stateManager.updateState({
|
|
72
|
+
pid,
|
|
73
|
+
heartbeatAt: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Recover a single state file: if it shows status "running" with a dead pid,
|
|
79
|
+
* transition it to "failed". Returns true if a recovery was applied.
|
|
80
|
+
*
|
|
81
|
+
* @param {{getState:Function,updateState:Function}} stateManager
|
|
82
|
+
* @param {(pid:number)=>boolean} [isAlive]
|
|
83
|
+
*/
|
|
84
|
+
function recoverState(stateManager, isAlive = pidIsAlive) {
|
|
85
|
+
const state = stateManager.getState();
|
|
86
|
+
if (state.status !== 'running') return false;
|
|
87
|
+
if (state.pid != null && isAlive(state.pid)) return false;
|
|
88
|
+
stateManager.updateState({ status: 'failed', pid: null, failedAt: new Date().toISOString() });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
SUPERVISOR_STATUSES,
|
|
94
|
+
pidIsAlive,
|
|
95
|
+
resumeCrashedSessions,
|
|
96
|
+
heartbeat,
|
|
97
|
+
recoverState,
|
|
98
|
+
};
|
package/bin/change-classifier.js
CHANGED
|
@@ -31,10 +31,21 @@ const SENSITIVE_PATTERNS = [
|
|
|
31
31
|
|
|
32
32
|
function classify() {
|
|
33
33
|
try {
|
|
34
|
-
// Get list of changed files compared to origin
|
|
34
|
+
// Get list of changed files compared to origin/<base> or HEAD~1.
|
|
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.)
|
|
35
38
|
const base = process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : 'HEAD~1';
|
|
36
|
-
const
|
|
37
|
-
|
|
39
|
+
const range = process.env.GITHUB_BASE_REF ? `${base}...HEAD` : `${base}..HEAD`;
|
|
40
|
+
const diffFiles = execFileSync('git', ['diff', '--name-only', range], { encoding: 'utf8' }).split('\n').filter(Boolean);
|
|
41
|
+
|
|
42
|
+
// Test and documentation files are excluded from the sensitive-PATTERN scan below: a test
|
|
43
|
+
// asserting on "password"/key patterns, or a doc mentioning secrets, is not a sensitive
|
|
44
|
+
// change and must not trip Tier 3. (Path-based detection still covers genuinely sensitive
|
|
45
|
+
// source paths.) This is the fix for test-only PRs being misclassified as Tier 3.
|
|
46
|
+
const isTestOrDoc = (f) =>
|
|
47
|
+
/(^|\/)(tests?|__tests__|docs)\//.test(f) || /\.(test|spec)\.[cm]?[jt]s$/.test(f) || f.endsWith('.md');
|
|
48
|
+
|
|
38
49
|
let tier = 1;
|
|
39
50
|
let reasons = [];
|
|
40
51
|
|
|
@@ -45,9 +56,12 @@ function classify() {
|
|
|
45
56
|
reasons.push(`Sensitive path modified: ${matchedPath}`);
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
// 2. Pattern-based detection in diff (Tier 3)
|
|
59
|
+
// 2. Pattern-based detection in diff (Tier 3) — non-test/doc files only
|
|
49
60
|
if (tier < 3) {
|
|
50
|
-
const
|
|
61
|
+
const scanFiles = diffFiles.filter(f => !isTestOrDoc(f));
|
|
62
|
+
const diffContent = scanFiles.length
|
|
63
|
+
? execFileSync('git', ['diff', range, '--', ...scanFiles], { encoding: 'utf8' })
|
|
64
|
+
: '';
|
|
51
65
|
for (const pattern of SENSITIVE_PATTERNS) {
|
|
52
66
|
if (pattern.test(diffContent)) {
|
|
53
67
|
tier = 3;
|