harness-everything 0.8.1 → 0.9.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +34 -0
- package/ci/action-gate.test.js +82 -2
- package/ci/phase4-ensemble-memory.test.js +128 -10
- package/ci/router-workflow-plan.test.js +23 -0
- package/ci/workflow-execution-contract.test.js +73 -0
- package/ci/workflow-plan-consumers.test.js +18 -0
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/agents/fable-orchestrator.md +9 -7
- package/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/fable-mode/scripts/workflow-plan-consumer.js +10 -7
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/schemas/router-task-shape.schema.json +4 -2
- package/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
- package/harness-everything/scripts/kernel-router-core.js +2 -0
- package/harness-everything/scripts/kernel-router.js +25 -2
- package/harness-everything/scripts/router-contract-core.js +25 -4
- package/harness-everything/scripts/tier-router.js +19 -1
- package/hooks/hooks.json +3 -3
- package/hooks/scripts/action-gate.js +111 -2
- package/hooks/scripts/contract-test.js +4 -1
- package/hooks/scripts/lib/workflow-runtime.js +153 -1
- package/hooks/scripts/subagent-scope-guard.js +20 -3
- package/hooks/scripts/workflow-disposition.js +11 -4
- package/hooks/scripts/workflow-gate.js +4 -1
- package/multi-agent-workspace/SKILL.md +2 -2
- package/multi-agent-workspace/scripts/index_memory.js +107 -1
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +24 -0
- package/plugins/harness-everything/hooks/scripts/action-gate.js +111 -2
- package/plugins/harness-everything/hooks/scripts/contract-test.js +4 -1
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +153 -1
- package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +20 -3
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +11 -4
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +4 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +9 -7
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +10 -7
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/schemas/router-task-shape.schema.json +4 -2
- package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +2 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +25 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +25 -4
- package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +19 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +2 -2
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +107 -1
- package/plugins/harness-everything/skills/self-evolve/SKILL.md +6 -6
- package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +22 -33
- package/plugins/harness-everything/skills/self-evolve/scripts/persist-memory.js +336 -57
- package/plugins/harness-everything/skills/self-evolve/scripts/self-regression.js +1 -0
- package/self-evolve/SKILL.md +6 -6
- package/self-evolve/references/memory-resolution.md +22 -33
- package/self-evolve/scripts/persist-memory.js +336 -57
- package/self-evolve/scripts/self-regression.js +1 -0
|
@@ -5,10 +5,14 @@ const crypto = require('crypto');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
let getWorkspaceRoot;
|
|
8
|
+
let getWorkspaceStateDir;
|
|
9
|
+
let getWorkspaceKey;
|
|
8
10
|
try {
|
|
9
|
-
({ getWorkspaceRoot } = require('../../scripts/lib/workspace'));
|
|
11
|
+
({ getWorkspaceRoot, getWorkspaceStateDir, getWorkspaceKey } = require('../../scripts/lib/workspace'));
|
|
10
12
|
} catch (err) {
|
|
11
13
|
getWorkspaceRoot = () => null;
|
|
14
|
+
getWorkspaceStateDir = () => null;
|
|
15
|
+
getWorkspaceKey = () => null;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
// NOTE: this script ships standalone - it is copied whole into every install
|
|
@@ -35,27 +39,54 @@ const INJECTION_PATTERNS = [
|
|
|
35
39
|
];
|
|
36
40
|
|
|
37
41
|
function parseArgs(argv) {
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
const parsed = {
|
|
43
|
+
help: false,
|
|
44
|
+
memoryText: null,
|
|
45
|
+
source: 'manual-unspecified',
|
|
46
|
+
authorization: null,
|
|
47
|
+
validUntil: null,
|
|
48
|
+
retentionDays: null,
|
|
49
|
+
scopeTask: null,
|
|
50
|
+
scopeRequirement: null,
|
|
51
|
+
scopeRole: null,
|
|
52
|
+
};
|
|
53
|
+
const valued = new Map([
|
|
54
|
+
['--source', 'source'],
|
|
55
|
+
['--authorization', 'authorization'],
|
|
56
|
+
['--valid-until', 'validUntil'],
|
|
57
|
+
['--retention-days', 'retentionDays'],
|
|
58
|
+
['--scope-task', 'scopeTask'],
|
|
59
|
+
['--scope-requirement', 'scopeRequirement'],
|
|
60
|
+
['--scope-role', 'scopeRole'],
|
|
61
|
+
]);
|
|
40
62
|
for (let index = 0; index < argv.length; index++) {
|
|
41
63
|
const token = argv[index];
|
|
42
|
-
if (token === '--help' || token === '-h')
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
if (token === '--help' || token === '-h') {
|
|
65
|
+
parsed.help = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const direct = valued.get(token);
|
|
69
|
+
if (direct) {
|
|
70
|
+
if (index + 1 >= argv.length) throw new Error(`${token} requires a value`);
|
|
71
|
+
parsed[direct] = argv[++index];
|
|
46
72
|
continue;
|
|
47
73
|
}
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
const equals = [...valued.entries()].find(([flag]) => token.startsWith(`${flag}=`));
|
|
75
|
+
if (equals) {
|
|
76
|
+
parsed[equals[1]] = token.slice(equals[0].length + 1);
|
|
50
77
|
continue;
|
|
51
78
|
}
|
|
52
79
|
if (token.startsWith('-')) throw new Error(`unknown option: ${token}`);
|
|
53
|
-
if (memoryText !== null) throw new Error('exactly one memory rule positional argument is allowed');
|
|
54
|
-
memoryText = token;
|
|
80
|
+
if (parsed.memoryText !== null) throw new Error('exactly one memory rule positional argument is allowed');
|
|
81
|
+
parsed.memoryText = token;
|
|
55
82
|
}
|
|
56
|
-
|
|
83
|
+
if (parsed.retentionDays !== null) {
|
|
84
|
+
const days = Number(parsed.retentionDays);
|
|
85
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650) throw new Error('--retention-days must be an integer from 1 to 3650');
|
|
86
|
+
parsed.retentionDays = days;
|
|
87
|
+
}
|
|
88
|
+
return parsed;
|
|
57
89
|
}
|
|
58
|
-
|
|
59
90
|
function normalizeSource(value) {
|
|
60
91
|
const source = String(value || 'manual-unspecified').trim();
|
|
61
92
|
if (!source) return 'manual-unspecified';
|
|
@@ -126,103 +157,346 @@ function contentHash(text) {
|
|
|
126
157
|
return crypto.createHash('sha256').update(String(text), 'utf8').digest('hex');
|
|
127
158
|
}
|
|
128
159
|
|
|
129
|
-
|
|
160
|
+
const MEMORY_INDEX_SCHEMA_VERSION = 1;
|
|
161
|
+
const PARAPHRASE_REVIEW_THRESHOLD = 0.45;
|
|
162
|
+
const DUPLICATE_REVIEW_THRESHOLD = 0.70;
|
|
163
|
+
|
|
164
|
+
function readJson(filePath) {
|
|
165
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (_) { return null; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function atomicWriteJson(filePath, value) {
|
|
169
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
170
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
171
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
172
|
+
fs.renameSync(temporary, filePath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function atomicWriteText(filePath, value) {
|
|
176
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
177
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
178
|
+
fs.writeFileSync(temporary, value, 'utf8');
|
|
179
|
+
fs.renameSync(temporary, filePath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function safeSessionName(sessionId) {
|
|
183
|
+
const value = String(sessionId || '').replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^\.+/, '').slice(0, 128);
|
|
184
|
+
return value || null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function listWorkflowAuthorizations(workspaceRoot, capability) {
|
|
188
|
+
if (!workspaceRoot || typeof getWorkspaceStateDir !== 'function' || !capability) return [];
|
|
189
|
+
const stateDir = getWorkspaceStateDir(workspaceRoot);
|
|
190
|
+
if (!stateDir) return [];
|
|
191
|
+
const sessionsRoot = path.join(stateDir, 'state', 'sessions');
|
|
192
|
+
let entries = [];
|
|
193
|
+
try { entries = fs.readdirSync(sessionsRoot, { withFileTypes: true }); } catch (_) { return []; }
|
|
194
|
+
const wantedHash = contentHash(capability);
|
|
195
|
+
const matches = [];
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (!entry.isDirectory()) continue;
|
|
198
|
+
const workflowFile = path.join(sessionsRoot, entry.name, 'workflow-run.json');
|
|
199
|
+
const workflow = readJson(workflowFile);
|
|
200
|
+
const auth = workflow && workflow.memoryAuthorization;
|
|
201
|
+
if (!workflow || workflow.schemaVersion !== 2 || !workflow.workflowId || !workflow.sessionId || !auth) continue;
|
|
202
|
+
if (auth.schemaVersion !== 1 || auth.capabilityHash !== wantedHash || auth.usedAt) continue;
|
|
203
|
+
if (auth.workflowId !== workflow.workflowId || auth.sessionId !== workflow.sessionId) continue;
|
|
204
|
+
if (safeSessionName(workflow.sessionId) !== entry.name) continue;
|
|
205
|
+
const disposition = workflow.workflowPlan && workflow.workflowPlan.memory && workflow.workflowPlan.memory.write;
|
|
206
|
+
if (auth.writeDisposition !== disposition || !['none', 'propose', 'persist-via-self-evolve'].includes(disposition)) continue;
|
|
207
|
+
if (['blocked', 'failed', 'deferred'].includes(workflow.state)) continue;
|
|
208
|
+
let effectiveDisposition = disposition;
|
|
209
|
+
const reasonCodes = [];
|
|
210
|
+
if (disposition === 'persist-via-self-evolve' && String(workflow.strategy || '').startsWith('fable-') &&
|
|
211
|
+
workflow.runId && workflow.state !== 'satisfied') {
|
|
212
|
+
effectiveDisposition = 'propose';
|
|
213
|
+
reasonCodes.push('active-fable-worker-candidate-only');
|
|
214
|
+
}
|
|
215
|
+
matches.push({
|
|
216
|
+
workflowFile,
|
|
217
|
+
sessionDir: path.dirname(workflowFile),
|
|
218
|
+
workflow,
|
|
219
|
+
sessionId: workflow.sessionId,
|
|
220
|
+
workflowId: workflow.workflowId,
|
|
221
|
+
runId: workflow.runId || null,
|
|
222
|
+
writerRole: auth.writerRole || 'coordinator',
|
|
223
|
+
disposition,
|
|
224
|
+
effectiveDisposition,
|
|
225
|
+
reasonCodes,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return matches;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveMemoryAuthorization(workspaceRoot, capability) {
|
|
232
|
+
if (!capability) return { ok: false, message: 'memory write requires the single-use workflow authorization capability' };
|
|
233
|
+
const matches = listWorkflowAuthorizations(workspaceRoot, capability);
|
|
234
|
+
if (matches.length !== 1) {
|
|
235
|
+
return { ok: false, message: matches.length === 0 ? 'memory authorization is invalid, used, expired, or not bound to this workspace' : 'memory authorization is ambiguous across runtime sessions' };
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, ...matches[0] };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function consumeAuthorization(auth, resultDisposition) {
|
|
241
|
+
const workflow = readJson(auth.workflowFile);
|
|
242
|
+
if (!workflow || workflow.workflowId !== auth.workflowId || workflow.sessionId !== auth.sessionId ||
|
|
243
|
+
!workflow.memoryAuthorization || workflow.memoryAuthorization.usedAt) {
|
|
244
|
+
throw new Error('memory authorization changed before commit; refusing write');
|
|
245
|
+
}
|
|
246
|
+
workflow.memoryAuthorization.usedAt = new Date().toISOString();
|
|
247
|
+
workflow.memoryAuthorization.resultDisposition = resultDisposition;
|
|
248
|
+
atomicWriteJson(auth.workflowFile, workflow);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeTerms(value) {
|
|
252
|
+
const stop = new Set(['the','and','for','with','from','this','that','into','when','then','before','after','always','never','should','must','use','using','verify','check']);
|
|
253
|
+
const matches = String(value || '').toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}._-]{1,}/gu) || [];
|
|
254
|
+
return [...new Set(matches.filter(term => !stop.has(term)))].slice(0, 48);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeRoles(value) {
|
|
258
|
+
return [...new Set(String(value || '').split(',').map(item => item.trim().toLowerCase()).filter(Boolean))].slice(0, 16);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function resolveValidUntil(validUntil, retentionDays) {
|
|
262
|
+
if (validUntil && retentionDays) throw new Error('use either --valid-until or --retention-days, not both');
|
|
263
|
+
if (retentionDays) return new Date(Date.now() + retentionDays * 86400000).toISOString();
|
|
264
|
+
if (!validUntil) return null;
|
|
265
|
+
const timestamp = Date.parse(validUntil);
|
|
266
|
+
if (!Number.isFinite(timestamp)) throw new Error('--valid-until must be an ISO-8601 date/time');
|
|
267
|
+
if (timestamp <= Date.now()) throw new Error('--valid-until must be in the future');
|
|
268
|
+
return new Date(timestamp).toISOString();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function loadMemoryIndex(indexFile) {
|
|
272
|
+
if (!fs.existsSync(indexFile)) return { schemaVersion: MEMORY_INDEX_SCHEMA_VERSION, records: [] };
|
|
273
|
+
const parsed = readJson(indexFile);
|
|
274
|
+
if (!parsed || parsed.schemaVersion !== MEMORY_INDEX_SCHEMA_VERSION || !Array.isArray(parsed.records)) {
|
|
275
|
+
throw new Error('memory-index.json is malformed; refusing to overwrite durable metadata');
|
|
276
|
+
}
|
|
277
|
+
return parsed;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function maxSimilarity(ruleText, existingRules) {
|
|
281
|
+
let best = { score: 0, text: null };
|
|
282
|
+
for (const existing of existingRules) {
|
|
283
|
+
const score = calculateSimilarity(ruleText, existing);
|
|
284
|
+
if (score > best.score) best = { score, text: existing };
|
|
285
|
+
}
|
|
286
|
+
return best;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function writeCandidate(auth, record) {
|
|
290
|
+
const candidateDir = path.join(auth.sessionDir, 'memory-candidates');
|
|
291
|
+
const candidateFile = path.join(candidateDir, `${record.candidateId}.json`);
|
|
292
|
+
atomicWriteJson(candidateFile, record);
|
|
293
|
+
consumeAuthorization(auth, record.status);
|
|
294
|
+
return candidateFile;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function candidateRecord(auth, ruleText, provenance, screening, qualityScore, similarity, reasonCodes, scope) {
|
|
298
|
+
const hash = contentHash(ruleText);
|
|
299
|
+
return {
|
|
300
|
+
schemaVersion: 1,
|
|
301
|
+
candidateId: `candidate-${hash.slice(0, 20)}`,
|
|
302
|
+
contentSha256: hash,
|
|
303
|
+
ruleText,
|
|
304
|
+
source: provenance,
|
|
305
|
+
status: 'proposed',
|
|
306
|
+
reasonCodes: [...new Set(reasonCodes)],
|
|
307
|
+
qualityScore,
|
|
308
|
+
screening: { secret: 'clear', promptInjection: 'clear' },
|
|
309
|
+
similarity: similarity.score > 0 ? { score: similarity.score, existingRule: similarity.text } : null,
|
|
310
|
+
scope,
|
|
311
|
+
authorization: {
|
|
312
|
+
sessionId: auth.sessionId,
|
|
313
|
+
workflowId: auth.workflowId,
|
|
314
|
+
runId: auth.runId,
|
|
315
|
+
writerRole: auth.writerRole,
|
|
316
|
+
requestedDisposition: auth.disposition,
|
|
317
|
+
effectiveDisposition: auth.effectiveDisposition,
|
|
318
|
+
},
|
|
319
|
+
createdAt: new Date().toISOString(),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function persistMemory({
|
|
324
|
+
memoryText,
|
|
325
|
+
source = 'manual-unspecified',
|
|
326
|
+
authorization = null,
|
|
327
|
+
validUntil = null,
|
|
328
|
+
retentionDays = null,
|
|
329
|
+
scopeTask = null,
|
|
330
|
+
scopeRequirement = null,
|
|
331
|
+
scopeRole = null,
|
|
332
|
+
}) {
|
|
130
333
|
const ruleText = String(memoryText || '').trim();
|
|
131
334
|
if (!ruleText) return { ok: false, exitCode: 1, message: 'memory rule is required' };
|
|
132
335
|
|
|
133
336
|
const screening = screenMemoryForPersistence(ruleText);
|
|
134
337
|
if (!screening.allowed) {
|
|
135
338
|
const reasons = [...screening.secretReasonCodes, ...screening.injectionReasonCodes];
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
exitCode: 2,
|
|
139
|
-
screening,
|
|
140
|
-
message: `memory rejected by persistence screening: ${reasons.join(', ')}`,
|
|
141
|
-
};
|
|
339
|
+
return { ok: false, exitCode: 2, screening, message: `memory rejected by persistence screening: ${reasons.join(', ')}` };
|
|
142
340
|
}
|
|
143
341
|
|
|
144
342
|
let provenance;
|
|
343
|
+
let resolvedValidUntil;
|
|
145
344
|
try {
|
|
146
345
|
provenance = normalizeSource(source);
|
|
346
|
+
resolvedValidUntil = resolveValidUntil(validUntil, retentionDays);
|
|
147
347
|
} catch (err) {
|
|
148
348
|
return { ok: false, exitCode: 2, screening, message: err.message };
|
|
149
349
|
}
|
|
150
350
|
|
|
151
351
|
const currentDir = getWorkspaceRoot();
|
|
152
352
|
if (!currentDir) {
|
|
153
|
-
return {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
353
|
+
return { ok: false, exitCode: 1, screening, message: 'Cannot persist project memory without a resolved git workspace.' };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const auth = resolveMemoryAuthorization(currentDir, authorization);
|
|
357
|
+
if (!auth.ok) {
|
|
358
|
+
return { ok: false, exitCode: 3, screening, message: auth.message };
|
|
359
|
+
}
|
|
360
|
+
if (auth.effectiveDisposition === 'none') {
|
|
361
|
+
return { ok: false, exitCode: 3, screening, message: 'current workflow memory.write=none forbids durable memory and proposals' };
|
|
159
362
|
}
|
|
160
363
|
|
|
161
364
|
const memoryDir = path.join(currentDir, 'memories', 'repo');
|
|
162
365
|
const rulesFile = path.join(memoryDir, 'RULES.md');
|
|
366
|
+
const indexFile = path.join(memoryDir, 'memory-index.json');
|
|
163
367
|
const existingRules = extractExistingRules(rulesFile);
|
|
368
|
+
const similarity = maxSimilarity(ruleText, existingRules);
|
|
164
369
|
const qualityScore = scoreRuleQuality(ruleText, existingRules);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
exitCode: 1,
|
|
169
|
-
screening,
|
|
170
|
-
qualityScore,
|
|
171
|
-
message: `Rule rejected - score ${qualityScore}/10 (minimum 5 required)`,
|
|
172
|
-
};
|
|
370
|
+
const baseQualityScore = scoreRuleQuality(ruleText, []);
|
|
371
|
+
if (Math.max(qualityScore, baseQualityScore) < 5) {
|
|
372
|
+
return { ok: false, exitCode: 1, screening, qualityScore, message: `Rule rejected - score ${qualityScore}/10 (minimum 5 required)` };
|
|
173
373
|
}
|
|
174
374
|
|
|
175
|
-
const
|
|
176
|
-
|
|
375
|
+
const scope = {
|
|
376
|
+
taskTerms: normalizeTerms(scopeTask || ruleText),
|
|
377
|
+
requirementTerms: normalizeTerms(scopeRequirement),
|
|
378
|
+
roles: normalizeRoles(scopeRole),
|
|
379
|
+
};
|
|
380
|
+
const candidateReasons = [...auth.reasonCodes];
|
|
381
|
+
if (similarity.score > DUPLICATE_REVIEW_THRESHOLD) candidateReasons.push('likely-duplicate-review-required');
|
|
382
|
+
else if (similarity.score >= PARAPHRASE_REVIEW_THRESHOLD) candidateReasons.push('possible-paraphrase-review-required');
|
|
383
|
+
|
|
384
|
+
if (auth.effectiveDisposition === 'propose' || candidateReasons.some(reason => /review-required$/.test(reason))) {
|
|
385
|
+
if (auth.effectiveDisposition === 'propose') candidateReasons.push('workflow-proposal-only');
|
|
386
|
+
const candidate = candidateRecord(auth, ruleText, provenance, screening, Math.max(qualityScore, baseQualityScore), similarity, candidateReasons, scope);
|
|
387
|
+
const candidateFile = writeCandidate(auth, candidate);
|
|
177
388
|
return {
|
|
178
|
-
ok:
|
|
179
|
-
exitCode:
|
|
389
|
+
ok: true,
|
|
390
|
+
exitCode: 0,
|
|
391
|
+
persisted: false,
|
|
392
|
+
disposition: 'proposed',
|
|
393
|
+
candidateFile,
|
|
394
|
+
qualityScore: candidate.qualityScore,
|
|
395
|
+
source: provenance,
|
|
396
|
+
contentSha256: candidate.contentSha256,
|
|
180
397
|
screening,
|
|
181
|
-
|
|
182
|
-
message: 'Rule rejected - similar rule already exists',
|
|
398
|
+
reasonCodes: candidate.reasonCodes,
|
|
183
399
|
};
|
|
184
400
|
}
|
|
185
401
|
|
|
186
|
-
|
|
187
|
-
|
|
402
|
+
if (auth.effectiveDisposition !== 'persist-via-self-evolve') {
|
|
403
|
+
return { ok: false, exitCode: 3, screening, message: `unsupported memory.write disposition: ${auth.effectiveDisposition}` };
|
|
404
|
+
}
|
|
405
|
+
|
|
188
406
|
const hash = contentHash(ruleText);
|
|
407
|
+
const now = new Date().toISOString();
|
|
408
|
+
const record = {
|
|
409
|
+
schemaVersion: 1,
|
|
410
|
+
id: `mem-${hash.slice(0, 20)}`,
|
|
411
|
+
contentSha256: hash,
|
|
412
|
+
ruleText,
|
|
413
|
+
source: provenance,
|
|
414
|
+
status: 'active',
|
|
415
|
+
createdAt: now,
|
|
416
|
+
reviewedAt: now,
|
|
417
|
+
validUntil: resolvedValidUntil,
|
|
418
|
+
workspaceKey: typeof getWorkspaceKey === 'function' ? getWorkspaceKey(currentDir) : null,
|
|
419
|
+
writer: {
|
|
420
|
+
sessionId: auth.sessionId,
|
|
421
|
+
workflowId: auth.workflowId,
|
|
422
|
+
runId: auth.runId,
|
|
423
|
+
role: auth.writerRole,
|
|
424
|
+
disposition: auth.disposition,
|
|
425
|
+
},
|
|
426
|
+
screening: { secret: 'clear', promptInjection: 'clear' },
|
|
427
|
+
qualityScore,
|
|
428
|
+
scope,
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
432
|
+
const index = loadMemoryIndex(indexFile);
|
|
433
|
+
if (index.records.some(item => item.contentSha256 === hash && item.status !== 'superseded')) {
|
|
434
|
+
const candidate = candidateRecord(auth, ruleText, provenance, screening, Math.max(qualityScore, baseQualityScore), { score: 1, text: ruleText }, ['indexed-duplicate-review-required'], scope);
|
|
435
|
+
const candidateFile = writeCandidate(auth, candidate);
|
|
436
|
+
return { ok: true, exitCode: 0, persisted: false, disposition: 'proposed', candidateFile, qualityScore, source: provenance, contentSha256: hash, screening, reasonCodes: candidate.reasonCodes };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const timestamp = now.slice(0, 10);
|
|
189
440
|
const formattedMemory = [
|
|
190
441
|
'',
|
|
191
442
|
`## [${timestamp}] Self-Evolution Insight (Quality: ${qualityScore}/10)`,
|
|
443
|
+
`Memory-ID: ${record.id}`,
|
|
192
444
|
`Source: ${provenance}`,
|
|
193
445
|
`Content-SHA256: ${hash}`,
|
|
446
|
+
`Retention: ${resolvedValidUntil ? `valid-until=${resolvedValidUntil}` : 'no-explicit-expiry'}`,
|
|
194
447
|
'Screening: secret=clear; prompt-injection=clear',
|
|
195
448
|
`- ${ruleText}`,
|
|
196
449
|
'',
|
|
197
450
|
].join('\n');
|
|
198
|
-
|
|
451
|
+
|
|
452
|
+
const previousRules = fs.existsSync(rulesFile) ? fs.readFileSync(rulesFile, 'utf8') : '';
|
|
453
|
+
const nextIndex = { ...index, updatedAt: now, records: [...index.records, record] };
|
|
454
|
+
try {
|
|
455
|
+
atomicWriteText(rulesFile, previousRules + formattedMemory);
|
|
456
|
+
atomicWriteJson(indexFile, nextIndex);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
try { atomicWriteText(rulesFile, previousRules); } catch (_) { /* best-effort rollback */ }
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
461
|
+
consumeAuthorization(auth, 'persisted');
|
|
462
|
+
|
|
199
463
|
return {
|
|
200
464
|
ok: true,
|
|
201
465
|
exitCode: 0,
|
|
466
|
+
persisted: true,
|
|
467
|
+
disposition: 'persisted',
|
|
202
468
|
rulesFile,
|
|
469
|
+
indexFile,
|
|
470
|
+
memoryId: record.id,
|
|
203
471
|
qualityScore,
|
|
204
472
|
source: provenance,
|
|
205
473
|
contentSha256: hash,
|
|
206
474
|
screening,
|
|
475
|
+
validUntil: resolvedValidUntil,
|
|
207
476
|
};
|
|
208
477
|
}
|
|
209
|
-
|
|
210
478
|
function printHelp() {
|
|
211
|
-
console.log(`
|
|
479
|
+
console.log(`Persist or propose a defensive project-memory rule under the active Harness workflow contract.
|
|
212
480
|
|
|
213
|
-
Usage:
|
|
481
|
+
Usage:
|
|
482
|
+
node persist-memory.js "<insight>" --authorization <single-use-capability> [options]
|
|
214
483
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
484
|
+
Options:
|
|
485
|
+
--source <provenance>
|
|
486
|
+
--valid-until <ISO-8601> | --retention-days <1..3650>
|
|
487
|
+
--scope-task <task context>
|
|
488
|
+
--scope-requirement <requirement ids/text>
|
|
489
|
+
--scope-role <comma-separated roles>
|
|
218
490
|
|
|
219
|
-
|
|
220
|
-
-
|
|
221
|
-
-
|
|
222
|
-
-
|
|
223
|
-
-
|
|
224
|
-
|
|
491
|
+
Authorization behavior:
|
|
492
|
+
- memory.write=none: reject
|
|
493
|
+
- memory.write=propose: write only a session-scoped review candidate
|
|
494
|
+
- memory.write=persist-via-self-evolve: persist after screening/quality/dedup checks
|
|
495
|
+
- active Fable runs remain candidate-only so workers cannot directly mutate durable memory
|
|
496
|
+
- the opaque capability is issued by the router and bound to one workflow/session; --role/--session-id self-claims are not accepted
|
|
225
497
|
|
|
498
|
+
Durable metadata is stored in memories/repo/memory-index.json. Expiry never deletes the rule; scoped retrieval excludes stale/expired/unrelated records and treats retrieved memory as untrusted data.`);
|
|
499
|
+
}
|
|
226
500
|
if (require.main === module) {
|
|
227
501
|
let parsed;
|
|
228
502
|
try {
|
|
@@ -237,7 +511,7 @@ if (require.main === module) {
|
|
|
237
511
|
process.exit(0);
|
|
238
512
|
}
|
|
239
513
|
if (!parsed.memoryText) {
|
|
240
|
-
console.error('Usage: node persist-memory.js "<insight to remember>"
|
|
514
|
+
console.error('Usage: node persist-memory.js "<insight to remember>" --authorization <single-use-capability> [options]');
|
|
241
515
|
process.exit(1);
|
|
242
516
|
}
|
|
243
517
|
|
|
@@ -254,7 +528,8 @@ if (require.main === module) {
|
|
|
254
528
|
}
|
|
255
529
|
process.exit(result.exitCode);
|
|
256
530
|
}
|
|
257
|
-
console.log(`[Success] Memory persisted to ${result.rulesFile} (Quality: ${result.qualityScore}/10, Source: ${result.source})`);
|
|
531
|
+
if (result.persisted) console.log(`[Success] Memory persisted to ${result.rulesFile} (Quality: ${result.qualityScore}/10, Source: ${result.source})`);
|
|
532
|
+
else console.log(`[Candidate] Memory proposal written to ${result.candidateFile} (${(result.reasonCodes || []).join(', ') || 'proposal-only'})`);
|
|
258
533
|
} catch (err) {
|
|
259
534
|
console.error(`[Error] Failed to write memory: ${err.message}`);
|
|
260
535
|
process.exit(1);
|
|
@@ -269,4 +544,8 @@ module.exports = {
|
|
|
269
544
|
persistMemory,
|
|
270
545
|
scoreRuleQuality,
|
|
271
546
|
screenMemoryForPersistence,
|
|
547
|
+
resolveMemoryAuthorization,
|
|
548
|
+
normalizeTerms,
|
|
549
|
+
loadMemoryIndex,
|
|
550
|
+
MEMORY_INDEX_SCHEMA_VERSION,
|
|
272
551
|
};
|
|
@@ -87,6 +87,7 @@ runNode('routing matrix', path.join(projectRoot, 'ci', 'runner.js'));
|
|
|
87
87
|
runNode('skill route coverage', path.join(projectRoot, 'ci', 'skill-routing-check.js'));
|
|
88
88
|
runNode('invariant-first routing contract', path.join(projectRoot, 'ci', 'invariant-routing.test.js'));
|
|
89
89
|
runNode('workflow-plan structured contract', path.join(projectRoot, 'ci', 'router-workflow-plan.test.js'));
|
|
90
|
+
runNode('workflow execution budget contract', path.join(projectRoot, 'ci', 'workflow-execution-contract.test.js'));
|
|
90
91
|
runNode('explicit parallel safety contract', path.join(projectRoot, 'ci', 'router-phase2-safety.test.js'));
|
|
91
92
|
runNode('workflow-plan consumers', path.join(projectRoot, 'ci', 'workflow-plan-consumers.test.js'));
|
|
92
93
|
runNode('workflow-plan consumer edge contracts', path.join(projectRoot, 'ci', 'workflow-plan-consumers-edge.test.js'));
|