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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +32 -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 +137 -0
- 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/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-runtime.js +18 -1
- package/hooks/scripts/state-persist.js +5 -1
- package/hooks/scripts/workflow-disposition.js +19 -8
- package/hooks/scripts/workflow-gate.js +16 -3
- 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-runtime.js +18 -1
- package/plugins/harness-everything/hooks/scripts/state-persist.js +5 -1
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +19 -8
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +16 -3
- 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);
|
|
@@ -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-
|
|
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;
|
|
@@ -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.
|
package/grill-with-docs/SKILL.md
CHANGED
|
@@ -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);
|