harness-everything 0.45.0 → 0.46.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/.agents/harness-everything/project-docs.json +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/README.md +75 -0
- package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/capture.sanitized.jsonl +32 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +2 -2
- package/ci/mechanism-2ax-doc-lifecycle.test.js +2 -1
- package/ci/mechanism-325-3b-s2b-capture.test.js +6 -1
- package/ci/mechanism-325-f4-projections.test.js +11 -1
- package/ci/mechanism-prb-outline-review.test.js +1449 -0
- package/docs/adr/DOCS-001-project-docs-config.md +32 -0
- package/docs/adr/DOCS-002-adr-identity-supersession-index.md +34 -0
- package/docs/adr/DOCS-003-prd-revision-contract.md +29 -0
- package/docs/adr/DOCS-004-doc-change-step-type.md +28 -0
- package/docs/adr/DOCS-005-doc-obligations-evidence-producer.md +29 -0
- package/docs/adr/DOCS-006-reconciliation-pilot.md +50 -0
- package/docs/adr/DOCS-007-shared-doc-craft-derived-views.md +31 -0
- package/docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md +30 -0
- package/docs/doc-workflow-reconciliation.md +47 -1
- package/docs/governed-index.md +8 -0
- package/docs/platform-capabilities.md +2 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +55 -0
- package/hooks/hooks.json +1 -1
- package/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
- package/hooks/scripts/lib/doc-reconciliation-provider.js +976 -8
- package/hooks/scripts/state-persist.js +10 -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/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +976 -8
- package/plugins/harness-everything/hooks/scripts/state-persist.js +10 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +55 -0
- package/sbom.cdx.json +7 -7
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const { execFileSync } = require('child_process');
|
|
6
7
|
const { atomicWriteJson } = require('./fable-contracts');
|
|
8
|
+
const { canonical, key } = require('./workflow-isolation');
|
|
7
9
|
|
|
8
10
|
const enabled = () => process.env.HARNESS_DOC_RECONCILIATION_PILOT === '1';
|
|
11
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
9
12
|
|
|
10
13
|
function git(root, args) {
|
|
11
14
|
const env = { ...process.env };
|
|
@@ -29,11 +32,40 @@ function runtimeModule(name) {
|
|
|
29
32
|
|
|
30
33
|
// Only capture at plan/start. A later credit assessment must never advance the baseline to HEAD.
|
|
31
34
|
function captureBaseline(context) {
|
|
32
|
-
if (!enabled()
|
|
35
|
+
if (!enabled()) return;
|
|
33
36
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
if (!context.workflow.docReconciliationBaseline) {
|
|
38
|
+
const root = canonical(git(context.root, ['rev-parse', '--show-toplevel']).trim());
|
|
39
|
+
const commit = git(root, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
|
|
40
|
+
const tree = git(root, ['rev-parse', '--verify', `${commit}^{tree}`]).trim();
|
|
41
|
+
context.workflow.docReconciliationBaseline = { root, commit, tree };
|
|
42
|
+
} else if (!context.workflow.docReconciliationBaseline.tree) {
|
|
43
|
+
const pin = context.workflow.docReconciliationBaseline;
|
|
44
|
+
pin.tree = git(pin.root, ['rev-parse', '--verify', `${pin.commit}^{tree}`]).trim();
|
|
45
|
+
}
|
|
46
|
+
if (!context.workflow.docReconciliationTask) {
|
|
47
|
+
const plan = context.workflow.pendingPlan || context.workflow.workflowPlan;
|
|
48
|
+
const loaded = plan && runtimeModule('workflow-obligations').loadObligationSet(context, plan);
|
|
49
|
+
const requirementIds = loaded?.set?.requirements?.map(item => item.id);
|
|
50
|
+
if (Array.isArray(requirementIds) && requirementIds.length && requirementIds.every(id => typeof id === 'string' && id)) {
|
|
51
|
+
const baseline = context.workflow.docReconciliationBaseline;
|
|
52
|
+
const resolver = runtimeModule('project-docs-resolver');
|
|
53
|
+
const configSources = configSourcePaths(resolver).map(relative => {
|
|
54
|
+
const bytes = bytesAtBaseline(baseline.root, baseline.commit, relative);
|
|
55
|
+
return [relative, bytes === null ? null : crypto.createHash('sha256').update(bytes).digest('hex')];
|
|
56
|
+
});
|
|
57
|
+
const hash = value => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
58
|
+
const workspaceId = 'ws-' + hash({ workspaceRoot: canonical(baseline.root) });
|
|
59
|
+
context.workflow.docReconciliationTask = {
|
|
60
|
+
schema: 'harness.doc-reconciliation-task-pin.v1',
|
|
61
|
+
taskId: `task-${requirementIds.join('+')}`, requirementIds,
|
|
62
|
+
requirementDigest: hash(requirementIds), sessionId: context.sessionId,
|
|
63
|
+
workflowId: context.workflow.workflowId, workspaceId,
|
|
64
|
+
baselineCommit: baseline.commit, baselineRoot: baseline.root,
|
|
65
|
+
configDigest: hash(configSources),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
37
69
|
} catch {
|
|
38
70
|
// Absence is intentionally retained; credit assessment will fail closed.
|
|
39
71
|
}
|
|
@@ -80,8 +112,9 @@ function assertConfigScopeUnchanged(root, baseline, resolver) {
|
|
|
80
112
|
function collect(context) {
|
|
81
113
|
const baseline = context.workflow.docReconciliationBaseline;
|
|
82
114
|
if (!baseline) throw new Error('baseline-unavailable');
|
|
83
|
-
const root = git(context.root, ['rev-parse', '--show-toplevel']).trim();
|
|
84
|
-
if (
|
|
115
|
+
const root = canonical(git(context.root, ['rev-parse', '--show-toplevel']).trim());
|
|
116
|
+
if (!text(baseline.root) || key(root) !== key(baseline.root)
|
|
117
|
+
|| !/^[0-9a-f]{40,64}$/.test(baseline.commit)) throw new Error('baseline-unavailable');
|
|
85
118
|
const resolver = runtimeModule('project-docs-resolver');
|
|
86
119
|
const resolved = resolver.resolveDocsConfig(root);
|
|
87
120
|
if (!['project', 'host'].includes(resolved.status)) throw new Error('config-unavailable');
|
|
@@ -120,17 +153,30 @@ function creditChain(context, set, dependencies = {}) {
|
|
|
120
153
|
: hostId === 'claude-code' ? providerModule.readCaptureView(context.sessionDir)
|
|
121
154
|
: { records: [], tampered: false };
|
|
122
155
|
if (captureView.tampered) {
|
|
123
|
-
return { eligible: false,
|
|
156
|
+
return { eligible: false, outlineEvidence: { outcome: 'none', observation: 'unresolved', reason: 'journal-tampered' },
|
|
157
|
+
diagnostics: [{ code: 'journal-tampered', message: 'the capture journal no longer matches its immutable event records' }] };
|
|
124
158
|
}
|
|
125
159
|
if (!['claude-code', 'opencode'].includes(hostId)) {
|
|
126
|
-
return { eligible: false,
|
|
160
|
+
return { eligible: false, outlineEvidence: { outcome: 'none', observation: 'unresolved', reason: 'unsupported-host' },
|
|
161
|
+
diagnostics: [{ code: 'unsupported-host', message: `no document origin provider is registered for ${String(hostId)}` }] };
|
|
127
162
|
}
|
|
128
163
|
const { produceDocSnapshot } = require('./doc-reconciliation-producer');
|
|
129
164
|
const { acceptDocEvents } = require('./doc-reconciliation-events');
|
|
130
165
|
const { checkAcceptedDocEvidence } = require('./doc-reconciliation-core');
|
|
131
166
|
const baseline = context.workflow.docReconciliationBaseline;
|
|
132
167
|
const requirements = Array.isArray(set?.requirements) ? set.requirements : [];
|
|
133
|
-
const
|
|
168
|
+
const requirementIds = requirements.map(item => item.id);
|
|
169
|
+
const derivedTaskId = `task-${requirementIds.join('+') || 'none'}`;
|
|
170
|
+
const taskBinding = context.workflow.docReconciliationTask || null;
|
|
171
|
+
const taskId = taskBinding?.taskId || derivedTaskId;
|
|
172
|
+
if (taskBinding && (taskBinding.taskId !== derivedTaskId || taskBinding.workflowId !== context.workflow.workflowId
|
|
173
|
+
|| taskBinding.sessionId !== context.sessionId || taskBinding.baselineCommit !== baseline?.commit
|
|
174
|
+
|| !text(taskBinding.baselineRoot) || !text(baseline?.root)
|
|
175
|
+
|| key(taskBinding.baselineRoot) !== key(baseline.root)
|
|
176
|
+
|| taskBinding.requirementDigest !== crypto.createHash('sha256').update(JSON.stringify(requirementIds)).digest('hex')) ) {
|
|
177
|
+
return { eligible: false, outlineEvidence: { outcome: 'none', observation: 'unresolved', reason: 'task-binding-mismatch' },
|
|
178
|
+
diagnostics: [{ code: 'task-binding-mismatch', message: 'the requirement set or original B differs from the task pin' }] };
|
|
179
|
+
}
|
|
134
180
|
const sessionId = context.sessionId;
|
|
135
181
|
const { createClaudeOriginProvider, createOpencodeOriginProvider, journalExecutors } = providerModule;
|
|
136
182
|
const captures = captureView.records;
|
|
@@ -145,6 +191,8 @@ function creditChain(context, set, dependencies = {}) {
|
|
|
145
191
|
workflowId: context.workflow.workflowId,
|
|
146
192
|
workflowRevision: context.workflow.revision,
|
|
147
193
|
taskId,
|
|
194
|
+
sessionDir: context.sessionDir,
|
|
195
|
+
taskBinding,
|
|
148
196
|
principals,
|
|
149
197
|
baseline,
|
|
150
198
|
captures,
|
|
@@ -161,6 +209,7 @@ function creditChain(context, set, dependencies = {}) {
|
|
|
161
209
|
}, { originProvider });
|
|
162
210
|
if (produced.state !== 'ready' || !produced.snapshot) return {
|
|
163
211
|
eligible: false,
|
|
212
|
+
outlineEvidence: { outcome: 'none', observation: 'unresolved', reason: produced.diagnostics?.[0]?.code || 'snapshot-unavailable' },
|
|
164
213
|
diagnostics: produced.diagnostics || [],
|
|
165
214
|
unsatisfied: [],
|
|
166
215
|
};
|
|
@@ -171,12 +220,25 @@ function creditChain(context, set, dependencies = {}) {
|
|
|
171
220
|
const passing = new Set((checked.results || []).filter(r => r.observation === 'observed' && r.outcome === 'pass').map(r => r.obligationId));
|
|
172
221
|
const expected = new Set(checked.checkedMustIds || []);
|
|
173
222
|
const satisfied = produced.snapshot.obligations.filter(o => o.strength === 'MUST' && o.applicability !== 'N/A' && expected.has(o.id) && passing.has(o.id));
|
|
223
|
+
const outlineObligation = produced.snapshot.obligations.find(item => item.capability === 'outline-confirmation');
|
|
224
|
+
const outlineResult = outlineObligation && (checked.results || []).find(item => item.obligationId === outlineObligation.id);
|
|
225
|
+
const outlineRefs = Object.entries(accepted.acceptedIndex || {}).filter(([, item]) => item.capability === 'outline-confirmation').map(([ref]) => ref);
|
|
226
|
+
const outlineEvidence = {
|
|
227
|
+
status: outlineResult?.observation === 'observed' && outlineResult?.outcome === 'pass' ? 'pass' : 'unresolved',
|
|
228
|
+
outcome: outlineResult?.outcome || 'none',
|
|
229
|
+
observation: outlineResult?.observation || 'unresolved',
|
|
230
|
+
recordRefs: outlineResult?.recordRefs || [],
|
|
231
|
+
acceptedRefs: outlineRefs,
|
|
232
|
+
diagnostics: [...(accepted.diagnostics || []), ...(accepted.rejected || []), ...(checked.diagnostics || [])]
|
|
233
|
+
.filter(item => !item?.obligationId || item.obligationId === outlineObligation?.id),
|
|
234
|
+
};
|
|
174
235
|
return {
|
|
175
236
|
eligible: checked.eligible === true,
|
|
176
237
|
capabilities: checked.eligible === true ? [...new Set(satisfied.map(o => o.capability))] : [],
|
|
177
238
|
satisfiedObligationIds: satisfied.map(o => o.id),
|
|
178
239
|
unsatisfied: checked.unsatisfied || [],
|
|
179
240
|
acceptedRefs: Object.keys(accepted.acceptedIndex || {}),
|
|
241
|
+
outlineEvidence,
|
|
180
242
|
diagnostics: [...(accepted.diagnostics || []), ...(checked.diagnostics || [])],
|
|
181
243
|
rejected: accepted.rejected || [],
|
|
182
244
|
};
|
|
@@ -232,6 +294,45 @@ function readCaptures(context) {
|
|
|
232
294
|
const docCheckCandidate = item => item.event === 'PostToolUse'
|
|
233
295
|
&& require('./doc-reconciliation-provider').resolveDocCheckInvocation(item.command, item.cwd).kind !== 'not-candidate';
|
|
234
296
|
|
|
297
|
+
function outlineReviewCaptureRefs(sessionDir, taskId) {
|
|
298
|
+
const refs = new Set();
|
|
299
|
+
if (!text(taskId)) return refs;
|
|
300
|
+
const attempts = path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', taskId, 'attempts');
|
|
301
|
+
try {
|
|
302
|
+
for (const entry of fs.readdirSync(attempts, { withFileTypes: true })) {
|
|
303
|
+
if (!entry.isDirectory()) continue;
|
|
304
|
+
try {
|
|
305
|
+
const handoff = JSON.parse(fs.readFileSync(path.join(attempts, entry.name, 'handoff.json'), 'utf8'));
|
|
306
|
+
if (handoff.taskId === taskId && typeof handoff.reviewCaptureRef === 'string') refs.add(handoff.reviewCaptureRef);
|
|
307
|
+
} catch { /* malformed handoff cannot add review credit */ }
|
|
308
|
+
}
|
|
309
|
+
} catch { /* no attempts yet */ }
|
|
310
|
+
return refs;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function outlineDispatchReferences(captures, taskId, sessionDir) {
|
|
314
|
+
if (!text(taskId)) return [];
|
|
315
|
+
const candidates = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
|
|
316
|
+
&& item.event === 'PreToolUse' && ['Task', 'Agent'].includes(item.tool_name)
|
|
317
|
+
&& text(item.ref) && item.outlineDispatch?.taskId === taskId && text(item.outlineDispatch.attemptId));
|
|
318
|
+
if (!candidates.length) return [];
|
|
319
|
+
if (text(sessionDir)) {
|
|
320
|
+
const provider = require('./doc-reconciliation-provider');
|
|
321
|
+
const attemptIds = [...new Set(candidates.map(item => item.outlineDispatch.attemptId))];
|
|
322
|
+
const resolved = attemptIds.map(attemptId => ({ attemptId,
|
|
323
|
+
attempt: provider._internals.resolveOutlineAttempt({ sessionDir, taskId, attemptId }) }));
|
|
324
|
+
const current = resolved.filter(item => item.attempt.state === 'accepted');
|
|
325
|
+
if (current.length === 1) {
|
|
326
|
+
const dispatches = candidates.filter(item => item.outlineDispatch.attemptId === current[0].attemptId);
|
|
327
|
+
return dispatches.length ? [`${dispatches[0].ref}#outline-confirmation`] : [];
|
|
328
|
+
}
|
|
329
|
+
if (current.length > 1 || attemptIds.length > 1) return [];
|
|
330
|
+
}
|
|
331
|
+
const tuples = [...new Set(candidates.map(item => `${item.outlineDispatch.attemptId}\0${item.tool_use_id || ''}\0${item.outlineDispatch.dispatchInputDigest || ''}`))];
|
|
332
|
+
if (tuples.length > 1 && !candidates.some(item => item.outlineDispatch.state === 'unresolved')) return [];
|
|
333
|
+
return [`${candidates[0].ref}#outline-confirmation`];
|
|
334
|
+
}
|
|
335
|
+
|
|
235
336
|
function captureReferences(context) {
|
|
236
337
|
const captures = readCaptures(context);
|
|
237
338
|
if (activeHostFor(context) === 'opencode') {
|
|
@@ -241,7 +342,10 @@ function captureReferences(context) {
|
|
|
241
342
|
const candidates = captures
|
|
242
343
|
.filter(item => item && typeof item.ref === 'string' && (item.event === 'SubagentStop' || docCheckCandidate(item)))
|
|
243
344
|
.filter(item => item.event !== 'SubagentStop');
|
|
244
|
-
const
|
|
345
|
+
const taskId = context.workflow.docReconciliationTask?.taskId;
|
|
346
|
+
const outlineReviews = outlineReviewCaptureRefs(context.sessionDir, taskId);
|
|
347
|
+
const reviews = captures.filter(item => item && typeof item.ref === 'string' && item.event === 'SubagentStop'
|
|
348
|
+
&& !outlineReviews.has(item.ref));
|
|
245
349
|
const references = [];
|
|
246
350
|
for (const [index, capability] of ['doc-lint', 'link-check'].entries()) {
|
|
247
351
|
const ordered = index ? candidates.slice(1).concat(candidates.slice(0, 1)) : candidates;
|
|
@@ -250,6 +354,7 @@ function captureReferences(context) {
|
|
|
250
354
|
const provider = require('./doc-reconciliation-provider');
|
|
251
355
|
references.push(...candidates.filter(item => provider.resolveDocCheckInvocation(item.command, item.cwd).conflictAssessment === true)
|
|
252
356
|
.map(item => `${item.ref}#conflict-check`));
|
|
357
|
+
references.push(...outlineDispatchReferences(captures, taskId, context.sessionDir));
|
|
253
358
|
// The adapter accepts one event per capability; rotating candidate order lets separate valid runs fill each capability.
|
|
254
359
|
return [...references, ...reviews.map(item => `${item.ref}#quality-review`)];
|
|
255
360
|
}
|
|
@@ -298,6 +403,7 @@ function assess(context, set, dependencies = {}) {
|
|
|
298
403
|
const report = {
|
|
299
404
|
schemaVersion: 1, reportOrigin: 'runtime', pilotStatus: 'unavailable', pilotCredit: false,
|
|
300
405
|
identity: null, expectedMustIds: [], results: [], unsatisfied: [], diagnostics: [],
|
|
406
|
+
outlineEvidence: { status: 'unresolved', outcome: 'none', observation: 'unresolved', acceptedRefs: [], recordRefs: [], diagnostics: [] },
|
|
301
407
|
};
|
|
302
408
|
const supported = set?.stepBindingsEnabled === true && !String(context.workflow.strategy || '').startsWith('fable-');
|
|
303
409
|
let withhold = false;
|
|
@@ -313,10 +419,17 @@ function assess(context, set, dependencies = {}) {
|
|
|
313
419
|
reasonCode: item.applicability === 'unknown' ? 'unknown-applicability' : 'producer-unavailable',
|
|
314
420
|
}));
|
|
315
421
|
if (!withhold) report.diagnostics.push('trusted-non-document-scope');
|
|
316
|
-
|
|
422
|
+
const hasOutlineCapture = captureReferences(context).some(reference => reference.endsWith('#outline-confirmation'));
|
|
423
|
+
if (withhold || hasOutlineCapture) {
|
|
317
424
|
let chain;
|
|
318
425
|
try { chain = creditChain(context, set, dependencies); }
|
|
319
|
-
catch (error) { chain = { eligible: false,
|
|
426
|
+
catch (error) { chain = { eligible: false, outlineEvidence: { status: 'unresolved', outcome: 'none',
|
|
427
|
+
observation: 'unresolved', reason: 'collector-unavailable', acceptedRefs: [], recordRefs: [], diagnostics: [] },
|
|
428
|
+
diagnostics: [{ code: 'collector-unavailable', message: String(error.message) }] }; }
|
|
429
|
+
report.outlineEvidence = chain.outlineEvidence || report.outlineEvidence;
|
|
430
|
+
report.acceptedRefs = chain.acceptedRefs || [];
|
|
431
|
+
report.diagnostics.push(...(chain.diagnostics || []));
|
|
432
|
+
if (withhold) {
|
|
320
433
|
// The unsatisfied list is rewritten from the chain: a MUST that the chain did not satisfy stays listed (F3).
|
|
321
434
|
const satisfied = new Set(chain.satisfiedObligationIds || []);
|
|
322
435
|
report.unsatisfied = [
|
|
@@ -327,12 +440,11 @@ function assess(context, set, dependencies = {}) {
|
|
|
327
440
|
...(chain.unsatisfied || []),
|
|
328
441
|
];
|
|
329
442
|
report.creditedCapabilities = chain.eligible === true ? chain.capabilities : [];
|
|
330
|
-
report.acceptedRefs = chain.acceptedRefs || [];
|
|
331
|
-
report.diagnostics.push(...(chain.diagnostics || []));
|
|
332
443
|
// Credit needs an empty unsatisfied list across both sources and at least one satisfied capability.
|
|
333
444
|
withhold = !(chain.eligible === true && report.unsatisfied.length === 0 && report.creditedCapabilities.length > 0);
|
|
334
445
|
report.pilotCredit = !withhold;
|
|
335
446
|
report.pilotStatus = report.pilotCredit ? 'credited' : 'unresolved';
|
|
447
|
+
}
|
|
336
448
|
}
|
|
337
449
|
} catch (error) {
|
|
338
450
|
withhold = true;
|
|
@@ -351,4 +463,5 @@ function assess(context, set, dependencies = {}) {
|
|
|
351
463
|
return { withhold, report };
|
|
352
464
|
}
|
|
353
465
|
|
|
354
|
-
module.exports = { enabled, captureBaseline, assess, reviewRequired, independentReviewAccepted
|
|
466
|
+
module.exports = { enabled, captureBaseline, assess, reviewRequired, independentReviewAccepted,
|
|
467
|
+
_internals: { captureReferences, outlineDispatchReferences, outlineReviewCaptureRefs } };
|