harness-everything 0.11.7 → 0.12.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +32 -0
  4. package/behavioral-evals/README.md +37 -0
  5. package/behavioral-evals/cases/self-evolve-retrieval-recurrence.yaml +48 -0
  6. package/behavioral-evals/paired-benchmark.js +211 -5
  7. package/ci/contract-integrity.test.js +68 -2
  8. package/ci/mechanism-2ah-workflow-lifecycle.test.js +137 -0
  9. package/ci/mechanism-32-paired-benchmark.test.js +139 -0
  10. package/ci/openai-plugin-package.test.js +17 -0
  11. package/ci/self-evolve-loop.test.js +37 -2
  12. package/ci/telemetry.test.js +31 -1
  13. package/grill-with-docs/ADR-FORMAT.md +22 -0
  14. package/grill-with-docs/SKILL.md +1 -1
  15. package/hooks/scripts/lib/learning-opportunity.js +22 -0
  16. package/hooks/scripts/lib/telemetry.js +40 -0
  17. package/hooks/scripts/lib/workflow-runtime.js +18 -1
  18. package/hooks/scripts/state-persist.js +5 -1
  19. package/hooks/scripts/workflow-disposition.js +19 -8
  20. package/hooks/scripts/workflow-gate.js +16 -3
  21. package/multi-agent-workspace/SKILL.md +1 -1
  22. package/multi-agent-workspace/scripts/index_memory.js +28 -0
  23. package/opencode-plugin/plugin.json +1 -1
  24. package/package.json +1 -1
  25. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  26. package/plugins/harness-everything/contract-integrity/scripts/audit.js +536 -0
  27. package/plugins/harness-everything/hooks/scripts/lib/learning-opportunity.js +22 -0
  28. package/plugins/harness-everything/hooks/scripts/lib/telemetry.js +40 -0
  29. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +18 -1
  30. package/plugins/harness-everything/hooks/scripts/state-persist.js +5 -1
  31. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +19 -8
  32. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +16 -3
  33. package/plugins/harness-everything/plugin.json +1 -1
  34. package/plugins/harness-everything/skills/grill-with-docs/ADR-FORMAT.md +22 -0
  35. package/plugins/harness-everything/skills/grill-with-docs/SKILL.md +1 -1
  36. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
  37. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +28 -0
  38. package/plugins/harness-everything/skills/self-evolve/SKILL.md +1 -1
  39. package/plugins/harness-everything/skills/self-evolve/scripts/lesson-candidate.js +52 -0
  40. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  41. package/plugins/harness-everything/skills/to-spec/references/process.md +26 -0
  42. package/plugins/harness-everything/skills/to-spec/templates/cli-reference.md +14 -0
  43. package/plugins/harness-everything/skills/to-spec/templates/dev-doc.md +14 -0
  44. package/plugins/harness-everything/skills/to-spec/templates/feature-spec.md +14 -0
  45. package/plugins/harness-everything/skills/to-spec/templates/schema-doc.md +14 -0
  46. package/plugins/harness-everything/skills/to-tickets/SKILL.md +1 -1
  47. package/plugins/harness-everything/skills/to-tickets/references/publishing-and-templates.md +20 -1
  48. package/plugins/harness-everything/skills/verification-loop/SKILL.md +3 -2
  49. package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md +25 -1
  50. package/plugins/harness-everything/skills/verification-loop/scripts/contract-integrity-audit.js +31 -0
  51. package/plugins/harness-everything/skills/verification-loop/templates/verification-report.template.md +3 -0
  52. package/scripts/sync-openai-plugin.js +12 -0
  53. package/self-evolve/SKILL.md +1 -1
  54. package/self-evolve/scripts/lesson-candidate.js +52 -0
  55. package/telemetry/schemas/event.schema.json +141 -22
  56. package/telemetry/scripts/report.js +42 -0
  57. package/to-spec/SKILL.md +1 -1
  58. package/to-spec/references/process.md +26 -0
  59. package/to-spec/templates/cli-reference.md +14 -0
  60. package/to-spec/templates/dev-doc.md +14 -0
  61. package/to-spec/templates/feature-spec.md +14 -0
  62. package/to-spec/templates/schema-doc.md +14 -0
  63. package/to-tickets/SKILL.md +1 -1
  64. package/to-tickets/references/publishing-and-templates.md +20 -1
  65. package/verification-loop/SKILL.md +3 -2
  66. package/verification-loop/references/verification-phases.md +25 -1
  67. package/verification-loop/scripts/contract-integrity-audit.js +31 -0
  68. 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);
@@ -85,6 +86,28 @@ const probeFiles = () => {
85
86
  };
86
87
  const stop = extra => node('hooks/scripts/workflow-stop-gate.js', { ...payload, ...extra });
87
88
  const control = (...args) => node('hooks/scripts/workflow-disposition.js', null, [args[0], '--session-id', sessionId, ...args.slice(1)]);
89
+ function fixtureSession(fixtureSessionId) {
90
+ const fixturePayload = { session_id: fixtureSessionId, cwd: repo };
91
+ const fixtureSessionDir = state.getSessionDir(repo, fixtureSessionId);
92
+ const fixtureFile = path.join(fixtureSessionDir, 'workflow-run.json');
93
+ return {
94
+ payload: fixturePayload,
95
+ file: fixtureFile,
96
+ sessionDir: fixtureSessionDir,
97
+ route: prompt => node('harness-everything/scripts/kernel-router.js', { ...fixturePayload, prompt }),
98
+ gate: (tool, input, extra = {}) => node('hooks/scripts/workflow-gate.js', { ...fixturePayload, ...extra, tool_name: tool, tool_input: input }),
99
+ persist: (tool, input, response = {}, extra = {}) => node('hooks/scripts/state-persist.js', {
100
+ ...fixturePayload,
101
+ ...extra,
102
+ hook_event_name: 'PostToolUse',
103
+ tool_name: tool,
104
+ tool_input: input,
105
+ tool_response: response,
106
+ }),
107
+ stop: extra => node('hooks/scripts/workflow-stop-gate.js', { ...fixturePayload, ...extra }),
108
+ control: (...args) => node('hooks/scripts/workflow-disposition.js', null, [args[0], '--session-id', fixtureSessionId, ...args.slice(1)]),
109
+ };
110
+ }
88
111
  const stages = [
89
112
  { stageId: 'build', goal: 'fix', agent: 'fable-worker', task: 'fix bounded module', dependsOn: [], writeSet: ['src'], checkCommand: 'node test-build.js', passCondition: 'exit 0' },
90
113
  { stageId: 'optional', goal: 'external scope', agent: 'fable-worker', task: 'external task', dependsOn: [], writeSet: [], checkCommand: 'node external.js', passCondition: 'exit 0' },
@@ -92,6 +115,78 @@ const stages = [
92
115
  ];
93
116
 
94
117
  try {
118
+ // #162: Tier-2 shell accounting must degrade conservatively when a workspace
119
+ // is not a Git repository. Lack of a Git fingerprint is not an isolation
120
+ // violation for a non-major workflow.
121
+ const nonGit = fixtureSession('workflow-non-git');
122
+ check(nonGit.route('Fix this checkout bug with a regression test').status === 0 &&
123
+ read(nonGit.file).strategy === 'iterative-single',
124
+ '#162 non-git fixture routes to Tier 2 iterative workflow');
125
+ check(nonGit.control('start').status === 0 && read(nonGit.file).state === 'running',
126
+ '#162 non-git Tier 2 workflow starts normally');
127
+ let nonGitIterations = read(nonGit.file).budget?.counters?.iterations || 0;
128
+ for (const [index, command] of ['npm --version', 'node -e "process.exit(0)"', 'cd .', 'git init -q'].entries()) {
129
+ const toolUseId = 'toolu_non_git_pre_' + index;
130
+ const admitted = nonGit.gate('Bash', { command }, { tool_use_id: toolUseId });
131
+ check(admitted.status === 0, '#162 non-git Tier 2 admits untrusted shell: ' + command);
132
+ const afterGate = read(nonGit.file);
133
+ check(afterGate.budget.counters.iterations === ++nonGitIterations &&
134
+ afterGate.lastMutationAt > 0 &&
135
+ afterGate.budget.events.at(-1)?.evidence === 'shell:unobservable-workspace',
136
+ '#162 unobservable non-git shell is conservatively counted once: ' + command);
137
+ check(nonGit.persist('Bash', { command }, { stdout: 'fixture success' }, { tool_use_id: toolUseId }).status === 0,
138
+ '#162 non-git shell PostToolUse stays fail-open without a probe: ' + command);
139
+ }
140
+
141
+ // A probe may have been created while Git was observable and become
142
+ // unobservable before PostToolUse (for example .git disappears). Tier 2
143
+ // must settle it conservatively instead of blocking the workflow.
144
+ const lostObservationId = 'toolu_non_git_post_fallback';
145
+ const lostObservationPayload = {
146
+ ...nonGit.payload,
147
+ tool_name: 'Bash',
148
+ tool_use_id: lostObservationId,
149
+ tool_input: { command: 'node lost-observation.js' },
150
+ };
151
+ const lostObservationContext = workflowRuntime.loadWorkflow(lostObservationPayload);
152
+ const lostObservationKey = workflowIsolation.shellProbeKey(lostObservationPayload, repo);
153
+ workflowRuntime.registerMutationProbe(lostObservationContext, lostObservationKey, 'f'.repeat(64), true);
154
+ const beforeLostObservation = read(nonGit.file);
155
+ check(nonGit.persist('Bash', { command: 'node lost-observation.js' }, { stdout: 'fixture success' },
156
+ { tool_use_id: lostObservationId }).status === 0,
157
+ '#162 Tier 2 PostToolUse degrades when workspace observation becomes unavailable');
158
+ const afterLostObservation = read(nonGit.file);
159
+ check(afterLostObservation.state === 'running' &&
160
+ afterLostObservation.budget.counters.iterations === beforeLostObservation.budget.counters.iterations + 1 &&
161
+ afterLostObservation.lastMutationAt >= (beforeLostObservation.lastMutationAt || 0),
162
+ '#162 lost post-observation is settled as one conservative mutation without blocking');
163
+
164
+ const nonGitTarget = path.join(repo, 'non-git-edit.txt');
165
+ check(nonGit.gate('Write', { file_path: nonGitTarget }).status === 0,
166
+ '#162 direct edit remains available in non-git Tier 2');
167
+ fs.writeFileSync(nonGitTarget, 'changed\n');
168
+ check(nonGit.persist('Write', { file_path: nonGitTarget }).status === 0,
169
+ '#162 direct non-git edit persists mutation milestone');
170
+ const verifyId = 'toolu_non_git_verify';
171
+ check(nonGit.gate('Bash', { command: 'node run-test.js' }, { tool_use_id: verifyId }).status === 0,
172
+ '#162 verification shell is admitted in non-git Tier 2');
173
+ check(nonGit.persist('Bash', { command: 'node run-test.js' }, { stdout: 'tests passed' },
174
+ { tool_use_id: verifyId }).status === 0,
175
+ '#162 non-git verification result is persisted');
176
+ check(nonGit.stop().status === 0 && read(nonGit.file).state === 'satisfied',
177
+ '#162 edit then verification can satisfy Stop in non-git Tier 2');
178
+
179
+ const nonGitMajor = fixtureSession('workflow-non-git-major');
180
+ check(nonGitMajor.route('Refactor the entire authentication architecture across all services without fable').status === 0 &&
181
+ read(nonGitMajor.file).tier === 'tier3',
182
+ '#162 non-git major fixture routes to Tier 3');
183
+ check(nonGitMajor.control('start').status === 0 && read(nonGitMajor.file).state === 'running',
184
+ '#162 non-Fable Tier 3 controller can enter running state before mutation');
185
+ const majorBlocked = nonGitMajor.gate('Bash', { command: 'node mutate.js' }, { tool_use_id: 'toolu_non_git_major' });
186
+ check(majorBlocked.status === 2 && /Git worktree isolation/i.test(majorBlocked.stderr),
187
+ '#162 non-git Tier 3 remains fail-closed with an isolation-specific error');
188
+
189
+ fs.unlinkSync(nonGitTarget);
95
190
  git(['init']); fs.writeFileSync(path.join(repo, 'README.md'), 'fixture\n'); git(['add', '.']);
96
191
  git(['-c', 'user.name=Harness Test', '-c', 'user.email=harness@example.invalid', 'commit', '-m', 'fixture']);
97
192
  git(['worktree', 'add', linked, '-b', 'isolated']);
@@ -225,6 +320,48 @@ try {
225
320
  promotedTier3.mutationIsolation?.required === true && !promotedTier3.pendingPlan, 'pending Tier 3 isolation becomes active atomically on start');
226
321
  check(workflowRuntime.isMajorWorkflow(promotedTier3), 'major-workflow classification flips only after explicit activation');
227
322
  check(gate('Write', { file_path: path.join(repo, 'premature-primary.js') }, repo).status === 2, 'activated Tier 3 plan still blocks primary-tree mutation');
323
+
324
+ // #163: a queued Tier-3 plan can intentionally remain single-agent when
325
+ // Fable is prohibited. Explicit start must still promote the whole plan,
326
+ // including isolation and budget metadata, before any mutation is admitted.
327
+ fs.unlinkSync(file);
328
+ check(route('Fix this checkout bug with a regression test').status === 0 && read(file).strategy === 'iterative-single',
329
+ '#163 fixture starts from a Tier 2 iterative workflow');
330
+ check(control('start').status === 0 && read(file).state === 'running',
331
+ '#163 fixture activates Tier 2 before a stronger route arrives');
332
+ check(route('Refactor the entire authentication architecture across all services and migrate the database schema without fable').status === 0,
333
+ '#163 stronger Tier 3 route without Fable is retained for explicit replan');
334
+ const queuedNonFableTier3 = read(file);
335
+ check(queuedNonFableTier3.state === 'blocked' &&
336
+ queuedNonFableTier3.pendingPlan?.tier === 'tier3' &&
337
+ queuedNonFableTier3.pendingPlan?.strategy === 'iterative-single' &&
338
+ queuedNonFableTier3.pendingPlan?.mutationIsolation?.required === true,
339
+ '#163 queued Tier 3 plan remains non-Fable but still requires isolation');
340
+ write(file, {
341
+ ...queuedNonFableTier3,
342
+ budget: {
343
+ ...queuedNonFableTier3.budget,
344
+ limits: { ...queuedNonFableTier3.budget.limits, maxIterations: 1 },
345
+ },
346
+ });
347
+ check(control('start').status === 0, '#163 explicit start activates queued non-Fable Tier 3 plan');
348
+ const promotedNonFableTier3 = read(file);
349
+ check(promotedNonFableTier3.workflowPlan?.tier === 'tier3' &&
350
+ promotedNonFableTier3.tier === 'tier3' &&
351
+ promotedNonFableTier3.strategy === 'iterative-single' &&
352
+ promotedNonFableTier3.mutationIsolation?.required === true &&
353
+ !promotedNonFableTier3.pendingPlan,
354
+ '#163 non-Fable Tier 3 plan replaces all authoritative active-plan fields');
355
+ check(promotedNonFableTier3.budget?.limits?.maxIterations === promotedNonFableTier3.workflowPlan?.limits?.maxIterations &&
356
+ promotedNonFableTier3.budget?.limits?.maxIterations !== 1,
357
+ '#163 plan activation re-derives budget limits from the promoted plan');
358
+ check(workflowRuntime.isMajorWorkflow(promotedNonFableTier3),
359
+ '#163 non-Fable Tier 3 promotion enables major-workflow isolation');
360
+ check(gate('Write', { file_path: path.join(repo, 'non-fable-primary.js') }, repo).status === 2,
361
+ '#163 promoted non-Fable Tier 3 blocks primary-tree mutation');
362
+ check(gate('Write', { file_path: path.join(linked, 'non-fable-linked.js') }, linked).status === 0,
363
+ '#163 promoted non-Fable Tier 3 admits linked-worktree mutation');
364
+
228
365
  fs.unlinkSync(file);
229
366
  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
367
  let tier2 = read(file);
@@ -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;
@@ -22,6 +22,28 @@ Only include these when they add genuine value. Most ADRs won't need them.
22
22
  - **Considered Options** — only when the rejected alternatives are worth remembering
23
23
  - **Consequences** — only when non-obvious downstream effects need to be called out
24
24
 
25
+ ## Decision lifecycle and contract lineage
26
+
27
+ Once an ADR is **accepted**, keep its decision and rationale historically stable. Do not rewrite an accepted ADR to make a later design look as though it was always the decision.
28
+
29
+ Use compact metadata when the ADR participates in a behavior contract:
30
+
31
+ ```md
32
+ ADR-ID: ADR-0001
33
+ Status: proposed | accepted | deprecated | superseded by ADR-0002
34
+ Affected requirements: REQ-001, REQ-004
35
+ ```
36
+
37
+ When a later change materially changes the decision or its rationale:
38
+
39
+ 1. create a new sequential ADR;
40
+ 2. state the new decision and why the prior trade-off changed;
41
+ 3. mark the earlier ADR `superseded by ADR-NNNN`;
42
+ 4. update the living spec requirements affected by that decision;
43
+ 5. let the structured contract trace carry detailed ticket/test/implementation lineage.
44
+
45
+ A ticket or implementation note never silently becomes the new architectural source of truth. If only implementation changes and the approved behavior/decision does not, keep the accepted ADR unchanged.
46
+
25
47
  ## Numbering
26
48
 
27
49
  Scan `docs/adr/` for the highest existing number and increment by one.
@@ -4,7 +4,7 @@ description: Challenge the plan against the existing domain model, sharpen termi
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.4.0
7
+ version: 0.12.0
8
8
  ---
9
9
 
10
10
  # Grill With Docs (Domain Modeling & Architectural Alignment)
@@ -8,6 +8,7 @@ const {
8
8
  getSessionId,
9
9
  getSessionDir,
10
10
  } = require('./harness-state');
11
+ const { emitLessonTelemetry } = require('./telemetry');
11
12
 
12
13
  const SCHEMA_VERSION = 1;
13
14
  const TRIGGERS = new Set(['rule-of-3-recovery', 'verifier-fail-pass']);
@@ -134,6 +135,27 @@ function createLearningOpportunity(payload, input = {}) {
134
135
  updatedAt: observedAt,
135
136
  };
136
137
  atomicWriteJson(file, candidate);
138
+ const lifecycleReasonCodes = [...candidate.trigger.reasonCodes, `trigger:${triggerType}`];
139
+ emitLessonTelemetry({
140
+ event: 'learning_opportunity',
141
+ root,
142
+ payload,
143
+ sessionId,
144
+ candidateId,
145
+ observedAt,
146
+ status: 'success',
147
+ reasonCodes: lifecycleReasonCodes,
148
+ });
149
+ emitLessonTelemetry({
150
+ event: 'lesson_proposed',
151
+ root,
152
+ payload,
153
+ sessionId,
154
+ candidateId,
155
+ observedAt,
156
+ status: 'success',
157
+ reasonCodes: lifecycleReasonCodes,
158
+ });
137
159
  return { created: true, file, candidate };
138
160
  }
139
161
 
@@ -6,11 +6,26 @@ const path = require('path');
6
6
  const { getWorkspaceRoot, getStateRoot, getSessionDir } = require('./harness-state');
7
7
 
8
8
  const TELEMETRY_SCHEMA_VERSION = 1;
9
+ const LESSON_EVENT_TYPES = new Set([
10
+ 'learning_opportunity',
11
+ 'lesson_proposed',
12
+ 'lesson_screened',
13
+ 'lesson_evaluated',
14
+ 'lesson_accepted',
15
+ 'lesson_rejected',
16
+ 'lesson_inconclusive',
17
+ 'lesson_persisted',
18
+ 'lesson_retrieved',
19
+ 'lesson_validated',
20
+ 'lesson_regressed',
21
+ 'lesson_superseded',
22
+ ]);
9
23
  const EVENT_TYPES = new Set([
10
24
  'skill.invoke',
11
25
  'skill.loaded',
12
26
  'skill.complete',
13
27
  'tool.observed',
28
+ ...LESSON_EVENT_TYPES,
14
29
  ]);
15
30
  const STATUSES = new Set(['unknown', 'success', 'failure', 'aborted']);
16
31
  const FORBIDDEN_KEYS = new Set([
@@ -102,6 +117,29 @@ function telemetryFile(root, payload) {
102
117
  return path.join(getStateRoot(root || getWorkspaceRoot(payload), payload), 'telemetry', 'events.jsonl');
103
118
  }
104
119
 
120
+ function emitLessonTelemetry(input = {}) {
121
+ const event = String(input.event || '');
122
+ if (!LESSON_EVENT_TYPES.has(event)) {
123
+ return { ok: false, error: `unsupported lesson telemetry event: ${event}`, overheadMs: 0 };
124
+ }
125
+ const invocationId = localId('lesson', input.candidateId);
126
+ if (!invocationId) {
127
+ return { ok: false, error: 'lesson telemetry requires candidateId', overheadMs: 0 };
128
+ }
129
+ return emitTelemetry({
130
+ event,
131
+ host: ['claude', 'codex', 'opencode', 'unknown'].includes(input.host)
132
+ ? input.host
133
+ : detectHost(input.payload || {}),
134
+ observedAt: input.observedAt || nowIso(),
135
+ sessionId: localId('session', input.sessionId),
136
+ invocationId,
137
+ skillName: 'self-evolve',
138
+ status: STATUSES.has(input.status) ? input.status : 'unknown',
139
+ reasonCodes: input.reasonCodes || [],
140
+ }, { root: input.root, payload: input.payload });
141
+ }
142
+
105
143
  function emitTelemetry(input, options = {}) {
106
144
  const started = process.hrtime.bigint();
107
145
  try {
@@ -361,9 +399,11 @@ function readEvents(file) {
361
399
  module.exports = {
362
400
  EVENT_TYPES,
363
401
  FORBIDDEN_KEYS,
402
+ LESSON_EVENT_TYPES,
364
403
  STATUSES,
365
404
  TELEMETRY_SCHEMA_VERSION,
366
405
  detectHost,
406
+ emitLessonTelemetry,
367
407
  emitTelemetry,
368
408
  hostDuration,
369
409
  localId,
@@ -162,6 +162,23 @@ function settleMutationProbe(context, key, afterFingerprint, tool) {
162
162
  });
163
163
  }
164
164
 
165
+ function settleMutationProbeConservative(context, key, tool, evidence = 'unobservable-workspace') {
166
+ return withMutationProbeLock(context, () => {
167
+ const file = mutationProbeFile(context, key);
168
+ const probe = readJson(file);
169
+ if (!probe || probe.schemaVersion !== 1) return null;
170
+ refreshWorkflowForProbe(context);
171
+ fs.unlinkSync(file);
172
+ context.workflow.lastMutationAt = Date.now();
173
+ if (probe.reserveIteration && context.workflow.strategy === 'iterative-single') {
174
+ recordBudgetEvent(context, 'iteration', { evidence: String(tool || 'shell') + ':' + evidence });
175
+ } else {
176
+ saveWorkflow(context);
177
+ }
178
+ return { changed: true, probe, conservative: true };
179
+ });
180
+ }
181
+
165
182
  function discardMutationProbe(context, key) {
166
183
  return withMutationProbeLock(context, () => {
167
184
  const file = mutationProbeFile(context, key);
@@ -396,4 +413,4 @@ function readHookInput(decide) {
396
413
  process.stdin.on('error', finish);
397
414
  }
398
415
 
399
- module.exports = { OPEN_STATES, SAFE_ID, WORKFLOW_CONTROLLER_COMMANDS, isMajorWorkflow, loadWorkflow, saveWorkflow, budgetLimits, ensureWorkflowBudget, mutationProbeReservations, assertIterationCapacity, registerMutationProbe, peekMutationProbe, settleMutationProbe, discardMutationProbe, clearMutationProbes, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, matchingRun, unresolvedStages, readHookInput };
416
+ module.exports = { OPEN_STATES, SAFE_ID, WORKFLOW_CONTROLLER_COMMANDS, isMajorWorkflow, loadWorkflow, saveWorkflow, budgetLimits, ensureWorkflowBudget, mutationProbeReservations, assertIterationCapacity, registerMutationProbe, peekMutationProbe, settleMutationProbe, settleMutationProbeConservative, discardMutationProbe, clearMutationProbes, recordBudgetEvent, resetWorkflowBudget, syncBudgetToRun, matchingRun, unresolvedStages, readHookInput };
@@ -2,7 +2,7 @@
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
5
- const { loadWorkflow, saveWorkflow, peekMutationProbe, settleMutationProbe } = require('./lib/workflow-runtime');
5
+ const { loadWorkflow, saveWorkflow, isMajorWorkflow, peekMutationProbe, settleMutationProbe, settleMutationProbeConservative } = require('./lib/workflow-runtime');
6
6
  const { cwdOf, shellProbeKey, workspaceFingerprint, directMutationInWorkspace } = require('./lib/workflow-isolation');
7
7
  const { observeTool } = require('./lib/telemetry');
8
8
 
@@ -42,6 +42,10 @@ function observeWorkspaceMutation(payload, root) {
42
42
  try {
43
43
  after = workspaceFingerprint(cwd);
44
44
  } catch (error) {
45
+ if (!isMajorWorkflow(context.workflow)) {
46
+ const conservative = settleMutationProbeConservative(context, probeKey, toolName, 'unobservable-workspace');
47
+ return Boolean(conservative && conservative.changed);
48
+ }
45
49
  context.workflow.state = 'blocked';
46
50
  context.workflow.blockReason = 'mutation-observation-failed';
47
51
  saveWorkflow(context);