harness-everything 0.11.7 → 0.12.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.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +53 -0
  4. package/README.md +1 -0
  5. package/behavioral-evals/README.md +37 -0
  6. package/behavioral-evals/cases/self-evolve-retrieval-recurrence.yaml +48 -0
  7. package/behavioral-evals/paired-benchmark.js +211 -5
  8. package/ci/contract-integrity.test.js +68 -2
  9. package/ci/mechanism-2ah-workflow-lifecycle.test.js +258 -2
  10. package/ci/mechanism-32-paired-benchmark.test.js +139 -0
  11. package/ci/openai-plugin-package.test.js +17 -0
  12. package/ci/self-evolve-loop.test.js +37 -2
  13. package/ci/telemetry.test.js +31 -1
  14. package/docs/enforcement-locks.md +114 -0
  15. package/grill-with-docs/ADR-FORMAT.md +22 -0
  16. package/grill-with-docs/SKILL.md +1 -1
  17. package/hooks/scripts/lib/learning-opportunity.js +22 -0
  18. package/hooks/scripts/lib/telemetry.js +40 -0
  19. package/hooks/scripts/lib/workflow-isolation.js +10 -2
  20. package/hooks/scripts/lib/workflow-runtime.js +108 -5
  21. package/hooks/scripts/state-persist.js +9 -3
  22. package/hooks/scripts/workflow-disposition.js +19 -8
  23. package/hooks/scripts/workflow-gate.js +33 -4
  24. package/multi-agent-workspace/SKILL.md +1 -1
  25. package/multi-agent-workspace/scripts/index_memory.js +28 -0
  26. package/opencode-plugin/plugin.json +1 -1
  27. package/package.json +1 -1
  28. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  29. package/plugins/harness-everything/contract-integrity/scripts/audit.js +536 -0
  30. package/plugins/harness-everything/hooks/scripts/lib/learning-opportunity.js +22 -0
  31. package/plugins/harness-everything/hooks/scripts/lib/telemetry.js +40 -0
  32. package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +10 -2
  33. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +108 -5
  34. package/plugins/harness-everything/hooks/scripts/state-persist.js +9 -3
  35. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +19 -8
  36. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +33 -4
  37. package/plugins/harness-everything/plugin.json +1 -1
  38. package/plugins/harness-everything/skills/grill-with-docs/ADR-FORMAT.md +22 -0
  39. package/plugins/harness-everything/skills/grill-with-docs/SKILL.md +1 -1
  40. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
  41. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +28 -0
  42. package/plugins/harness-everything/skills/self-evolve/SKILL.md +1 -1
  43. package/plugins/harness-everything/skills/self-evolve/scripts/lesson-candidate.js +52 -0
  44. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  45. package/plugins/harness-everything/skills/to-spec/references/process.md +26 -0
  46. package/plugins/harness-everything/skills/to-spec/templates/cli-reference.md +14 -0
  47. package/plugins/harness-everything/skills/to-spec/templates/dev-doc.md +14 -0
  48. package/plugins/harness-everything/skills/to-spec/templates/feature-spec.md +14 -0
  49. package/plugins/harness-everything/skills/to-spec/templates/schema-doc.md +14 -0
  50. package/plugins/harness-everything/skills/to-tickets/SKILL.md +1 -1
  51. package/plugins/harness-everything/skills/to-tickets/references/publishing-and-templates.md +20 -1
  52. package/plugins/harness-everything/skills/verification-loop/SKILL.md +3 -2
  53. package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md +25 -1
  54. package/plugins/harness-everything/skills/verification-loop/scripts/contract-integrity-audit.js +31 -0
  55. package/plugins/harness-everything/skills/verification-loop/templates/verification-report.template.md +3 -0
  56. package/scripts/sync-openai-plugin.js +12 -0
  57. package/self-evolve/SKILL.md +1 -1
  58. package/self-evolve/scripts/lesson-candidate.js +52 -0
  59. package/telemetry/schemas/event.schema.json +141 -22
  60. package/telemetry/scripts/report.js +42 -0
  61. package/to-spec/SKILL.md +1 -1
  62. package/to-spec/references/process.md +26 -0
  63. package/to-spec/templates/cli-reference.md +14 -0
  64. package/to-spec/templates/dev-doc.md +14 -0
  65. package/to-spec/templates/feature-spec.md +14 -0
  66. package/to-spec/templates/schema-doc.md +14 -0
  67. package/to-tickets/SKILL.md +1 -1
  68. package/to-tickets/references/publishing-and-templates.md +20 -1
  69. package/verification-loop/SKILL.md +3 -2
  70. package/verification-loop/references/verification-phases.md +25 -1
  71. package/verification-loop/scripts/contract-integrity-audit.js +31 -0
  72. package/verification-loop/templates/verification-report.template.md +3 -0
@@ -17,6 +17,7 @@ const env = { ...process.env, HARNESS_STATE_HOME: path.join(temp, 'state'), HARN
17
17
  process.env.HARNESS_STATE_HOME = env.HARNESS_STATE_HOME;
18
18
  const state = require(path.join(runtimeRoot, 'hooks/scripts/lib/harness-state'));
19
19
  const workflowRuntime = require(path.join(runtimeRoot, 'hooks/scripts/lib/workflow-runtime'));
20
+ const workflowIsolation = require(path.join(runtimeRoot, 'hooks/scripts/lib/workflow-isolation'));
20
21
  const sessionId = 'workflow-lifecycle';
21
22
  const payload = { session_id: sessionId, cwd: repo };
22
23
  const sessionDir = state.getSessionDir(repo, sessionId);
@@ -44,9 +45,15 @@ const persistDirect = (tool, input, cwd = repo, response = {}) => node('hooks/sc
44
45
  tool_response: response,
45
46
  });
46
47
  let toolUseSeq = 0;
47
- function beginShell(command, cwd = repo) {
48
+ function beginShell(command, cwd = repo, extraToolInput = {}) {
48
49
  const toolUseId = 'toolu_fixture_' + (++toolUseSeq);
49
- const input = { ...payload, cwd, tool_name: 'Bash', tool_use_id: toolUseId, tool_input: { command } };
50
+ const input = {
51
+ ...payload,
52
+ cwd,
53
+ tool_name: 'Bash',
54
+ tool_use_id: toolUseId,
55
+ tool_input: { command, ...extraToolInput },
56
+ };
50
57
  return { command, cwd, toolUseId, result: node('hooks/scripts/workflow-gate.js', input) };
51
58
  }
52
59
  function persistShell(call, response = {}, hookEvent = 'PostToolUse') {
@@ -83,8 +90,37 @@ const probeFiles = () => {
83
90
  try { return fs.readdirSync(probeDir).filter(name => /^[a-f0-9]{64}\.json$/.test(name)); }
84
91
  catch (_) { return []; }
85
92
  };
93
+ function ageProbeFiles(ms = 60000) {
94
+ for (const name of probeFiles()) {
95
+ const target = path.join(probeDir, name);
96
+ const probe = read(target);
97
+ write(target, { ...probe, observedAt: Date.now() - ms, reclaimAfterAt: Date.now() - 1 });
98
+ }
99
+ }
86
100
  const stop = extra => node('hooks/scripts/workflow-stop-gate.js', { ...payload, ...extra });
87
101
  const control = (...args) => node('hooks/scripts/workflow-disposition.js', null, [args[0], '--session-id', sessionId, ...args.slice(1)]);
102
+ function fixtureSession(fixtureSessionId) {
103
+ const fixturePayload = { session_id: fixtureSessionId, cwd: repo };
104
+ const fixtureSessionDir = state.getSessionDir(repo, fixtureSessionId);
105
+ const fixtureFile = path.join(fixtureSessionDir, 'workflow-run.json');
106
+ return {
107
+ payload: fixturePayload,
108
+ file: fixtureFile,
109
+ sessionDir: fixtureSessionDir,
110
+ route: prompt => node('harness-everything/scripts/kernel-router.js', { ...fixturePayload, prompt }),
111
+ gate: (tool, input, extra = {}) => node('hooks/scripts/workflow-gate.js', { ...fixturePayload, ...extra, tool_name: tool, tool_input: input }),
112
+ persist: (tool, input, response = {}, extra = {}) => node('hooks/scripts/state-persist.js', {
113
+ ...fixturePayload,
114
+ ...extra,
115
+ hook_event_name: 'PostToolUse',
116
+ tool_name: tool,
117
+ tool_input: input,
118
+ tool_response: response,
119
+ }),
120
+ stop: extra => node('hooks/scripts/workflow-stop-gate.js', { ...fixturePayload, ...extra }),
121
+ control: (...args) => node('hooks/scripts/workflow-disposition.js', null, [args[0], '--session-id', fixtureSessionId, ...args.slice(1)]),
122
+ };
123
+ }
88
124
  const stages = [
89
125
  { stageId: 'build', goal: 'fix', agent: 'fable-worker', task: 'fix bounded module', dependsOn: [], writeSet: ['src'], checkCommand: 'node test-build.js', passCondition: 'exit 0' },
90
126
  { stageId: 'optional', goal: 'external scope', agent: 'fable-worker', task: 'external task', dependsOn: [], writeSet: [], checkCommand: 'node external.js', passCondition: 'exit 0' },
@@ -92,6 +128,78 @@ const stages = [
92
128
  ];
93
129
 
94
130
  try {
131
+ // #162: Tier-2 shell accounting must degrade conservatively when a workspace
132
+ // is not a Git repository. Lack of a Git fingerprint is not an isolation
133
+ // violation for a non-major workflow.
134
+ const nonGit = fixtureSession('workflow-non-git');
135
+ check(nonGit.route('Fix this checkout bug with a regression test').status === 0 &&
136
+ read(nonGit.file).strategy === 'iterative-single',
137
+ '#162 non-git fixture routes to Tier 2 iterative workflow');
138
+ check(nonGit.control('start').status === 0 && read(nonGit.file).state === 'running',
139
+ '#162 non-git Tier 2 workflow starts normally');
140
+ let nonGitIterations = read(nonGit.file).budget?.counters?.iterations || 0;
141
+ for (const [index, command] of ['npm --version', 'node -e "process.exit(0)"', 'cd .', 'git init -q'].entries()) {
142
+ const toolUseId = 'toolu_non_git_pre_' + index;
143
+ const admitted = nonGit.gate('Bash', { command }, { tool_use_id: toolUseId });
144
+ check(admitted.status === 0, '#162 non-git Tier 2 admits untrusted shell: ' + command);
145
+ const afterGate = read(nonGit.file);
146
+ check(afterGate.budget.counters.iterations === ++nonGitIterations &&
147
+ afterGate.lastMutationAt > 0 &&
148
+ afterGate.budget.events.at(-1)?.evidence === 'shell:unobservable-workspace',
149
+ '#162 unobservable non-git shell is conservatively counted once: ' + command);
150
+ check(nonGit.persist('Bash', { command }, { stdout: 'fixture success' }, { tool_use_id: toolUseId }).status === 0,
151
+ '#162 non-git shell PostToolUse stays fail-open without a probe: ' + command);
152
+ }
153
+
154
+ // A probe may have been created while Git was observable and become
155
+ // unobservable before PostToolUse (for example .git disappears). Tier 2
156
+ // must settle it conservatively instead of blocking the workflow.
157
+ const lostObservationId = 'toolu_non_git_post_fallback';
158
+ const lostObservationPayload = {
159
+ ...nonGit.payload,
160
+ tool_name: 'Bash',
161
+ tool_use_id: lostObservationId,
162
+ tool_input: { command: 'node lost-observation.js' },
163
+ };
164
+ const lostObservationContext = workflowRuntime.loadWorkflow(lostObservationPayload);
165
+ const lostObservationKey = workflowIsolation.shellProbeKey(lostObservationPayload, repo);
166
+ workflowRuntime.registerMutationProbe(lostObservationContext, lostObservationKey, 'f'.repeat(64), true);
167
+ const beforeLostObservation = read(nonGit.file);
168
+ check(nonGit.persist('Bash', { command: 'node lost-observation.js' }, { stdout: 'fixture success' },
169
+ { tool_use_id: lostObservationId }).status === 0,
170
+ '#162 Tier 2 PostToolUse degrades when workspace observation becomes unavailable');
171
+ const afterLostObservation = read(nonGit.file);
172
+ check(afterLostObservation.state === 'running' &&
173
+ afterLostObservation.budget.counters.iterations === beforeLostObservation.budget.counters.iterations + 1 &&
174
+ afterLostObservation.lastMutationAt >= (beforeLostObservation.lastMutationAt || 0),
175
+ '#162 lost post-observation is settled as one conservative mutation without blocking');
176
+
177
+ const nonGitTarget = path.join(repo, 'non-git-edit.txt');
178
+ check(nonGit.gate('Write', { file_path: nonGitTarget }).status === 0,
179
+ '#162 direct edit remains available in non-git Tier 2');
180
+ fs.writeFileSync(nonGitTarget, 'changed\n');
181
+ check(nonGit.persist('Write', { file_path: nonGitTarget }).status === 0,
182
+ '#162 direct non-git edit persists mutation milestone');
183
+ const verifyId = 'toolu_non_git_verify';
184
+ check(nonGit.gate('Bash', { command: 'node run-test.js' }, { tool_use_id: verifyId }).status === 0,
185
+ '#162 verification shell is admitted in non-git Tier 2');
186
+ check(nonGit.persist('Bash', { command: 'node run-test.js' }, { stdout: 'tests passed' },
187
+ { tool_use_id: verifyId }).status === 0,
188
+ '#162 non-git verification result is persisted');
189
+ check(nonGit.stop().status === 0 && read(nonGit.file).state === 'satisfied',
190
+ '#162 edit then verification can satisfy Stop in non-git Tier 2');
191
+
192
+ const nonGitMajor = fixtureSession('workflow-non-git-major');
193
+ check(nonGitMajor.route('Refactor the entire authentication architecture across all services without fable').status === 0 &&
194
+ read(nonGitMajor.file).tier === 'tier3',
195
+ '#162 non-git major fixture routes to Tier 3');
196
+ check(nonGitMajor.control('start').status === 0 && read(nonGitMajor.file).state === 'running',
197
+ '#162 non-Fable Tier 3 controller can enter running state before mutation');
198
+ const majorBlocked = nonGitMajor.gate('Bash', { command: 'node mutate.js' }, { tool_use_id: 'toolu_non_git_major' });
199
+ check(majorBlocked.status === 2 && /Git worktree isolation/i.test(majorBlocked.stderr),
200
+ '#162 non-git Tier 3 remains fail-closed with an isolation-specific error');
201
+
202
+ fs.unlinkSync(nonGitTarget);
95
203
  git(['init']); fs.writeFileSync(path.join(repo, 'README.md'), 'fixture\n'); git(['add', '.']);
96
204
  git(['-c', 'user.name=Harness Test', '-c', 'user.email=harness@example.invalid', 'commit', '-m', 'fixture']);
97
205
  git(['worktree', 'add', linked, '-b', 'isolated']);
@@ -225,6 +333,48 @@ try {
225
333
  promotedTier3.mutationIsolation?.required === true && !promotedTier3.pendingPlan, 'pending Tier 3 isolation becomes active atomically on start');
226
334
  check(workflowRuntime.isMajorWorkflow(promotedTier3), 'major-workflow classification flips only after explicit activation');
227
335
  check(gate('Write', { file_path: path.join(repo, 'premature-primary.js') }, repo).status === 2, 'activated Tier 3 plan still blocks primary-tree mutation');
336
+
337
+ // #163: a queued Tier-3 plan can intentionally remain single-agent when
338
+ // Fable is prohibited. Explicit start must still promote the whole plan,
339
+ // including isolation and budget metadata, before any mutation is admitted.
340
+ fs.unlinkSync(file);
341
+ check(route('Fix this checkout bug with a regression test').status === 0 && read(file).strategy === 'iterative-single',
342
+ '#163 fixture starts from a Tier 2 iterative workflow');
343
+ check(control('start').status === 0 && read(file).state === 'running',
344
+ '#163 fixture activates Tier 2 before a stronger route arrives');
345
+ check(route('Refactor the entire authentication architecture across all services and migrate the database schema without fable').status === 0,
346
+ '#163 stronger Tier 3 route without Fable is retained for explicit replan');
347
+ const queuedNonFableTier3 = read(file);
348
+ check(queuedNonFableTier3.state === 'blocked' &&
349
+ queuedNonFableTier3.pendingPlan?.tier === 'tier3' &&
350
+ queuedNonFableTier3.pendingPlan?.strategy === 'iterative-single' &&
351
+ queuedNonFableTier3.pendingPlan?.mutationIsolation?.required === true,
352
+ '#163 queued Tier 3 plan remains non-Fable but still requires isolation');
353
+ write(file, {
354
+ ...queuedNonFableTier3,
355
+ budget: {
356
+ ...queuedNonFableTier3.budget,
357
+ limits: { ...queuedNonFableTier3.budget.limits, maxIterations: 1 },
358
+ },
359
+ });
360
+ check(control('start').status === 0, '#163 explicit start activates queued non-Fable Tier 3 plan');
361
+ const promotedNonFableTier3 = read(file);
362
+ check(promotedNonFableTier3.workflowPlan?.tier === 'tier3' &&
363
+ promotedNonFableTier3.tier === 'tier3' &&
364
+ promotedNonFableTier3.strategy === 'iterative-single' &&
365
+ promotedNonFableTier3.mutationIsolation?.required === true &&
366
+ !promotedNonFableTier3.pendingPlan,
367
+ '#163 non-Fable Tier 3 plan replaces all authoritative active-plan fields');
368
+ check(promotedNonFableTier3.budget?.limits?.maxIterations === promotedNonFableTier3.workflowPlan?.limits?.maxIterations &&
369
+ promotedNonFableTier3.budget?.limits?.maxIterations !== 1,
370
+ '#163 plan activation re-derives budget limits from the promoted plan');
371
+ check(workflowRuntime.isMajorWorkflow(promotedNonFableTier3),
372
+ '#163 non-Fable Tier 3 promotion enables major-workflow isolation');
373
+ check(gate('Write', { file_path: path.join(repo, 'non-fable-primary.js') }, repo).status === 2,
374
+ '#163 promoted non-Fable Tier 3 blocks primary-tree mutation');
375
+ check(gate('Write', { file_path: path.join(linked, 'non-fable-linked.js') }, linked).status === 0,
376
+ '#163 promoted non-Fable Tier 3 admits linked-worktree mutation');
377
+
228
378
  fs.unlinkSync(file);
229
379
  check(route('Fix this checkout bug with a regression test').status === 0 && read(file).strategy === 'iterative-single', 'fresh bounded fix returns to Tier 2 iterative lifecycle');
230
380
  let tier2 = read(file);
@@ -387,6 +537,112 @@ try {
387
537
  'no-effect untrusted shell command does not advance lastMutationAt');
388
538
  }
389
539
 
540
+ // #161: Post payload cwd may differ from Pre payload cwd after a shell
541
+ // command changes directories. Stable tool_use_id identity plus the stored
542
+ // Pre-time observation cwd must settle the original probe.
543
+ const driftCwd = path.join(repo, 'hooks');
544
+ fs.mkdirSync(driftCwd, { recursive: true });
545
+ const driftNoEffectBefore = read(file);
546
+ const driftNoEffect = beginShell('cd hooks && node -e "process.exit(0)"', repo);
547
+ check(driftNoEffect.result.status === 0 && probeFiles().length === 1,
548
+ '#161 cwd-drift no-effect call registers one probe');
549
+ const driftNoEffectPost = node('hooks/scripts/state-persist.js', {
550
+ ...payload,
551
+ cwd: driftCwd,
552
+ hook_event_name: 'PostToolUse',
553
+ tool_name: 'Bash',
554
+ tool_use_id: driftNoEffect.toolUseId,
555
+ tool_input: { command: driftNoEffect.command },
556
+ tool_response: { stdout: 'ok' },
557
+ });
558
+ check(driftNoEffectPost.status === 0 && probeFiles().length === 0,
559
+ '#161 Post with drifted cwd settles the Pre-time probe');
560
+ check((read(file).budget?.counters?.iterations || 0) === (driftNoEffectBefore.budget?.counters?.iterations || 0),
561
+ '#161 cwd-drift no-effect shell consumes zero iterations');
562
+
563
+ const driftMutationBefore = read(file);
564
+ const driftMutation = beginShell('cd hooks && node mutate-from-subdir.js', repo);
565
+ check(driftMutation.result.status === 0, '#161 cwd-drift mutator is admitted under observation');
566
+ fs.writeFileSync(path.join(repo, 'drift-mutated.txt'), 'changed under cwd drift\n');
567
+ const driftMutationPost = node('hooks/scripts/state-persist.js', {
568
+ ...payload,
569
+ cwd: driftCwd,
570
+ hook_event_name: 'PostToolUse',
571
+ tool_name: 'Bash',
572
+ tool_use_id: driftMutation.toolUseId,
573
+ tool_input: { command: driftMutation.command },
574
+ tool_response: { stdout: 'changed' },
575
+ });
576
+ const driftMutationAfter = read(file);
577
+ check(driftMutationPost.status === 0 && probeFiles().length === 0 &&
578
+ (driftMutationAfter.budget?.counters?.iterations || 0) === (driftMutationBefore.budget?.counters?.iterations || 0) + 1 &&
579
+ driftMutationAfter.lastMutationAt >= (driftMutationBefore.lastMutationAt || 0),
580
+ '#161 cwd-drift real mutation is observed exactly once');
581
+
582
+ // A live command may legitimately run longer than the old 1s grace period.
583
+ // Its probe must remain reserved until the declared tool timeout expires.
584
+ const longRunningStateBefore = read(file);
585
+ const longRunningBefore = longRunningStateBefore.budget.counters.iterations;
586
+ const longRunningA = beginShell('node long-running-mutator.js', repo, { timeout: 60000 });
587
+ check(longRunningA.result.status === 0 && probeFiles().length === 1,
588
+ '#161 long-running call registers one timeout-bound probe');
589
+ {
590
+ const target = path.join(probeDir, probeFiles()[0]);
591
+ const probe = read(target);
592
+ write(target, { ...probe, observedAt: Date.now() - 2000 });
593
+ }
594
+ const concurrentB = beginShell('node concurrent-observer.js', repo, { timeout: 60000 });
595
+ check(concurrentB.result.status === 0 && probeFiles().length === 2,
596
+ '#161 a second Pre does not reclaim an in-flight probe merely because it is older than 1s');
597
+ fs.writeFileSync(path.join(repo, 'long-running-mutated.txt'), 'changed by long-running A\n');
598
+ check(persistShell(longRunningA).status === 0 &&
599
+ read(file).budget.counters.iterations === longRunningBefore + 1,
600
+ '#161 long-running A still settles and records its later mutation');
601
+ check(denyShell(concurrentB).status === 0 && probeFiles().length === 0,
602
+ '#161 concurrent non-mutating probe can be denied independently after A settles');
603
+ fs.rmSync(path.join(repo, 'long-running-mutated.txt'), { force: true });
604
+ write(file, longRunningStateBefore);
605
+ check(read(file).budget.counters.iterations === longRunningBefore,
606
+ '#161 long-running regression restores the shared fixture budget before later boundary tests');
607
+
608
+ // Simulate default-mode denial / sibling PreToolUse blocking: Pre fires but
609
+ // neither PostToolUse nor PermissionDenied follows. Force each orphan past
610
+ // its timeout-bound reclaim deadline so the next real Pre can reclaim it
611
+ // deterministically without sleeping in CI.
612
+ const orphanStartIterations = read(file).budget.counters.iterations;
613
+ for (let i = 0; i < iterationLimit; i++) {
614
+ const orphan = beginShell('node rejected-before-exec-' + i + '.js', repo);
615
+ check(orphan.result.status === 0, '#161 Pre-only orphan ' + (i + 1) + ' is initially admitted');
616
+ ageProbeFiles();
617
+ }
618
+ check(workflowRuntime.mutationProbeReservations(workflowRuntime.loadWorkflow(payload)) === 0,
619
+ '#161 stale Pre-only probes no longer reserve iteration capacity');
620
+ const harmlessAfterOrphans = beginShell('node harmless-after-orphans.js', repo);
621
+ check(harmlessAfterOrphans.result.status === 0 && probeFiles().length === 1,
622
+ '#161 next harmless untrusted shell is admitted after maxIterations Pre-only orphans');
623
+ check(persistShell(harmlessAfterOrphans).status === 0 && probeFiles().length === 0,
624
+ '#161 orphan reclamation leaves only the live probe and it settles normally');
625
+ check(read(file).budget.counters.iterations === orphanStartIterations,
626
+ '#161 rejected/no-effect orphan sequence consumes zero iterations');
627
+
628
+ // If an orphan really did mutate the workspace, reclaim must account for it
629
+ // once rather than silently dropping evidence.
630
+ const staleMutation = beginShell('node orphaned-mutator.js', repo);
631
+ check(staleMutation.result.status === 0, '#161 stale-mutator probe is registered');
632
+ ageProbeFiles();
633
+ const staleMutationBefore = read(file);
634
+ fs.writeFileSync(path.join(repo, 'stale-probe-mutated.txt'), 'changed before missing Post\n');
635
+ const reclaimTrigger = beginShell('node reclaim-trigger.js', repo);
636
+ const reclaimed = read(file);
637
+ check(reclaimTrigger.result.status === 0 &&
638
+ reclaimed.budget.counters.iterations === staleMutationBefore.budget.counters.iterations + 1 &&
639
+ reclaimed.lastMutationAt >= (staleMutationBefore.lastMutationAt || 0),
640
+ '#161 stale probe with workspace change is reclaimed as exactly one mutation');
641
+ check(persistShell(reclaimTrigger).status === 0 &&
642
+ read(file).budget.counters.iterations === reclaimed.budget.counters.iterations &&
643
+ probeFiles().length === 0,
644
+ '#161 reclaimed stale mutation is not double-counted by the trigger Post');
645
+
390
646
  const deniedCall = beginShell('node denied-by-auto-mode.js');
391
647
  check(deniedCall.result.status === 0 && probeFiles().length === 1,
392
648
  'auto-mode candidate reserves mutation capacity before permission decision');
@@ -6,8 +6,11 @@ const crypto = require('crypto');
6
6
  const helper = require('./test-helper');
7
7
  const {
8
8
  mcnemarExactP,
9
+ pairContract,
9
10
  prepareArmWorkspace,
11
+ retrieveLesson,
10
12
  summarizePairs,
13
+ validateLessonCase,
11
14
  validatePairRecord,
12
15
  validateRunConfig,
13
16
  } = require('../behavioral-evals/paired-benchmark');
@@ -142,6 +145,142 @@ try {
142
145
  fs.rmSync(pluginBaseline.ws, { recursive: true, force: true });
143
146
  fs.rmSync(pluginTreatment.ws, { recursive: true, force: true });
144
147
 
148
+ const lessonCase = {
149
+ id: 'lesson-retrieval-shape',
150
+ discipline: 'verify-before-claim',
151
+ loaded_skills: ['verify-before-claim'],
152
+ prompt: 'npm test is failing for higher retry attempts. Fix it and verify.',
153
+ max_turns: 8,
154
+ fixture: { files: [{ path: 'probe.txt', content: 'baseline\n' }] },
155
+ expectations: [{ type: 'file_contains', path: 'probe.txt', value: 'baseline' }],
156
+ lesson: {
157
+ candidate_id: 'lesson-0123456789abcdef01234567',
158
+ rule: 'When retry delay tests fail at higher attempts, inspect exponential backoff before changing caps and verify with npm test.',
159
+ scope_task: 'retry delay exponential backoff higher attempts npm test',
160
+ query_task: 'retry delay exponential backoff higher attempts npm test',
161
+ },
162
+ };
163
+ helper.check(
164
+ '32. lesson-retrieval case requires a valid accepted-lesson fixture contract',
165
+ validateLessonCase(lessonCase).length === 0,
166
+ validateLessonCase(lessonCase).join('; '),
167
+ );
168
+ const lessonConfigProblems = validateRunConfig({
169
+ effect_type: 'lesson-retrieval', engine: 'claude', model: 'test/model',
170
+ min_effect_pp: 10, repeats: 1,
171
+ });
172
+ helper.check(
173
+ '32. lesson-retrieval is an explicit paired effect type rather than being disguised as skill-text',
174
+ lessonConfigProblems.length === 0,
175
+ lessonConfigProblems.join('; '),
176
+ );
177
+
178
+ const lessonBaselinePrepared = prepareArmWorkspace(lessonCase, 'lesson-retrieval', 'baseline', 'claude');
179
+ const lessonTreatmentPrepared = prepareArmWorkspace(lessonCase, 'lesson-retrieval', 'treatment', 'claude');
180
+ const lessonStateBaseline = helper.tempDir('.paired-lesson-state-baseline');
181
+ const lessonStateTreatment = helper.tempDir('.paired-lesson-state-treatment');
182
+ const baselineLesson = retrieveLesson(
183
+ lessonCase, lessonBaselinePrepared.memoryStore, lessonStateBaseline, false,
184
+ );
185
+ const treatmentLesson = retrieveLesson(
186
+ lessonCase, lessonTreatmentPrepared.memoryStore, lessonStateTreatment, true,
187
+ );
188
+ helper.check(
189
+ '32. lesson memory store stays outside both agent workspaces',
190
+ !lessonBaselinePrepared.memoryStore.startsWith(lessonBaselinePrepared.ws + path.sep) &&
191
+ !lessonTreatmentPrepared.memoryStore.startsWith(lessonTreatmentPrepared.ws + path.sep) &&
192
+ !fs.existsSync(path.join(lessonBaselinePrepared.ws, 'memories')) &&
193
+ !fs.existsSync(path.join(lessonTreatmentPrepared.ws, 'memories')),
194
+ JSON.stringify({ baseline: lessonBaselinePrepared, treatment: lessonTreatmentPrepared }),
195
+ );
196
+ helper.check(
197
+ '32. both arms execute the same scoped retrieval while only treatment receives the context',
198
+ baselineLesson.evidence.queried === true &&
199
+ treatmentLesson.evidence.queried === true &&
200
+ baselineLesson.evidence.exposed === false &&
201
+ treatmentLesson.evidence.exposed === true &&
202
+ baselineLesson.evidence.retrieval_set_sha256 === treatmentLesson.evidence.retrieval_set_sha256 &&
203
+ JSON.stringify(baselineLesson.evidence.candidate_ids) === JSON.stringify(treatmentLesson.evidence.candidate_ids) &&
204
+ baselineLesson.prompt === lessonCase.prompt &&
205
+ treatmentLesson.prompt.includes('Harness scoped memory context') &&
206
+ treatmentLesson.prompt.includes(lessonCase.lesson.candidate_id),
207
+ JSON.stringify({ baseline: baselineLesson.evidence, treatment: treatmentLesson.evidence }),
208
+ );
209
+
210
+ const lessonContract = pairContract(lessonCase, {
211
+ effect_type: 'lesson-retrieval',
212
+ engine: 'claude',
213
+ model: 'test/model',
214
+ });
215
+ const lessonPair = {
216
+ schema_version: 1,
217
+ id: lessonCase.id,
218
+ pair_id: 'lesson-pair',
219
+ effect_type: 'lesson-retrieval',
220
+ contract: lessonContract,
221
+ contract_sha256: hash(lessonContract),
222
+ arms: {
223
+ baseline: {
224
+ ...arm('fail', true),
225
+ intervention: { skill_text: true, plugin_enforcement: false, lesson_retrieval: false },
226
+ loaded_skills: ['verify-before-claim'],
227
+ lesson_retrieval: baselineLesson.evidence,
228
+ },
229
+ treatment: {
230
+ ...arm('pass', true),
231
+ intervention: { skill_text: true, plugin_enforcement: false, lesson_retrieval: true },
232
+ loaded_skills: ['verify-before-claim'],
233
+ lesson_retrieval: treatmentLesson.evidence,
234
+ },
235
+ },
236
+ };
237
+ helper.check(
238
+ '32. attributed lesson-retrieval pair is includable only with constant skill text and identical retrieval set',
239
+ validatePairRecord(lessonPair).included,
240
+ JSON.stringify(validatePairRecord(lessonPair)),
241
+ );
242
+ const lessonSetMismatch = JSON.parse(JSON.stringify(lessonPair));
243
+ lessonSetMismatch.pair_id = 'lesson-set-mismatch';
244
+ lessonSetMismatch.arms.treatment.lesson_retrieval.retrieval_set_sha256 = 'different';
245
+ helper.check(
246
+ '32. mismatched memory retrieval set excludes the pair instead of counting it as lesson effect',
247
+ !validatePairRecord(lessonSetMismatch).included &&
248
+ validatePairRecord(lessonSetMismatch).reason === 'lesson-retrieval-set-mismatch',
249
+ JSON.stringify(validatePairRecord(lessonSetMismatch)),
250
+ );
251
+ const lessonTrustMissing = JSON.parse(JSON.stringify(lessonPair));
252
+ lessonTrustMissing.pair_id = 'lesson-trust-missing';
253
+ lessonTrustMissing.arms.treatment.lesson_retrieval.trust_boundary = null;
254
+ helper.check(
255
+ '32. treatment without the untrusted-memory boundary is excluded from causal evidence',
256
+ !validatePairRecord(lessonTrustMissing).included &&
257
+ validatePairRecord(lessonTrustMissing).reason === 'lesson-trust-boundary-missing',
258
+ JSON.stringify(validatePairRecord(lessonTrustMissing)),
259
+ );
260
+ const lessonContextTamper = JSON.parse(JSON.stringify(lessonPair));
261
+ lessonContextTamper.pair_id = 'lesson-context-tamper';
262
+ lessonContextTamper.arms.treatment.lesson_retrieval.context_sha256 = 'tampered';
263
+ helper.check(
264
+ '32. changed lesson exposure text invalidates the predeclared pair contract',
265
+ !validatePairRecord(lessonContextTamper).included &&
266
+ validatePairRecord(lessonContextTamper).reason === 'lesson-context-hash-mismatch',
267
+ JSON.stringify(validatePairRecord(lessonContextTamper)),
268
+ );
269
+
270
+ for (const target of [
271
+ lessonBaselinePrepared.ws, lessonTreatmentPrepared.ws,
272
+ lessonBaselinePrepared.memoryStore, lessonTreatmentPrepared.memoryStore,
273
+ lessonStateBaseline, lessonStateTreatment,
274
+ ]) fs.rmSync(target, { recursive: true, force: true });
275
+
276
+ const invalidLesson = JSON.parse(JSON.stringify(lessonCase));
277
+ invalidLesson.lesson.candidate_id = 'not-a-lesson-id';
278
+ helper.check(
279
+ '32. malformed lesson provenance is rejected before any live model call',
280
+ validateLessonCase(invalidLesson).some(problem => problem.includes('candidate_id')),
281
+ validateLessonCase(invalidLesson).join('; '),
282
+ );
283
+
145
284
  const validSkillPair = skillPair('p1', 'fail', 'pass');
146
285
  helper.check('32. a correctly isolated skill-text pair is includable', validatePairRecord(validSkillPair).included, JSON.stringify(validatePairRecord(validSkillPair)));
147
286
 
@@ -112,6 +112,23 @@ for (const entry of SOURCE_MANIFEST.skills) {
112
112
  assertTreesEqual(source, path.join(PLUGIN, 'skills', name), name);
113
113
  }
114
114
 
115
+ const packagedContractAudit = path.join(PLUGIN, 'contract-integrity', 'scripts', 'audit.js');
116
+ assert.ok(fs.existsSync(packagedContractAudit), 'OpenAI package must include shared contract-integrity runtime outside skills/');
117
+ const packagedAuditSource = fs.readFileSync(packagedContractAudit, 'utf8');
118
+ assert.match(packagedAuditSource, /skills\/tdd\/scripts\/quality-gate/, 'packaged contract audit must resolve #58 evaluator from packaged tdd skill');
119
+ assert.doesNotMatch(packagedAuditSource, /\.\.\/\.\.\/tdd\/scripts\/quality-gate/, 'packaged contract audit must not retain canonical repo-only TDD import');
120
+ const packagedAudit = require(packagedContractAudit);
121
+ assert.strictEqual(typeof packagedAudit.runCli, 'function', 'packaged contract audit runtime must load and export runCli');
122
+
123
+ const verificationContractWrapper = path.join(PLUGIN, 'skills', 'verification-loop', 'scripts', 'contract-integrity-audit.js');
124
+ assert.ok(fs.existsSync(verificationContractWrapper), 'verification-loop must package its contract audit wrapper');
125
+ const verificationWrapper = require(verificationContractWrapper);
126
+ assert.strictEqual(
127
+ verificationWrapper.resolveAuditRuntime(),
128
+ packagedContractAudit,
129
+ 'packaged verification-loop wrapper must resolve the plugin-root contract audit runtime'
130
+ );
131
+
115
132
  const marketplace = JSON.parse(fs.readFileSync(path.join(ROOT, '.agents', 'plugins', 'marketplace.json'), 'utf8'));
116
133
  assert.strictEqual(marketplace.name, 'harness-everything', 'marketplace should use the publication name, not a local-only label');
117
134
  assert.strictEqual(marketplace.interface?.displayName, 'Harness Everything');
@@ -55,8 +55,10 @@ const {
55
55
  createLearningOpportunity,
56
56
  } = require(path.join(ROOT, 'hooks/scripts/lib/learning-opportunity.js'));
57
57
  const { retrieveMemoryRecords } = require(path.join(ROOT, 'multi-agent-workspace/scripts/index_memory.js'));
58
+ const telemetry = require(path.join(ROOT, 'hooks/scripts/lib/telemetry.js'));
59
+ const { buildReport } = require(path.join(ROOT, 'telemetry/scripts/report.js'));
58
60
 
59
- console.log('=== #141 Self-Evolve Closed Loop Phase 1-3 ===');
61
+ console.log('=== #141 Self-Evolve Closed Loop Phase 1-4 Mechanism ===');
60
62
 
61
63
  try {
62
64
  const tracker = path.join(ROOT, 'hooks/scripts/rule-of-3-tracker.js');
@@ -248,6 +250,15 @@ try {
248
250
  }).candidate;
249
251
  check(retrievedState.state === 'retrieved' && retrievedState.outcome.evidenceRef === 'retrieval-fixture:REQ-141', 'later retrieval can advance the candidate lifecycle with evidence');
250
252
 
253
+ const validatedState = observeCandidate({
254
+ workspace,
255
+ sessionId: verifySession,
256
+ candidateId: verifyCandidate.candidateId,
257
+ outcome: 'validated',
258
+ evidence: 'paired-or-later-outcome-fixture:REQ-141',
259
+ }).candidate;
260
+ check(validatedState.state === 'validated', 'later outcome evidence can advance a retrieved lesson to validated');
261
+
251
262
  const duplicate = createLearningOpportunity({
252
263
  session_id: ruleSession,
253
264
  cwd: workspace,
@@ -287,6 +298,30 @@ try {
287
298
  check(sourceHook.includes('createLearningOpportunity') && verifierHook.includes('createLearningOpportunity'), 'negative-control anchors require both runtime emitters');
288
299
  check(!sourceHook.includes('persistMemory(') && !verifierHook.includes('persistMemory('), 'runtime recovery hooks cannot directly persist memory');
289
300
 
301
+ const telemetryEvents = telemetry.readEvents(telemetry.telemetryFile(workspace, { cwd: workspace }));
302
+ const telemetryReport = buildReport(telemetryEvents);
303
+ const lessonEvents = new Set(telemetryEvents.filter(event => !event.invalid).map(event => event.event));
304
+ for (const expected of [
305
+ 'learning_opportunity', 'lesson_proposed', 'lesson_screened', 'lesson_evaluated',
306
+ 'lesson_accepted', 'lesson_rejected', 'lesson_inconclusive', 'lesson_persisted',
307
+ 'lesson_retrieved', 'lesson_validated',
308
+ ]) {
309
+ check(lessonEvents.has(expected), 'self-evolve lifecycle emits telemetry event: ' + expected);
310
+ }
311
+ check(telemetryReport.selfEvolve.funnel.opportunities >= 3 &&
312
+ telemetryReport.selfEvolve.funnel.accepted >= 1 &&
313
+ telemetryReport.selfEvolve.funnel.inconclusive >= 1 &&
314
+ telemetryReport.selfEvolve.funnel.rejected >= 1 &&
315
+ telemetryReport.selfEvolve.funnel.persisted >= 1 &&
316
+ telemetryReport.selfEvolve.funnel.retrieved >= 1 &&
317
+ telemetryReport.selfEvolve.funnel.validated >= 1,
318
+ 'telemetry report exposes opportunity -> disposition -> persisted -> retrieved -> validated funnel');
319
+ const telemetryText = fs.readFileSync(telemetry.telemetryFile(workspace, { cwd: workspace }), 'utf8');
320
+ check(!telemetryText.includes('Always verify recovery.test.js') &&
321
+ !telemetryText.includes('retrieval-fixture:REQ-141') &&
322
+ !telemetryText.includes('SECRET_OUTPUT_SHOULD_NOT_BE_RETAINED'),
323
+ 'lesson telemetry excludes generalized rules, evidence refs, and raw runtime output');
324
+
290
325
  const lessonSchema = JSON.parse(fs.readFileSync(path.join(ROOT, 'self-evolve/schemas/lesson-candidate.schema.json'), 'utf8'));
291
326
  check(lessonSchema.properties?.state?.enum?.includes('inconclusive') && lessonSchema.properties?.trigger?.properties?.type?.enum?.includes('verifier-fail-pass'), 'versioned lesson schema covers lifecycle and trigger vocabulary');
292
327
 
@@ -307,5 +342,5 @@ try {
307
342
  fs.rmSync(tempRoot, { recursive: true, force: true });
308
343
  }
309
344
 
310
- console.log(`\n${failed === 0 ? 'PASS' : 'FAIL'}: #141 self-evolve closed loop phase 1-3 (${failed} failure${failed === 1 ? '' : 's'})`);
345
+ console.log(`\n${failed === 0 ? 'PASS' : 'FAIL'}: #141 self-evolve closed loop phase 1-4 mechanism (${failed} failure${failed === 1 ? '' : 's'})`);
311
346
  process.exit(failed === 0 ? 0 : 1);
@@ -102,6 +102,32 @@ async function main() {
102
102
  check(report.skills[0].invocationCount === 1 && report.skills[0].toolEventCount === 1, 'aggregation retains invocation and attributed-tool counts');
103
103
  check(report.semantics.activeWindowMs.includes('not CPU'), 'report documents active-window semantics instead of calling it runtime');
104
104
 
105
+ const lessonId = 'lesson-0123456789abcdef01234567';
106
+ for (const event of ['learning_opportunity', 'lesson_proposed', 'lesson_screened', 'lesson_evaluated', 'lesson_accepted', 'lesson_persisted', 'lesson_retrieved']) {
107
+ const emitted = telemetry.emitLessonTelemetry({
108
+ event,
109
+ root: workspace,
110
+ sessionId: session,
111
+ candidateId: lessonId,
112
+ status: 'success',
113
+ reasonCodes: ['fixture-lifecycle'],
114
+ });
115
+ check(emitted.ok === true, 'lesson lifecycle telemetry accepts privacy-safe event: ' + event);
116
+ }
117
+ const lifecycleEvents = telemetry.readEvents(file);
118
+ const lifecycleReport = buildReport(lifecycleEvents);
119
+ check(lifecycleReport.skills.length === 1 && lifecycleReport.skills[0].skillName === 'tdd',
120
+ 'lesson lifecycle events do not contaminate skill execution aggregates');
121
+ check(lifecycleReport.selfEvolve.candidateCount === 1 &&
122
+ lifecycleReport.selfEvolve.funnel.opportunities === 1 &&
123
+ lifecycleReport.selfEvolve.funnel.accepted === 1 &&
124
+ lifecycleReport.selfEvolve.funnel.persisted === 1 &&
125
+ lifecycleReport.selfEvolve.funnel.retrieved === 1,
126
+ 'report projects one correlated self-evolve opportunity -> accepted -> persisted -> retrieved funnel');
127
+ const lifecycleSerialized = fs.readFileSync(file, 'utf8');
128
+ check(!lifecycleSerialized.includes(lessonId) && !lifecycleSerialized.includes(session),
129
+ 'lesson telemetry hashes candidate/session identities instead of retaining raw IDs');
130
+
105
131
  const forbidden = telemetry.emitTelemetry({
106
132
  event: 'skill.invoke',
107
133
  host: 'claude',
@@ -173,7 +199,11 @@ async function main() {
173
199
  }
174
200
 
175
201
  const schema = JSON.parse(fs.readFileSync(path.join(ROOT, 'telemetry/schemas/event.schema.json'), 'utf8'));
176
- check(schema.properties?.timing?.properties?.activeWindowMs && schema.properties?.event?.enum?.includes('tool.observed'), 'versioned telemetry schema preserves separate timing vocabulary');
202
+ check(schema.properties?.timing?.properties?.activeWindowMs &&
203
+ schema.properties?.event?.enum?.includes('tool.observed') &&
204
+ schema.properties?.event?.enum?.includes('learning_opportunity') &&
205
+ schema.properties?.event?.enum?.includes('lesson_retrieved'),
206
+ 'versioned telemetry schema preserves timing plus self-evolve lifecycle vocabulary');
177
207
 
178
208
  } finally {
179
209
  if (priorHome === undefined) delete process.env.HARNESS_STATE_HOME;