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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +53 -0
- package/README.md +1 -0
- package/behavioral-evals/README.md +37 -0
- package/behavioral-evals/cases/self-evolve-retrieval-recurrence.yaml +48 -0
- package/behavioral-evals/paired-benchmark.js +211 -5
- package/ci/contract-integrity.test.js +68 -2
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +258 -2
- package/ci/mechanism-32-paired-benchmark.test.js +139 -0
- package/ci/openai-plugin-package.test.js +17 -0
- package/ci/self-evolve-loop.test.js +37 -2
- package/ci/telemetry.test.js +31 -1
- package/docs/enforcement-locks.md +114 -0
- package/grill-with-docs/ADR-FORMAT.md +22 -0
- package/grill-with-docs/SKILL.md +1 -1
- package/hooks/scripts/lib/learning-opportunity.js +22 -0
- package/hooks/scripts/lib/telemetry.js +40 -0
- package/hooks/scripts/lib/workflow-isolation.js +10 -2
- package/hooks/scripts/lib/workflow-runtime.js +108 -5
- package/hooks/scripts/state-persist.js +9 -3
- package/hooks/scripts/workflow-disposition.js +19 -8
- package/hooks/scripts/workflow-gate.js +33 -4
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/index_memory.js +28 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/contract-integrity/scripts/audit.js +536 -0
- package/plugins/harness-everything/hooks/scripts/lib/learning-opportunity.js +22 -0
- package/plugins/harness-everything/hooks/scripts/lib/telemetry.js +40 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +10 -2
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +108 -5
- package/plugins/harness-everything/hooks/scripts/state-persist.js +9 -3
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +19 -8
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +33 -4
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/grill-with-docs/ADR-FORMAT.md +22 -0
- package/plugins/harness-everything/skills/grill-with-docs/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +28 -0
- package/plugins/harness-everything/skills/self-evolve/SKILL.md +1 -1
- package/plugins/harness-everything/skills/self-evolve/scripts/lesson-candidate.js +52 -0
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/references/process.md +26 -0
- package/plugins/harness-everything/skills/to-spec/templates/cli-reference.md +14 -0
- package/plugins/harness-everything/skills/to-spec/templates/dev-doc.md +14 -0
- package/plugins/harness-everything/skills/to-spec/templates/feature-spec.md +14 -0
- package/plugins/harness-everything/skills/to-spec/templates/schema-doc.md +14 -0
- package/plugins/harness-everything/skills/to-tickets/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-tickets/references/publishing-and-templates.md +20 -1
- package/plugins/harness-everything/skills/verification-loop/SKILL.md +3 -2
- package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md +25 -1
- package/plugins/harness-everything/skills/verification-loop/scripts/contract-integrity-audit.js +31 -0
- package/plugins/harness-everything/skills/verification-loop/templates/verification-report.template.md +3 -0
- package/scripts/sync-openai-plugin.js +12 -0
- package/self-evolve/SKILL.md +1 -1
- package/self-evolve/scripts/lesson-candidate.js +52 -0
- package/telemetry/schemas/event.schema.json +141 -22
- package/telemetry/scripts/report.js +42 -0
- package/to-spec/SKILL.md +1 -1
- package/to-spec/references/process.md +26 -0
- package/to-spec/templates/cli-reference.md +14 -0
- package/to-spec/templates/dev-doc.md +14 -0
- package/to-spec/templates/feature-spec.md +14 -0
- package/to-spec/templates/schema-doc.md +14 -0
- package/to-tickets/SKILL.md +1 -1
- package/to-tickets/references/publishing-and-templates.md +20 -1
- package/verification-loop/SKILL.md +3 -2
- package/verification-loop/references/verification-phases.md +25 -1
- package/verification-loop/scripts/contract-integrity-audit.js +31 -0
- 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 = {
|
|
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-
|
|
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-
|
|
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);
|
package/ci/telemetry.test.js
CHANGED
|
@@ -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 &&
|
|
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;
|