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,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MindForge install-state provenance contract.
|
|
5
|
+
*
|
|
6
|
+
* Writes/reads/validates .mindforge/install-state.json — a provenance record
|
|
7
|
+
* (who installed, from which source commit, for which runtimes/scope, when, and
|
|
8
|
+
* the operation log). Sibling to .agent/file-manifest.json (content hashes).
|
|
9
|
+
*
|
|
10
|
+
* Adapted from ECC's install-state lib: keeps the robust ajv-with-fallback
|
|
11
|
+
* validation pattern (works in bare checkouts without ajv installed), but the
|
|
12
|
+
* data model is MindForge-native — runtimes/scope/ownership instead of ECC's
|
|
13
|
+
* profile/module/component vocabulary. Additive: writing this file changes no
|
|
14
|
+
* copy behavior.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
let Ajv = null;
|
|
21
|
+
try {
|
|
22
|
+
const ajvModule = require('ajv');
|
|
23
|
+
Ajv = ajvModule.default || ajvModule;
|
|
24
|
+
} catch (_error) {
|
|
25
|
+
Ajv = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SCHEMA_VERSION = 'mindforge.install.v1';
|
|
29
|
+
const SCHEMA_PATH = path.join(__dirname, '..', '..', '.mindforge', 'schemas', 'install-state.schema.json');
|
|
30
|
+
|
|
31
|
+
let cachedValidator = null;
|
|
32
|
+
|
|
33
|
+
function cloneJsonValue(value) {
|
|
34
|
+
if (value === undefined) return undefined;
|
|
35
|
+
return JSON.parse(JSON.stringify(value));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readJson(filePath, label) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`Failed to read ${label}: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getValidator() {
|
|
47
|
+
if (cachedValidator) return cachedValidator;
|
|
48
|
+
|
|
49
|
+
if (Ajv) {
|
|
50
|
+
const schema = readJson(SCHEMA_PATH, 'install-state schema');
|
|
51
|
+
const ajv = new Ajv({ allErrors: true });
|
|
52
|
+
cachedValidator = ajv.compile(schema);
|
|
53
|
+
return cachedValidator;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
cachedValidator = createFallbackValidator();
|
|
57
|
+
return cachedValidator;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Schema-free validator for bare checkouts where ajv is not installed. Mirrors
|
|
62
|
+
* the draft-07 schema's required fields and enums.
|
|
63
|
+
*/
|
|
64
|
+
function createFallbackValidator() {
|
|
65
|
+
const validate = state => {
|
|
66
|
+
const errors = [];
|
|
67
|
+
validate.errors = errors;
|
|
68
|
+
|
|
69
|
+
const pushError = (instancePath, message) => errors.push({ instancePath, message });
|
|
70
|
+
const isNonEmptyString = v => typeof v === 'string' && v.length > 0;
|
|
71
|
+
|
|
72
|
+
const noAdditional = (value, instancePath, allowedKeys) => {
|
|
73
|
+
for (const key of Object.keys(value)) {
|
|
74
|
+
if (!allowedKeys.includes(key)) {
|
|
75
|
+
pushError(`${instancePath}/${key}`, 'must NOT have additional properties');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const stringArray = (value, instancePath) => {
|
|
81
|
+
if (!Array.isArray(value)) { pushError(instancePath, 'must be array'); return; }
|
|
82
|
+
value.forEach((item, index) => {
|
|
83
|
+
if (!isNonEmptyString(item)) pushError(`${instancePath}/${index}`, 'must be non-empty string');
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const optionalNullableString = (value, instancePath) => {
|
|
88
|
+
if (value !== null && !isNonEmptyString(value)) pushError(instancePath, 'must be string or null');
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
92
|
+
pushError('/', 'must be object');
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
noAdditional(state, '', ['schemaVersion', 'installedAt', 'lastValidatedAt', 'target', 'request', 'source', 'operations']);
|
|
97
|
+
|
|
98
|
+
if (state.schemaVersion !== SCHEMA_VERSION) {
|
|
99
|
+
pushError('/schemaVersion', `must equal ${SCHEMA_VERSION}`);
|
|
100
|
+
}
|
|
101
|
+
if (!isNonEmptyString(state.installedAt)) {
|
|
102
|
+
pushError('/installedAt', 'must be non-empty string');
|
|
103
|
+
}
|
|
104
|
+
if (state.lastValidatedAt !== undefined && !isNonEmptyString(state.lastValidatedAt)) {
|
|
105
|
+
pushError('/lastValidatedAt', 'must be non-empty string');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const target = state.target;
|
|
109
|
+
if (!target || typeof target !== 'object' || Array.isArray(target)) {
|
|
110
|
+
pushError('/target', 'must be object');
|
|
111
|
+
} else {
|
|
112
|
+
noAdditional(target, '/target', ['scope', 'root', 'installStatePath']);
|
|
113
|
+
if (!['global', 'local'].includes(target.scope)) {
|
|
114
|
+
pushError('/target/scope', 'must be one of global|local');
|
|
115
|
+
}
|
|
116
|
+
if (!isNonEmptyString(target.root)) pushError('/target/root', 'must be non-empty string');
|
|
117
|
+
if (target.installStatePath !== undefined && !isNonEmptyString(target.installStatePath)) {
|
|
118
|
+
pushError('/target/installStatePath', 'must be non-empty string');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const request = state.request;
|
|
123
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)) {
|
|
124
|
+
pushError('/request', 'must be object');
|
|
125
|
+
} else {
|
|
126
|
+
noAdditional(request, '/request', ['runtimes', 'withUtils', 'minimal']);
|
|
127
|
+
stringArray(request.runtimes, '/request/runtimes');
|
|
128
|
+
if (request.withUtils !== undefined && typeof request.withUtils !== 'boolean') {
|
|
129
|
+
pushError('/request/withUtils', 'must be boolean');
|
|
130
|
+
}
|
|
131
|
+
if (request.minimal !== undefined && typeof request.minimal !== 'boolean') {
|
|
132
|
+
pushError('/request/minimal', 'must be boolean');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const source = state.source;
|
|
137
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
|
138
|
+
pushError('/source', 'must be object');
|
|
139
|
+
} else {
|
|
140
|
+
noAdditional(source, '/source', ['repoVersion', 'repoCommit']);
|
|
141
|
+
optionalNullableString(source.repoVersion, '/source/repoVersion');
|
|
142
|
+
optionalNullableString(source.repoCommit, '/source/repoCommit');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!Array.isArray(state.operations)) {
|
|
146
|
+
pushError('/operations', 'must be array');
|
|
147
|
+
} else {
|
|
148
|
+
state.operations.forEach((operation, index) => {
|
|
149
|
+
const instancePath = `/operations/${index}`;
|
|
150
|
+
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
|
|
151
|
+
pushError(instancePath, 'must be object');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!isNonEmptyString(operation.kind)) pushError(`${instancePath}/kind`, 'must be non-empty string');
|
|
155
|
+
if (!isNonEmptyString(operation.destinationPath)) pushError(`${instancePath}/destinationPath`, 'must be non-empty string');
|
|
156
|
+
if (!isNonEmptyString(operation.ownership)) pushError(`${instancePath}/ownership`, 'must be non-empty string');
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return errors.length === 0;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
validate.errors = [];
|
|
164
|
+
return validate;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatValidationErrors(errors = []) {
|
|
168
|
+
return errors.map(error => `${error.instancePath || '/'} ${error.message}`).join('; ');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function validateInstallState(state) {
|
|
172
|
+
const validator = getValidator();
|
|
173
|
+
const valid = validator(state);
|
|
174
|
+
return { valid, errors: validator.errors || [] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function assertValidInstallState(state, label) {
|
|
178
|
+
const result = validateInstallState(state);
|
|
179
|
+
if (!result.valid) {
|
|
180
|
+
throw new Error(`Invalid install-state${label ? ` (${label})` : ''}: ${formatValidationErrors(result.errors)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build a validated install-state object.
|
|
186
|
+
* @param {object} options
|
|
187
|
+
* @param {string} [options.installedAt] ISO timestamp; defaults to now.
|
|
188
|
+
* @param {string} [options.lastValidatedAt] ISO timestamp.
|
|
189
|
+
* @param {{scope:string, root:string, installStatePath?:string}} options.target
|
|
190
|
+
* @param {{runtimes:string[], withUtils?:boolean, minimal?:boolean}} options.request
|
|
191
|
+
* @param {{repoVersion?:string|null, repoCommit?:string|null}} options.source
|
|
192
|
+
* @param {Array<object>} [options.operations]
|
|
193
|
+
*/
|
|
194
|
+
function createInstallState(options) {
|
|
195
|
+
const installedAt = options.installedAt || new Date().toISOString();
|
|
196
|
+
const state = {
|
|
197
|
+
schemaVersion: SCHEMA_VERSION,
|
|
198
|
+
installedAt,
|
|
199
|
+
target: {
|
|
200
|
+
scope: options.target.scope,
|
|
201
|
+
root: options.target.root,
|
|
202
|
+
},
|
|
203
|
+
request: {
|
|
204
|
+
runtimes: Array.isArray(options.request.runtimes) ? [...options.request.runtimes] : [],
|
|
205
|
+
},
|
|
206
|
+
source: {
|
|
207
|
+
repoVersion: options.source.repoVersion || null,
|
|
208
|
+
repoCommit: options.source.repoCommit || null,
|
|
209
|
+
},
|
|
210
|
+
operations: Array.isArray(options.operations)
|
|
211
|
+
? options.operations.map(operation => cloneJsonValue(operation))
|
|
212
|
+
: [],
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (options.target.installStatePath) state.target.installStatePath = options.target.installStatePath;
|
|
216
|
+
if (typeof options.request.withUtils === 'boolean') state.request.withUtils = options.request.withUtils;
|
|
217
|
+
if (typeof options.request.minimal === 'boolean') state.request.minimal = options.request.minimal;
|
|
218
|
+
if (options.lastValidatedAt) state.lastValidatedAt = options.lastValidatedAt;
|
|
219
|
+
|
|
220
|
+
assertValidInstallState(state, 'create');
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readInstallState(filePath) {
|
|
225
|
+
const state = readJson(filePath, 'install-state');
|
|
226
|
+
assertValidInstallState(state, filePath);
|
|
227
|
+
return state;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function writeInstallState(filePath, state) {
|
|
231
|
+
assertValidInstallState(state, filePath);
|
|
232
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
233
|
+
fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
234
|
+
return state;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
SCHEMA_VERSION,
|
|
239
|
+
createInstallState,
|
|
240
|
+
readInstallState,
|
|
241
|
+
validateInstallState,
|
|
242
|
+
writeInstallState,
|
|
243
|
+
};
|
package/bin/installer-core.js
CHANGED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MindForge — Instinct CLI (deterministic, no LLM spawn).
|
|
6
|
+
*
|
|
7
|
+
* Manages the single JSONL instinct store the Wave-2 capture hook writes:
|
|
8
|
+
* list | projects | export | import | promote | prune
|
|
9
|
+
*
|
|
10
|
+
* Adapted from ECC's instinct-cli.py: keeps the security hardening (SSRF /
|
|
11
|
+
* path-traversal / id validation via lib/ssrf-guard.js, advisory file lock,
|
|
12
|
+
* atomic temp-rename writes) but operates over MindForge's flat JSONL store
|
|
13
|
+
* (NOT ECC's per-file YAML/homunculus model), keeps MindForge confidence math,
|
|
14
|
+
* and does NOT duplicate skill evolution (promote only LISTS/flags candidates;
|
|
15
|
+
* /mindforge:evolve-skills + cluster-instincts own actual promotion).
|
|
16
|
+
*
|
|
17
|
+
* Mutating subcommands (import/promote/prune) default to confirm/list-only and
|
|
18
|
+
* require --force to write; --dry-run everywhere. Project scoping uses the same
|
|
19
|
+
* detectProject() as the writer, so reads/writes scope identically.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const guard = require('./lib/ssrf-guard');
|
|
26
|
+
const { detectProject } = require('../hooks/lib/detect-project');
|
|
27
|
+
|
|
28
|
+
const CONFIG_PATH = path.join(process.cwd(), '.mindforge', 'config.json');
|
|
29
|
+
|
|
30
|
+
// ── config + store I/O ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function loadConfig() {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')).instincts || {};
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function storePath(cfg) {
|
|
41
|
+
const rel = cfg.store_path || '.mindforge/engine/instincts/instinct-store.jsonl';
|
|
42
|
+
return path.resolve(process.cwd(), rel);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readStore(p) {
|
|
46
|
+
let raw;
|
|
47
|
+
try { raw = fs.readFileSync(p, 'utf8'); } catch { return []; }
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const line of raw.split('\n')) {
|
|
50
|
+
const t = line.trim();
|
|
51
|
+
if (!t) continue;
|
|
52
|
+
try { out.push(JSON.parse(t)); }
|
|
53
|
+
catch { process.stderr.write('[instinct-cli] skipping malformed JSONL line\n'); }
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Atomic write: temp file + fsync + rename over the real store. */
|
|
59
|
+
function writeStoreAtomic(p, entries) {
|
|
60
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
61
|
+
const tmp = `${p}.tmp.${process.pid}`;
|
|
62
|
+
const body = entries.map(e => JSON.stringify(e)).join('\n') + (entries.length ? '\n' : '');
|
|
63
|
+
const fd = fs.openSync(tmp, 'w');
|
|
64
|
+
try {
|
|
65
|
+
fs.writeFileSync(fd, body);
|
|
66
|
+
fs.fsyncSync(fd);
|
|
67
|
+
} finally {
|
|
68
|
+
fs.closeSync(fd);
|
|
69
|
+
}
|
|
70
|
+
fs.renameSync(tmp, p);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Advisory lock (Node has no fcntl): exclusive lockfile create, spin-with-timeout,
|
|
75
|
+
* stale-break after 10s by mtime. Runs fn() while held, releases in finally.
|
|
76
|
+
* Read the store INSIDE this so a prune/import rewrite can't race a hook append.
|
|
77
|
+
*/
|
|
78
|
+
function withStoreLock(p, fn) {
|
|
79
|
+
const lock = `${p}.lock`;
|
|
80
|
+
const maxTries = 50, waitMs = 20, staleMs = 10000;
|
|
81
|
+
let held = false;
|
|
82
|
+
for (let i = 0; i < maxTries && !held; i++) {
|
|
83
|
+
try {
|
|
84
|
+
const fd = fs.openSync(lock, 'wx');
|
|
85
|
+
fs.closeSync(fd);
|
|
86
|
+
held = true;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err.code !== 'EEXIST') throw err;
|
|
89
|
+
// stale-break: if the lockfile is older than staleMs, remove it.
|
|
90
|
+
try {
|
|
91
|
+
const age = Date.now() - fs.statSync(lock).mtimeMs;
|
|
92
|
+
if (age > staleMs) { fs.unlinkSync(lock); continue; }
|
|
93
|
+
} catch { /* lock vanished — retry */ }
|
|
94
|
+
const until = Date.now() + waitMs;
|
|
95
|
+
while (Date.now() < until) { /* busy-wait (short) */ }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!held) throw new Error(`could not acquire instinct-store lock: ${lock}`);
|
|
99
|
+
try { return fn(); }
|
|
100
|
+
finally { try { fs.unlinkSync(lock); } catch { /* already gone */ } }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function currentProjectId() {
|
|
104
|
+
try { return detectProject(process.cwd()).id; } catch { return 'global'; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function confidenceBar(c) {
|
|
110
|
+
const n = Math.max(0, Math.min(10, Math.round((Number(c) || 0) * 10)));
|
|
111
|
+
return '█'.repeat(n) + '░'.repeat(10 - n);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function inScope(entry, scopeId, all) {
|
|
115
|
+
if (all) return true;
|
|
116
|
+
const pid = entry.project_id || 'global';
|
|
117
|
+
return pid === scopeId || pid === 'global';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── subcommands ─────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function cmdList(args, cfg) {
|
|
123
|
+
const store = readStore(storePath(cfg));
|
|
124
|
+
const scopeId = args.project || currentProjectId();
|
|
125
|
+
const filtered = store.filter(e =>
|
|
126
|
+
inScope(e, scopeId, args.all) &&
|
|
127
|
+
(!args.status || e.status === args.status));
|
|
128
|
+
if (args.json) { process.stdout.write(JSON.stringify(filtered, null, 2) + '\n'); return 0; }
|
|
129
|
+
if (!filtered.length) { process.stdout.write('No instincts in scope.\n'); return 0; }
|
|
130
|
+
for (const e of filtered) {
|
|
131
|
+
process.stdout.write(`${confidenceBar(e.confidence)} ${(e.confidence ?? 0).toFixed(2)} [${e.status}] ${e.id}\n obs: ${e.observation}\n do: ${e.behavior}\n applied ${e.times_applied || 0} (✓${e.times_succeeded || 0}/✗${e.times_failed || 0})\n`);
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function cmdProjects(args, cfg) {
|
|
137
|
+
const store = readStore(storePath(cfg));
|
|
138
|
+
const groups = new Map();
|
|
139
|
+
for (const e of store) {
|
|
140
|
+
const pid = e.project_id || 'global';
|
|
141
|
+
if (!groups.has(pid)) groups.set(pid, { project_id: pid, project: e.project || pid, count: 0, active: 0, sumConf: 0, lastApplied: null });
|
|
142
|
+
const g = groups.get(pid);
|
|
143
|
+
g.count++;
|
|
144
|
+
if (e.status === 'active') g.active++;
|
|
145
|
+
g.sumConf += Number(e.confidence) || 0;
|
|
146
|
+
if (e.last_applied_at && (!g.lastApplied || e.last_applied_at > g.lastApplied)) g.lastApplied = e.last_applied_at;
|
|
147
|
+
}
|
|
148
|
+
const rows = [...groups.values()].map(g => ({
|
|
149
|
+
project_id: g.project_id, project: g.project, count: g.count, active: g.active,
|
|
150
|
+
avg_confidence: g.count ? +(g.sumConf / g.count).toFixed(3) : 0, last_applied: g.lastApplied,
|
|
151
|
+
}));
|
|
152
|
+
if (args.json) { process.stdout.write(JSON.stringify(rows, null, 2) + '\n'); return 0; }
|
|
153
|
+
for (const r of rows) {
|
|
154
|
+
process.stdout.write(`${r.project} [${r.project_id}] — ${r.count} instincts (${r.active} active), avg ${r.avg_confidence}, last ${r.last_applied || 'never'}\n`);
|
|
155
|
+
}
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cmdExport(args, cfg) {
|
|
160
|
+
const store = readStore(storePath(cfg));
|
|
161
|
+
const scopeId = args.project || currentProjectId();
|
|
162
|
+
const minC = args.minConfidence != null ? Number(args.minConfidence) : -1;
|
|
163
|
+
const subset = store.filter(e =>
|
|
164
|
+
inScope(e, scopeId, args.all) &&
|
|
165
|
+
(!args.status || e.status === args.status) &&
|
|
166
|
+
(Number(e.confidence) || 0) >= minC);
|
|
167
|
+
const body = subset.map(e => JSON.stringify(e)).join('\n') + (subset.length ? '\n' : '');
|
|
168
|
+
if (args.output) {
|
|
169
|
+
const out = guard.validateFilePath(args.output);
|
|
170
|
+
fs.writeFileSync(out, body);
|
|
171
|
+
process.stderr.write(`Exported ${subset.length} instinct(s) to ${out}\n`);
|
|
172
|
+
} else {
|
|
173
|
+
process.stdout.write(body);
|
|
174
|
+
}
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseImportPayload(text) {
|
|
179
|
+
const trimmed = text.trim();
|
|
180
|
+
if (trimmed.startsWith('[')) {
|
|
181
|
+
const arr = JSON.parse(trimmed);
|
|
182
|
+
if (!Array.isArray(arr)) throw new Error('import JSON must be an array');
|
|
183
|
+
return arr;
|
|
184
|
+
}
|
|
185
|
+
// JSONL
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const line of trimmed.split('\n')) {
|
|
188
|
+
const t = line.trim();
|
|
189
|
+
if (t) out.push(JSON.parse(t));
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function cmdImport(args, cfg) {
|
|
195
|
+
if (!args._[0]) { process.stderr.write('import requires a <file|https-url>\n'); return 1; }
|
|
196
|
+
const source = args._[0];
|
|
197
|
+
let text;
|
|
198
|
+
if (/^https?:\/\//i.test(source)) {
|
|
199
|
+
text = await guard.fetchImportUrl(source);
|
|
200
|
+
} else {
|
|
201
|
+
text = fs.readFileSync(guard.validateFilePath(source, { mustExist: true }), 'utf8');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let incoming;
|
|
205
|
+
try { incoming = parseImportPayload(text); }
|
|
206
|
+
catch (e) { process.stderr.write(`malformed import payload: ${e.message}\n`); return 2; }
|
|
207
|
+
|
|
208
|
+
const minC = args.minConfidence != null ? Number(args.minConfidence) : -1;
|
|
209
|
+
const scopeId = args.scope === 'global' ? 'global' : currentProjectId();
|
|
210
|
+
const projectName = args.scope === 'global' ? 'global' : (() => { try { return detectProject(process.cwd()).name; } catch { return 'global'; } })();
|
|
211
|
+
const now = new Date().toISOString();
|
|
212
|
+
|
|
213
|
+
const valid = [];
|
|
214
|
+
for (const e of incoming) {
|
|
215
|
+
if (!e || !guard.validateInstinctId(e.id || '')) { process.stderr.write(`skipping entry with invalid id: ${e && e.id}\n`); continue; }
|
|
216
|
+
if ((Number(e.confidence) || 0) < minC) continue;
|
|
217
|
+
valid.push(Object.assign({}, e, {
|
|
218
|
+
project: projectName, project_id: scopeId, source: 'imported',
|
|
219
|
+
imported_from: source, created_at: now, updated_at: now,
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (args.dryRun || !args.force) {
|
|
224
|
+
process.stdout.write(`${args.dryRun ? '[dry-run] ' : ''}${valid.length} instinct(s) would be imported into ${scopeId}. Re-run with --force to write.\n`);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const p = storePath(cfg);
|
|
229
|
+
withStoreLock(p, () => {
|
|
230
|
+
const store = readStore(p); // read INSIDE the lock (race-safe)
|
|
231
|
+
const byId = new Map(store.map(e => [e.id, e]));
|
|
232
|
+
for (const e of valid) {
|
|
233
|
+
const existing = byId.get(e.id);
|
|
234
|
+
// dedup: keep the higher-confidence entry (immutable — new object)
|
|
235
|
+
if (!existing || (Number(e.confidence) || 0) > (Number(existing.confidence) || 0)) byId.set(e.id, e);
|
|
236
|
+
}
|
|
237
|
+
writeStoreAtomic(p, [...byId.values()]);
|
|
238
|
+
});
|
|
239
|
+
process.stdout.write(`Imported ${valid.length} instinct(s) into ${scopeId}.\n`);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function cmdPromote(args, cfg) {
|
|
244
|
+
const store = readStore(storePath(cfg));
|
|
245
|
+
const threshold = cfg.promotion_confidence_threshold ?? 0.85;
|
|
246
|
+
const minApplied = cfg.promotion_min_applications ?? 5;
|
|
247
|
+
const scopeId = args.project || currentProjectId();
|
|
248
|
+
const candidates = store.filter(e =>
|
|
249
|
+
inScope(e, scopeId, args.all) && e.status === 'active' &&
|
|
250
|
+
(Number(e.confidence) || 0) >= threshold && (e.times_applied || 0) >= minApplied &&
|
|
251
|
+
(!args._[0] || e.id === args._[0]));
|
|
252
|
+
|
|
253
|
+
if (!candidates.length) { process.stdout.write('No promotion candidates (need confidence ≥ ' + threshold + ' AND applied ≥ ' + minApplied + ').\n'); return 0; }
|
|
254
|
+
|
|
255
|
+
// Default: LIST candidates only. Actual SKILL.md creation is owned by
|
|
256
|
+
// /mindforge:evolve-skills + /mindforge:cluster-instincts — do NOT duplicate.
|
|
257
|
+
process.stdout.write('Promotion candidate(s) — run /mindforge:evolve-skills to create skills:\n');
|
|
258
|
+
for (const e of candidates) process.stdout.write(` ${e.id} conf ${(e.confidence).toFixed(2)} applied ${e.times_applied}\n ${e.behavior}\n`);
|
|
259
|
+
|
|
260
|
+
if (args.force && !args.dryRun) {
|
|
261
|
+
const ids = new Set(candidates.map(e => e.id));
|
|
262
|
+
const p = storePath(cfg);
|
|
263
|
+
withStoreLock(p, () => {
|
|
264
|
+
const fresh = readStore(p);
|
|
265
|
+
const updated = fresh.map(e => ids.has(e.id) ? Object.assign({}, e, { status: 'active', promotion_candidate: true, updated_at: new Date().toISOString() }) : e);
|
|
266
|
+
writeStoreAtomic(p, updated);
|
|
267
|
+
});
|
|
268
|
+
process.stdout.write(`Flagged ${ids.size} instinct(s) as promotion_candidate (skill creation still via evolve-skills).\n`);
|
|
269
|
+
}
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function cmdPrune(args, cfg) {
|
|
274
|
+
const pruneBelow = cfg.prune_below_confidence ?? 0.2;
|
|
275
|
+
const maxAgeDays = args.maxAge != null ? Number(args.maxAge) : (cfg.prune_after_days_inactive ?? 30);
|
|
276
|
+
const cutoff = Date.now() - maxAgeDays * 86400000;
|
|
277
|
+
|
|
278
|
+
const p = storePath(cfg);
|
|
279
|
+
const store = readStore(p);
|
|
280
|
+
const isStale = e => {
|
|
281
|
+
if ((Number(e.confidence) || 0) < pruneBelow && (e.times_applied || 0) >= 10) return true;
|
|
282
|
+
if (e.last_applied_at && Date.parse(e.last_applied_at) < cutoff) return true;
|
|
283
|
+
return false;
|
|
284
|
+
};
|
|
285
|
+
const doomed = store.filter(e => e.status === 'active' && isStale(e));
|
|
286
|
+
|
|
287
|
+
if (!doomed.length) { process.stdout.write('Nothing to prune.\n'); return 0; }
|
|
288
|
+
if (args.dryRun || !args.force) {
|
|
289
|
+
process.stdout.write(`${args.dryRun ? '[dry-run] ' : ''}${doomed.length} instinct(s) would be pruned. Re-run with --force.\n`);
|
|
290
|
+
for (const e of doomed) process.stdout.write(` ${e.id} conf ${(e.confidence ?? 0).toFixed(2)}\n`);
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const doomedIds = new Set(doomed.map(e => e.id));
|
|
295
|
+
withStoreLock(p, () => {
|
|
296
|
+
const fresh = readStore(p); // read INSIDE the lock
|
|
297
|
+
const updated = fresh.map(e => doomedIds.has(e.id) ? Object.assign({}, e, { status: 'pruned', updated_at: new Date().toISOString() }) : e);
|
|
298
|
+
writeStoreAtomic(p, updated);
|
|
299
|
+
});
|
|
300
|
+
process.stdout.write(`Pruned ${doomedIds.size} instinct(s).\n`);
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── arg parsing + main ──────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function parseArgs(argv) {
|
|
307
|
+
const args = { _: [] };
|
|
308
|
+
for (let i = 0; i < argv.length; i++) {
|
|
309
|
+
const a = argv[i];
|
|
310
|
+
if (a === '--all') args.all = true;
|
|
311
|
+
else if (a === '--json') args.json = true;
|
|
312
|
+
else if (a === '--dry-run') args.dryRun = true;
|
|
313
|
+
else if (a === '--force') args.force = true;
|
|
314
|
+
else if (a === '--project') args.project = argv[++i];
|
|
315
|
+
else if (a === '--status') args.status = argv[++i];
|
|
316
|
+
else if (a === '--scope') args.scope = argv[++i];
|
|
317
|
+
else if (a === '--min-confidence') args.minConfidence = argv[++i];
|
|
318
|
+
else if (a === '--max-age') args.maxAge = argv[++i];
|
|
319
|
+
else if (a === '-o' || a === '--output') args.output = argv[++i];
|
|
320
|
+
else if (!a.startsWith('-')) args._.push(a);
|
|
321
|
+
}
|
|
322
|
+
return args;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const USAGE = `Usage: instinct-cli <command> [options]
|
|
326
|
+
Commands:
|
|
327
|
+
list [--project <id>|--all] [--status active|promoted|deprecated|pruned] [--json]
|
|
328
|
+
projects [--json]
|
|
329
|
+
export [--project <id>|--all] [--status <s>] [--min-confidence N] [-o file]
|
|
330
|
+
import <file|https-url> [--scope project|global] [--min-confidence N] [--dry-run] [--force]
|
|
331
|
+
promote [<id>] [--project <id>|--all] [--dry-run] [--force] (lists candidates; skills via /mindforge:evolve-skills)
|
|
332
|
+
prune [--max-age <days>] [--dry-run] [--force]`;
|
|
333
|
+
|
|
334
|
+
async function main() {
|
|
335
|
+
const [, , sub, ...rest] = process.argv;
|
|
336
|
+
if (!sub || sub === '--help' || sub === '-h') { process.stdout.write(USAGE + '\n'); return 0; }
|
|
337
|
+
const args = parseArgs(rest);
|
|
338
|
+
const cfg = loadConfig();
|
|
339
|
+
switch (sub) {
|
|
340
|
+
case 'list': return cmdList(args, cfg);
|
|
341
|
+
case 'projects': return cmdProjects(args, cfg);
|
|
342
|
+
case 'export': return cmdExport(args, cfg);
|
|
343
|
+
case 'import': return cmdImport(args, cfg);
|
|
344
|
+
case 'promote': return cmdPromote(args, cfg);
|
|
345
|
+
case 'prune': return cmdPrune(args, cfg);
|
|
346
|
+
default:
|
|
347
|
+
process.stderr.write(`Unknown command: ${sub}\n${USAGE}\n`);
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (require.main === module) {
|
|
353
|
+
main().then(code => process.exit(code)).catch(err => {
|
|
354
|
+
process.stderr.write(`[instinct-cli] ${err.message}\n`);
|
|
355
|
+
process.exit(2);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = { loadConfig, storePath, readStore, writeStoreAtomic, withStoreLock, parseArgs };
|