mindforge-cc 11.4.0 → 11.5.1
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 +120 -0
- package/MINDFORGE.md +3 -3
- package/README.md +0 -1
- package/RELEASENOTES.md +131 -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/dashboard/api-router.js +10 -1
- package/bin/governance/approve.js +65 -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/memory/federated-sync.js +11 -2
- package/bin/memory/knowledge-capture.js +10 -1
- package/bin/memory/pillar-health-tracker.js +9 -1
- 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/review/ads-engine.js +2 -2
- package/bin/revops/market-evaluator.js +23 -2
- package/bin/revops/router-steering-v2.js +17 -2
- package/bin/security/trust-boundaries.js +20 -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
|
@@ -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;
|
|
@@ -75,12 +75,21 @@ function register(app) {
|
|
|
75
75
|
app.post('/api/approve/:id', (req, res) => {
|
|
76
76
|
try {
|
|
77
77
|
const { id } = req.params;
|
|
78
|
-
const { decision, comment
|
|
78
|
+
const { decision, comment } = req.body || {};
|
|
79
79
|
|
|
80
80
|
if (!decision) {
|
|
81
81
|
return res.status(400).json({ error: 'Missing "decision" field (approve|reject)' });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// SECURITY (v11.5.1): do NOT trust a client-supplied `approver` for the
|
|
85
|
+
// recorded identity — it is forgeable and would let any caller write a
|
|
86
|
+
// false approval audit trail (e.g. resolved_by: 'admin'). requireAuth
|
|
87
|
+
// (server.js) proves the caller holds the owner-only dashboard token but
|
|
88
|
+
// exposes no named principal, so we attribute the action to a FIXED
|
|
89
|
+
// trusted actor. (A future RBAC pass can map a Bearer token -> DID and
|
|
90
|
+
// record the real principal; until then, never echo req.body.approver.)
|
|
91
|
+
const approver = 'dashboard-authenticated';
|
|
92
|
+
|
|
84
93
|
const result = Approval.processDecision(id, decision, comment, approver);
|
|
85
94
|
|
|
86
95
|
if (!result.success) {
|
|
@@ -12,14 +12,9 @@ const os = require('os');
|
|
|
12
12
|
const crypto = require('crypto');
|
|
13
13
|
const { execFileSync } = require('child_process');
|
|
14
14
|
|
|
15
|
-
const REASON = process.argv[2] || 'Manual approval for sensitive changes.';
|
|
16
15
|
const ROOT = path.resolve(__dirname, '../../');
|
|
17
16
|
const APPROVALS_DIR = path.join(ROOT, '.planning/approvals');
|
|
18
17
|
|
|
19
|
-
if (!fs.existsSync(APPROVALS_DIR)) {
|
|
20
|
-
fs.mkdirSync(APPROVALS_DIR, { recursive: true });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
18
|
/**
|
|
24
19
|
* Attempts to retrieve the GPG signing key configured in git.
|
|
25
20
|
* Returns null if no key is configured or git is unavailable.
|
|
@@ -34,35 +29,69 @@ function getGPGSigningKey() {
|
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
/**
|
|
37
|
-
* Verifies the identity of the approver using GPG
|
|
38
|
-
*
|
|
32
|
+
* Verifies the identity of the approver using GPG.
|
|
33
|
+
*
|
|
34
|
+
* FAIL-CLOSED (Wave 6): a Tier 3 approval is a security gate. If no GPG signing
|
|
35
|
+
* key is configured, identity cannot be cryptographically attributed — git
|
|
36
|
+
* identity comes from spoofable env (USER / git config), so it is NOT a
|
|
37
|
+
* verification. We therefore REFUSE to mint an approval unless the operator
|
|
38
|
+
* explicitly opts into the weaker git-identity mode via
|
|
39
|
+
* MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1 (audited as unverified). Previously this
|
|
40
|
+
* returned {verified:false} but the record was written anyway and no consumer
|
|
41
|
+
* checked the flag — a cosmetic gate.
|
|
42
|
+
*
|
|
39
43
|
* @param {string} approver - The approver identity string
|
|
44
|
+
* @returns {{verified:boolean, method:string, identity:string, keyId?:string}}
|
|
45
|
+
* @throws if no GPG key AND the unverified-approval opt-in is not set.
|
|
40
46
|
*/
|
|
41
47
|
function verifyApproverIdentity(approver) {
|
|
42
48
|
const gpgKey = getGPGSigningKey();
|
|
43
49
|
|
|
44
50
|
if (!gpgKey) {
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
const allowUnverified = process.env.MINDFORGE_ALLOW_UNVERIFIED_APPROVAL === '1';
|
|
52
|
+
if (!allowUnverified) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'No GPG signing key configured (git config user.signingkey is empty). ' +
|
|
55
|
+
'A Tier 3 approval requires a verifiable identity. Either configure GPG signing, ' +
|
|
56
|
+
'or explicitly accept an UNVERIFIED approval by setting ' +
|
|
57
|
+
'MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1 (the record will be marked verified:false).'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
console.warn('[GOVERNANCE] No GPG key — minting an UNVERIFIED approval (MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1). ' +
|
|
61
|
+
'git identity is spoofable; this approval is NOT cryptographically attributed.');
|
|
62
|
+
// unverified_ack=true is the EXPLICIT, audited override the CI Tier-3 gate
|
|
63
|
+
// looks for. A bare verified:false record WITHOUT this marker (e.g. a stale
|
|
64
|
+
// pre-fail-closed file or a hand-forged empty one) is still rejected by the
|
|
65
|
+
// gate — only a deliberately opted-in unverified approval is accepted.
|
|
66
|
+
return { verified: false, method: 'git_identity_unverified', identity: approver, unverified_ack: true };
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
return { verified: true, method: 'gpg_key', identity: approver, keyId: gpgKey };
|
|
50
70
|
}
|
|
51
71
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Mint a Tier 3 approval record. Throws (fail-closed) if identity cannot be
|
|
74
|
+
* verified and the unverified-approval opt-in is not set — the record is NOT
|
|
75
|
+
* written in that case.
|
|
76
|
+
* @param {{reason?:string, approvalsDir?:string, root?:string}} [opts]
|
|
77
|
+
* @returns {{filePath:string, record:object}}
|
|
78
|
+
*/
|
|
79
|
+
function approve(opts = {}) {
|
|
80
|
+
const reason = opts.reason || 'Manual approval for sensitive changes.';
|
|
81
|
+
const root = opts.root || ROOT;
|
|
82
|
+
const approvalsDir = opts.approvalsDir || APPROVALS_DIR;
|
|
83
|
+
|
|
84
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
55
85
|
|
|
56
86
|
const id = `MF-AUTH-${Date.now().toString(36).toUpperCase()}`;
|
|
57
87
|
const timestamp = new Date().toISOString();
|
|
58
88
|
const approver = process.env.USER || 'MindForge User';
|
|
59
89
|
|
|
60
|
-
// Verify approver identity
|
|
90
|
+
// Verify approver identity — THROWS fail-closed if unverifiable (before any write).
|
|
61
91
|
const identityVerification = verifyApproverIdentity(approver);
|
|
62
92
|
|
|
63
|
-
// Calculate a signature based on current state
|
|
64
93
|
const signature = crypto.createHash('sha256')
|
|
65
|
-
.update(`${id}:${
|
|
94
|
+
.update(`${id}:${reason}:${timestamp}:${os.hostname()}`)
|
|
66
95
|
.digest('hex');
|
|
67
96
|
|
|
68
97
|
const record = {
|
|
@@ -72,25 +101,33 @@ async function approve() {
|
|
|
72
101
|
tier: 3,
|
|
73
102
|
approved_by: approver,
|
|
74
103
|
timestamp,
|
|
75
|
-
reason
|
|
104
|
+
reason,
|
|
76
105
|
signature: `sha256:${signature}`,
|
|
77
106
|
identity_verification: identityVerification
|
|
78
107
|
};
|
|
79
108
|
|
|
109
|
+
if (!fs.existsSync(approvalsDir)) fs.mkdirSync(approvalsDir, { recursive: true });
|
|
80
110
|
const filename = `approval-${id.toLowerCase()}.json`;
|
|
81
|
-
const filePath = path.join(
|
|
82
|
-
|
|
111
|
+
const filePath = path.join(approvalsDir, filename);
|
|
83
112
|
fs.writeFileSync(filePath, JSON.stringify(record, null, 2));
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
console.log(`ID: ${id}`);
|
|
87
|
-
console.log(`Reason: ${REASON}`);
|
|
88
|
-
console.log(`Verified: ${identityVerification.verified ? 'GPG (' + identityVerification.keyId + ')' : 'git identity only (no GPG key)'}`);
|
|
89
|
-
console.log(`File: .planning/approvals/${filename}`);
|
|
90
|
-
console.log('\nCommit this file to unblock Tier 3 gates in CI.\n');
|
|
114
|
+
return { filePath, record, filename };
|
|
91
115
|
}
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
module.exports = { verifyApproverIdentity, getGPGSigningKey, approve };
|
|
118
|
+
|
|
119
|
+
if (require.main === module) {
|
|
120
|
+
try {
|
|
121
|
+
const { filename, record } = approve({ reason: process.argv[2] });
|
|
122
|
+
const iv = record.identity_verification;
|
|
123
|
+
console.log('\n✅ Governance approval generated!\n');
|
|
124
|
+
console.log(`ID: ${record.id}`);
|
|
125
|
+
console.log(`Reason: ${record.reason}`);
|
|
126
|
+
console.log(`Verified: ${iv.verified ? 'GPG (' + iv.keyId + ')' : 'git identity only — UNVERIFIED'}`);
|
|
127
|
+
console.log(`File: .planning/approvals/${filename}`);
|
|
128
|
+
console.log('\nCommit this file to unblock Tier 3 gates in CI.\n');
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`❌ Approval failed: ${err.message}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -21,7 +21,9 @@ class ConfigManager {
|
|
|
21
21
|
if (fs.existsSync(this.configPath)) {
|
|
22
22
|
const raw = fs.readFileSync(this.configPath, 'utf8');
|
|
23
23
|
this.config = JSON.parse(raw);
|
|
24
|
-
|
|
24
|
+
// Diagnostic goes to stderr (not stdout) so it never pollutes JSON that a
|
|
25
|
+
// consumer parses from this process's stdout. Matches the warn/error lines below.
|
|
26
|
+
console.error(`[ConfigManager] Loaded configuration from ${this.configPath}`);
|
|
25
27
|
} else {
|
|
26
28
|
console.warn(`[ConfigManager] Config file not found at ${this.configPath}. Using defaults.`);
|
|
27
29
|
this.config = { env: 'default' };
|
|
@@ -40,18 +40,26 @@ class RBACManager {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* [HARDEN] Dynamically binds roles based on ZTAI trust tiers
|
|
44
|
-
*
|
|
43
|
+
* [HARDEN] Dynamically binds roles based on ZTAI trust tiers on top of the
|
|
44
|
+
* agent's explicit/default roles. High-trust agents automatically gain
|
|
45
|
+
* architect-level visibility.
|
|
46
|
+
*
|
|
47
|
+
* ztai-manager is a SINGLETON instance (not a constructor), and exposes no
|
|
48
|
+
* getIdentity(); the agent's tier lives in the trust registry, read via
|
|
49
|
+
* getAgent(did). Fails SAFE: an unregistered/unknown DID has no resolvable
|
|
50
|
+
* tier, so it receives only its base roles (no tier-based elevation) rather
|
|
51
|
+
* than throwing. (Wave 6: the previous `new ztai().getIdentity()` threw on
|
|
52
|
+
* every call — "ztai is not a constructor".)
|
|
45
53
|
*/
|
|
46
54
|
async getRolesByTier(did) {
|
|
47
|
-
const manager = new ztai();
|
|
48
|
-
const identity = await manager.getIdentity();
|
|
49
55
|
const explicit = this.getRoles(did);
|
|
56
|
+
const agent = ztai.getAgent(did);
|
|
57
|
+
const tier = agent && typeof agent.tier === 'number' ? agent.tier : 0;
|
|
50
58
|
|
|
51
|
-
if (
|
|
59
|
+
if (tier >= 3) {
|
|
52
60
|
return [...new Set([...explicit, 'lead-architect'])];
|
|
53
61
|
}
|
|
54
|
-
if (
|
|
62
|
+
if (tier >= 2) {
|
|
55
63
|
return [...new Set([...explicit, 'developer'])];
|
|
56
64
|
}
|
|
57
65
|
return explicit;
|