harness-everything 0.8.0 → 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.
Files changed (102) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/ci.yml +12 -2
  4. package/AGENTS.md +36 -35
  5. package/CHANGELOG.md +51 -0
  6. package/README.md +2 -2
  7. package/benchmarks/results/reviews/pr142-verification.json +95 -0
  8. package/ci/action-gate.test.js +83 -3
  9. package/ci/doc-capability-consistency.test.js +3 -0
  10. package/ci/invariant-routing.test.js +87 -74
  11. package/ci/mechanism-2af-worktree-isolation.test.js +152 -0
  12. package/ci/mechanism-2ag-router-worktree-contract.test.js +44 -0
  13. package/ci/mechanism-2ah-workflow-lifecycle.test.js +140 -0
  14. package/ci/openai-plugin-package.test.js +9 -6
  15. package/ci/phase4-ensemble-memory.test.js +128 -10
  16. package/ci/router-workflow-plan.test.js +23 -0
  17. package/ci/workflow-execution-contract.test.js +133 -0
  18. package/ci/workflow-plan-consumers.test.js +18 -0
  19. package/docs/architecture.md +111 -114
  20. package/docs/mechanism-first-skill-mesh.md +92 -103
  21. package/docs/openai-plugin.md +1 -1
  22. package/docs/philosophy.md +40 -33
  23. package/docs/platform-capabilities.md +2 -2
  24. package/docs/reviews/pr-142-issue-131.md +89 -0
  25. package/docs/routing.md +61 -124
  26. package/docs/workflow-runtime.md +109 -0
  27. package/fable-mode/CONTRACT-FORMAT.md +3 -1
  28. package/fable-mode/SKILL.md +1 -1
  29. package/fable-mode/agents/fable-orchestrator.md +9 -7
  30. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  31. package/fable-mode/fable-haiku/SKILL.md +1 -1
  32. package/fable-mode/fable-opus/SKILL.md +1 -1
  33. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  34. package/fable-mode/scripts/workflow-plan-consumer.js +13 -8
  35. package/harness-everything/SKILL.md +11 -10
  36. package/harness-everything/references/triage-and-tiers.md +76 -107
  37. package/harness-everything/schemas/router-task-shape.schema.json +4 -2
  38. package/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
  39. package/harness-everything/scripts/kernel-router-core.js +265 -0
  40. package/harness-everything/scripts/kernel-router.js +118 -223
  41. package/harness-everything/scripts/router-contract-core.js +633 -0
  42. package/harness-everything/scripts/router-contract.js +21 -581
  43. package/harness-everything/scripts/tier-router.js +19 -1
  44. package/hooks/hooks.json +25 -4
  45. package/hooks/scripts/action-gate.js +111 -2
  46. package/hooks/scripts/contract-test.js +27 -2
  47. package/hooks/scripts/lib/workflow-isolation.js +102 -0
  48. package/hooks/scripts/lib/workflow-runtime.js +254 -0
  49. package/hooks/scripts/state-persist.js +1 -0
  50. package/hooks/scripts/subagent-scope-guard.js +20 -3
  51. package/hooks/scripts/workflow-disposition.js +104 -0
  52. package/hooks/scripts/workflow-gate.js +56 -0
  53. package/hooks/scripts/workflow-stop-gate.js +44 -0
  54. package/multi-agent-workspace/SKILL.md +2 -2
  55. package/multi-agent-workspace/scripts/index_memory.js +107 -1
  56. package/opencode-plugin/plugin.json +1 -1
  57. package/package.json +2 -2
  58. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  59. package/plugins/harness-everything/hooks/hooks.json +44 -1
  60. package/plugins/harness-everything/hooks/scripts/action-gate.js +111 -2
  61. package/plugins/harness-everything/hooks/scripts/contract-test.js +27 -2
  62. package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +102 -0
  63. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +254 -0
  64. package/plugins/harness-everything/hooks/scripts/state-persist.js +1 -0
  65. package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +20 -3
  66. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +104 -0
  67. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +56 -0
  68. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +44 -0
  69. package/plugins/harness-everything/plugin.json +1 -1
  70. package/plugins/harness-everything/scripts/lib/advisory-text.js +51 -54
  71. package/plugins/harness-everything/skills/fable-mode/CONTRACT-FORMAT.md +3 -1
  72. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  73. package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +9 -7
  74. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  75. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  76. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  77. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  78. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +13 -8
  79. package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -10
  80. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +76 -107
  81. package/plugins/harness-everything/skills/harness-everything/schemas/router-task-shape.schema.json +4 -2
  82. package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
  83. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +265 -0
  84. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +118 -223
  85. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +633 -0
  86. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +21 -581
  87. package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +19 -1
  88. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +2 -2
  89. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +107 -1
  90. package/plugins/harness-everything/skills/self-evolve/SKILL.md +6 -6
  91. package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +22 -33
  92. package/plugins/harness-everything/skills/self-evolve/scripts/persist-memory.js +336 -57
  93. package/plugins/harness-everything/skills/self-evolve/scripts/self-regression.js +1 -0
  94. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +14 -16
  95. package/plugins/harness-everything/skills/using-git-worktrees/references/workflow-details.md +44 -85
  96. package/scripts/lib/advisory-text.js +51 -54
  97. package/self-evolve/SKILL.md +6 -6
  98. package/self-evolve/references/memory-resolution.md +22 -33
  99. package/self-evolve/scripts/persist-memory.js +336 -57
  100. package/self-evolve/scripts/self-regression.js +1 -0
  101. package/using-git-worktrees/SKILL.md +14 -16
  102. package/using-git-worktrees/references/workflow-details.md +44 -85
@@ -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
- let memoryText = null;
39
- let source = 'manual-unspecified';
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') return { help: true, memoryText: null, source };
43
- if (token === '--source') {
44
- if (index + 1 >= argv.length) throw new Error('--source requires a value');
45
- source = argv[++index];
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
- if (token.startsWith('--source=')) {
49
- source = token.slice('--source='.length);
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
- return { help: false, memoryText, source };
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
- function persistMemory({ memoryText, source = 'manual-unspecified' }) {
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
- ok: false,
155
- exitCode: 1,
156
- screening,
157
- message: 'Cannot persist project memory without a resolved git workspace.',
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
- if (qualityScore < 5) {
166
- return {
167
- ok: false,
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 isDuplicate = existingRules.some(existing => calculateSimilarity(ruleText, existing) > 0.7);
176
- if (isDuplicate) {
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: false,
179
- exitCode: 1,
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
- qualityScore,
182
- message: 'Rule rejected - similar rule already exists',
398
+ reasonCodes: candidate.reasonCodes,
183
399
  };
184
400
  }
185
401
 
186
- fs.mkdirSync(memoryDir, { recursive: true });
187
- const timestamp = new Date().toISOString().split('T')[0];
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
- fs.appendFileSync(rulesFile, formattedMemory, 'utf8');
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(`Append a short defensive rule to memories/repo/RULES.md.
479
+ console.log(`Persist or propose a defensive project-memory rule under the active Harness workflow contract.
212
480
 
213
- Usage: node persist-memory.js "<insight to remember>" [--source <provenance>]
481
+ Usage:
482
+ node persist-memory.js "<insight>" --authorization <single-use-capability> [options]
214
483
 
215
- The rule must be universal and actionable. --source records where the lesson came
216
- from (for example issue-85, verifier-stage-3, or manual-review); when omitted the
217
- source is recorded as manual-unspecified.
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
- Persistence gates:
220
- - secret-like tokens and credential material are rejected before any write
221
- - prompt-injection-shaped instructions are rejected before any write
222
- - source provenance is recorded with a SHA-256 content fingerprint
223
- - minimum quality score of 5/10 and similarity deduplication still apply`);
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>" [--source <provenance>]');
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'));
@@ -4,46 +4,44 @@ description: Use when starting feature work needing isolation or before implemen
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.4.0
7
+ version: 0.8.1
8
8
  ---
9
9
 
10
10
  # Using Git Worktrees
11
11
 
12
- Use native tools first; git fallback.
12
+ Use native worktree support first; raw Git is the fallback.
13
13
 
14
- ## 📋 Skill Contract
14
+ ## Contract
15
15
 
16
- | Component | Specification |
17
- | :--- | :--- |
18
- | **Trigger / Input** | Isolation-needing feature work; pre-plan setup. |
19
- | **Expected Output** | Isolated workspace plus clean test baseline. |
20
- | **State Mutations** | Creates `.worktrees/<branch>`; updates `.gitignore` if needed. |
21
- | **Enforcement Gate** | Detect existing isolation first; seamless sandbox fallback. |
16
+ - **Major workflow mode** — Tier 3 / Fable-class engineering must be isolated before source/artifact mutation. Reuse an existing linked worktree or create one. If isolation cannot be established, stop `BLOCKED`; never fall back to the primary working tree.
17
+ - **Ordinary mode** — isolation-needing feature work may honor an explicit user preference to stay in place.
18
+ - Never nest a worktree inside an already isolated worktree.
22
19
 
23
20
  ## Workflow
24
21
 
25
- **Step 0 — Detect existing isolation** (adapt per `environment-detection`):
22
+ **Step 0 — Detect isolation** (adapt per `environment-detection`):
26
23
  ```bash
27
24
  git rev-parse --git-dir
28
25
  git rev-parse --git-common-dir
29
26
  git branch --show-current
30
27
  git rev-parse --show-superproject-working-tree
31
28
  ```
32
- Dirs differ and no superproject → already isolated; skip to Step 2; never nest. Superproject → submodule (normal repo). Else honor declared preference or ask consent; declined → in place.
29
+ Different git/common dirs and no superproject => already isolated; continue to Step 2. A submodule is a normal repo for this check.
33
30
 
34
- **Step 1 — Create**: native tool (`EnterWorktree`, `/worktree`, `--worktree`) first — raw git creates phantom state. Else verify ignored via `git check-ignore -q .worktrees || git check-ignore -q worktrees`; unignored → add to .gitignore and commit. Then `git worktree add "$path" -b "$BRANCH_NAME"` at declared preference, existing `.worktrees/`/`worktrees/`, or default `.worktrees/`. Sandbox denial → work in place.
31
+ **Step 1 — Create/enter**: prefer native `EnterWorktree`, `/worktree`, or `--worktree`. Otherwise use `git worktree add "$path" -b "$BRANCH_NAME"`. For major mode, choose an already-ignored or external/sibling path so setup does not require modifying the primary tree first. Creation/entry failure => `BLOCKED`. Only ordinary mode may continue in place after an explicit decline/unsupported environment.
35
32
 
36
- **Step 2 — Setup**: npm/cargo/pip/poetry/go.
33
+ **Step 2 — Setup**: install project dependencies as needed.
37
34
 
38
- **Step 3 — Baseline**: run tests (`npm test`/`cargo test`/`pytest`/`go test ./...`); failures → report, ask.
35
+ **Step 3 — Baseline**: run the relevant tests/build in the isolated tree before implementation; existing failures must be surfaced before mutation proceeds.
39
36
 
40
37
  Deep dive: <this-skill-dir>/references/workflow-details.md
41
38
 
42
39
  ## USE FOR:
40
+ - Tier 3 / Fable-class mutable engineering
43
41
  - feature work needing isolation
44
42
  - pre-implementation-plan setup
45
- - native vs manual `git worktree add`
46
43
 
47
44
  ## DO NOT USE FOR:
48
45
  - nesting inside an existing worktree
49
- - branch/merge workflows
46
+ - read-only analysis with no mutation
47
+ - branch/merge workflows unrelated to workspace isolation