harness-everything 0.30.9 → 0.31.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/AGENTS.md +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +8 -9
- package/VERIFICATION.md +1 -1
- package/benchmarks/results/live-host/claude-2026-10-08-issue-297/README.md +27 -0
- package/benchmarks/results/live-host/codex-2026-10-13/README.md +55 -0
- package/benchmarks/results/live-host/codex-2026-10-13/observation-records.json +88 -0
- package/benchmarks/results/pr-303-review-2026-10-13/README.md +81 -0
- package/benchmarks/results/pr-303-review-2026-10-13/ci/run.json +1330 -0
- package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/mechanism-35-clean-env.log +1 -0
- package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/mechanism-35-inherited-flag.log +34 -0
- package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate-captured.json +46 -0
- package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate-diagnosis.md +13 -0
- package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate.json +46 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/summary.json +57 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-collision.log +8 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-consistency.log +624 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-docs-capabilities.log +131 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-platform-compatibility.log +9 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-plugin-openai.log +6 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-plugin-submission.log +5 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-references.log +6 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-release.log +73 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-repo-contract.log +5 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-routing-invariants.log +785 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-routing-skills.log +44 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/summary.json +12 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/test-consistency.log +624 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/test-release.log +73 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/summary.json +79 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test-mechanism.log +1737 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test-mutations.log +16 -0
- package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test.log +3297 -0
- package/benchmarks/results/pr-303-review-2026-10-13/manifest.json +319 -0
- package/benchmarks/results/pr-303-review-2026-10-13/regression/binding-resolution-green.log +36 -0
- package/benchmarks/results/pr-303-review-2026-10-13/regression/determinism.json +174 -0
- package/benchmarks/results/pr-303-review-2026-10-13/regression/quality-evidence.json +314 -0
- package/benchmarks/results/pr-303-review-2026-10-13/regression/quality-report.json +638 -0
- package/benchmarks/results/pr-303-review-2026-10-13/regression/red.log +240 -0
- package/benchmarks/results/pr-303-review-2026-10-13/regression/untyped-fable-probe.json +22 -0
- package/benchmarks/results/pr-303-review-2026-10-13/regression/workflow-obligations-green.log +254 -0
- package/benchmarks/results/pr-303-review-2026-10-13/verification/mechanism-35-clean-env-fixed.log +1 -0
- package/benchmarks/results/pr-303-review-2026-10-13/verification/mechanism-35-inherited-flag-fixed.log +1 -0
- package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.meta.json +8 -0
- package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.stderr.log +90 -0
- package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.stdout.log +3186 -0
- package/benchmarks/results/pr-303-review-2026-10-13/verification/verify-gate-final.json +46 -0
- package/bin/cli.js +1 -1
- package/ci/doc-capability-consistency.test.js +11 -1
- package/ci/invariant-routing.test.js +7 -4
- package/ci/mechanism-2ag-router-worktree-contract.test.js +2 -2
- package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +2 -1
- package/ci/mechanism-2al-codex-hooks-compat.test.js +19 -0
- package/ci/mechanism-2am-observations-session-import.test.js +221 -0
- package/ci/mechanism-2an-workflow-obligations.test.js +352 -3
- package/ci/mechanism-2ao-plugin-sync.test.js +129 -4
- package/ci/mechanism-2aq-fable-stage-bindings.test.js +168 -0
- package/ci/mechanism-2ar-binding-resolution.test.js +211 -0
- package/ci/mechanism-2w-platform-compatibility.test.js +12 -0
- package/ci/mechanism-34-observations.test.js +64 -0
- package/ci/mechanism-35-verification-contract.test.js +1 -1
- package/ci/mutation-check.js +2 -0
- package/ci/mutation-loader.js +4 -0
- package/ci/openai-plugin-package.test.js +7 -5
- package/ci/prompt-contract-regressions.test.js +7 -6
- package/ci/router-workflow-plan.test.js +109 -14
- package/ci/skill-routing-check.js +44 -15
- package/ci/workflow-plan-consumers.test.js +41 -1
- package/docs/architecture.md +10 -10
- package/docs/enforcement-locks.md +1 -1
- package/docs/mechanism-first-skill-mesh.md +12 -11
- package/docs/openai-plugin.md +1 -1
- package/docs/philosophy.md +7 -6
- package/docs/platform-capabilities.md +14 -12
- package/docs/platform-compatibility.json +5 -5
- package/docs/routing.md +16 -17
- package/docs/system-one-observations.md +36 -9
- package/docs/system-one-routing.md +10 -7
- package/docs/system-one-suggestion-gates.md +5 -5
- package/docs/workflow-runtime.md +6 -2
- package/docs/workflows/harness-everything.md +14 -14
- package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
- package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
- package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
- package/fable-mode/CONTRACT-FORMAT.md +4 -1
- package/fable-mode/SKILL.md +6 -8
- package/fable-mode/agents/fable-orchestrator.md +10 -3
- package/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/fable-mode/scripts/workflow-plan-consumer.js +101 -2
- package/harness-everything/SKILL.md +8 -7
- package/harness-everything/references/router-workflow-plan.md +11 -6
- package/harness-everything/references/triage-and-tiers.md +21 -23
- package/harness-everything/schemas/router-workflow-plan.schema.json +2 -0
- package/harness-everything/scripts/kernel-router-core.js +52 -55
- package/harness-everything/scripts/kernel-router.js +48 -5
- package/harness-everything/scripts/router-contract-core.js +7 -7
- package/harness-everything/scripts/router-contract.js +1 -3
- package/harness-everything/scripts/routing-keywords.json +417 -121
- package/harness-everything/scripts/tier-router.js +64 -113
- package/hooks/scripts/contract-test.js +29 -3
- package/hooks/scripts/lib/binding-resolver.js +76 -0
- package/hooks/scripts/lib/fable-contracts.js +1 -1
- package/hooks/scripts/lib/observations.js +88 -12
- package/hooks/scripts/lib/workflow-obligations.js +292 -7
- package/hooks/scripts/lib/workflow-runtime.js +8 -1
- package/hooks/scripts/workflow-disposition.js +116 -2
- 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/contract-test.js +29 -3
- package/plugins/harness-everything/hooks/scripts/lib/binding-resolver.js +76 -0
- package/plugins/harness-everything/hooks/scripts/lib/fable-contracts.js +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/observations.js +88 -12
- package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +292 -7
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +8 -1
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +116 -2
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/advisory-text.js +2 -2
- package/plugins/harness-everything/scripts/lib/generated-skills.js +78 -0
- package/plugins/harness-everything/skills/fable-mode/CONTRACT-FORMAT.md +4 -1
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +6 -8
- package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +10 -3
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +101 -2
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +8 -7
- package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +11 -6
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +21 -23
- package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +2 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +52 -55
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +48 -5
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +7 -7
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +1 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +417 -121
- package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +64 -113
- package/sbom.cdx.json +7 -7
- package/scripts/codex-hooks-compat.js +39 -14
- package/scripts/lib/advisory-text.js +2 -2
- package/scripts/lib/generated-skills.js +78 -0
- package/scripts/plugin-sync.js +100 -29
- package/scripts/system-one-observations-export.js +29 -2
- package/scripts/system-one-observations-import-codex-sessions.js +307 -0
- package/scripts/system-one-observations-review.js +11 -1
|
@@ -5,11 +5,11 @@ const {
|
|
|
5
5
|
buildRouterContract,
|
|
6
6
|
writeRouterContract,
|
|
7
7
|
} = require('./router-contract');
|
|
8
|
-
const { requireWorkspace } = require('./runtime-paths');
|
|
8
|
+
const { getHarnessRoot, requireWorkspace } = require('./runtime-paths');
|
|
9
9
|
|
|
10
10
|
const DEFAULT_ROUTING_CONFIG = {
|
|
11
11
|
tiers: { tier3: [], tier2: [] },
|
|
12
|
-
|
|
12
|
+
inputSignalGroups: [],
|
|
13
13
|
factAudit: { externalClaim: [], estimate: [] },
|
|
14
14
|
};
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ function loadRoutingConfig() {
|
|
|
21
21
|
return {
|
|
22
22
|
config: {
|
|
23
23
|
tiers: parsed.tiers || DEFAULT_ROUTING_CONFIG.tiers,
|
|
24
|
-
|
|
24
|
+
inputSignalGroups: Array.isArray(parsed.inputSignalGroups) ? parsed.inputSignalGroups : DEFAULT_ROUTING_CONFIG.inputSignalGroups,
|
|
25
25
|
factAudit: parsed.factAudit || DEFAULT_ROUTING_CONFIG.factAudit,
|
|
26
26
|
},
|
|
27
27
|
routingStatus: 'ok',
|
|
@@ -60,15 +60,25 @@ function detectFableModel(prompt) {
|
|
|
60
60
|
// retains execution routing; format field lists are recognized separately.
|
|
61
61
|
function containsOnlyLookupTerms(text, operation) {
|
|
62
62
|
const terms = {
|
|
63
|
-
explanation: /\b(?:please|help|me|explain|resolve|define|what|is|does|mean|means|meaning|the|a|an|of|to|into|in|for|behavior|behaviour|profile|profiles|selection|json|record|alias|role|runtime|model|effort|floor|fableprofile)\b/gi,
|
|
64
|
-
output: /\b(?:return|respond|output|show|provide|only|a|an|small|compact|json|record|selection|profile|result|with|fields|containing|include|including)\b/gi,
|
|
63
|
+
explanation: /\b(?:please|help|me|could|would|you|explain|resolve|define|what|is|does|mean|means|meaning|the|a|an|of|to|into|in|for|as|compact|small|only|behavior|behaviour|profile|profiles|selection|json|record|object|alias|role|runtime|model|effort|floor|fableprofile)\b/gi,
|
|
64
|
+
output: /\b(?:as|return|respond|output|show|provide|only|a|an|small|compact|short|json|record|object|selection|profile|result|with|fields|containing|include|including)\b/gi,
|
|
65
65
|
};
|
|
66
66
|
const chineseTerms = {
|
|
67
|
-
explanation: /(
|
|
67
|
+
explanation: /(?:請問|麻煩|請|幫我|解釋|說明|解析|的意思|意思|含義|意義|是什麼|設定檔|行為|角色|精簡|簡短|緊湊|JSON|紀錄|記錄)/g,
|
|
68
68
|
output: /(?:只|僅|回傳|返回|輸出|顯示|提供|包含|含|的|欄位|記錄|紀錄|結果|設定檔)/g,
|
|
69
69
|
};
|
|
70
70
|
return text.replace(PROFILE_LOOKUP_FIELDS, '').replace(terms[operation], '')
|
|
71
|
-
.replace(chineseTerms[operation], '').replace(/[\s"'`“”‘’?::?]/g, '') === '';
|
|
71
|
+
.replace(chineseTerms[operation], '').replace(/[.!?]+$/g, '').replace(/[\s"'`“”‘’?::?]/g, '') === '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function splitProfileLookupClauses(prompt) {
|
|
75
|
+
let text = String(prompt || '').trim()
|
|
76
|
+
.replace(/^(?:could\s+you(?:\s+please)?|would\s+you(?:\s+please)?|please)\s*,?\s+/i, '')
|
|
77
|
+
.replace(/^(?:麻煩請|麻煩|請問|請)\s*/u, '');
|
|
78
|
+
const outputLead = String.raw`(?:please\s+)?(?:return|respond|output|show|provide|no|without|do\s+not|don't)\b|(?:請(?:問)?\s*)?(?:只|僅|回傳|返回|輸出|顯示|提供|不要|不需要|無需|無須|不必|毋須|不用|不會|不)`;
|
|
79
|
+
text = text.replace(new RegExp(String.raw`\.(?=\s*${outputLead})|[—–](?=\s*(?:${outputLead}|as\s+(?:a\s+)?(?:compact\s+|small\s+|short\s+)?json\s+(?:record|object)\b))`, 'giu'), ';');
|
|
80
|
+
return text.split(/(?:\.{3,}|[;!??!,,;。、…]|\b(?:and\s+then|and|then|also|plus|afterwards|additionally)\b|(?:並且?|以及|接著|然後|另外))/iu)
|
|
81
|
+
.map(part => part.trim().replace(/[.!?…]+$/g, '')).filter(Boolean);
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
// A negation scopes only its own noun/verb list. Vocabulary-only matching would
|
|
@@ -80,11 +90,12 @@ const NEGATED_ITEM = '(?:orchestration|delegation|stages?|staging|execution|mode
|
|
|
80
90
|
const NEGATED_ACTION = '(?:switch(?:ing)?\\s+(?:the\\s+)?models?|delegate|orchestrate|stage|execute)';
|
|
81
91
|
const ENGLISH_NEGATION = new RegExp(`^(?:(?:no|without)\\s+${negatedList(NEGATED_ITEM)}|(?:do not|don't)\\s+${negatedList(NEGATED_ACTION)})$`, 'i');
|
|
82
92
|
const ENGLISH_NEGATION_CONTINUATION = new RegExp(`^(?:or\\s+)?${negatedList(NEGATED_ITEM)}$`, 'i');
|
|
83
|
-
const
|
|
93
|
+
const CHINESE_NEGATION_PREFIX = /^(?:不要|不需要|無需|無須|不必|毋須|不用|不會|不)/;
|
|
94
|
+
const CHINESE_NEGATION_TERMS = /(?:不要|不需要|無需|無須|不必|毋須|不用|不會|不|或|也|任何|一切|啟動|開始|開啟|進入|執行|進行|運行|啟用|切換|模型|階段|流程|任務|操作|編排|委派)/g;
|
|
84
95
|
|
|
85
96
|
function isBoundedNegation(clause, continuation) {
|
|
86
|
-
const text = clause.replace(/’/g, "'").replace(/[“”"`?::?]/g, '').replace(/\s+/g, ' ').trim();
|
|
87
|
-
if (
|
|
97
|
+
const text = clause.replace(/’/g, "'").replace(/[.!?…]+$/g, '').replace(/[“”"`?::?]/g, '').replace(/\s+/g, ' ').trim();
|
|
98
|
+
if (CHINESE_NEGATION_PREFIX.test(text)) {
|
|
88
99
|
return !continuation && text.replace(CHINESE_NEGATION_TERMS, '').replace(/\s/g, '') === '';
|
|
89
100
|
}
|
|
90
101
|
return (continuation ? ENGLISH_NEGATION_CONTINUATION : ENGLISH_NEGATION).test(text);
|
|
@@ -94,8 +105,7 @@ function isFableProfileLookup(prompt, hasMacroSignal) {
|
|
|
94
105
|
if (!detectFableModel(prompt) || hasMacroSignal) return false;
|
|
95
106
|
if (/\b(?:direct[- ]single|iterative[- ]single|fable[- ](?:staged|parallel|multi[- ]agent[- ]workspace))\b/i.test(prompt)) return false;
|
|
96
107
|
|
|
97
|
-
const clauses = prompt
|
|
98
|
-
.split(/(?:[;,.,;。、]|\b(?:and|then|also|plus|afterwards|additionally)\b|(?:並且?|以及|接著|然後|另外))/i)
|
|
108
|
+
const clauses = splitProfileLookupClauses(prompt)
|
|
99
109
|
.map(part => part.trim())
|
|
100
110
|
.filter(Boolean);
|
|
101
111
|
if (clauses.length === 0) return false;
|
|
@@ -105,14 +115,15 @@ function isFableProfileLookup(prompt, hasMacroSignal) {
|
|
|
105
115
|
const normalizedLookup = lookupClause.replace(/[“”]/g, '"').replace(/[‘’]/g, "'")
|
|
106
116
|
.replace(quotedInvocation, 'fableprofile')
|
|
107
117
|
.replace(new RegExp(FABLE_PROFILE_INVOCATION.source, 'gi'), 'fableprofile');
|
|
108
|
-
const isLookupLead = /^(?:(?:please|help me)\s+|(
|
|
118
|
+
const isLookupLead = /^(?:(?:please|help me|could you(?: please)?|would you(?: please)?)\s+|(?:請問|麻煩請|麻煩|請|幫我)\s*)?(?:explain|resolve|define|what (?:is|does)|解釋|說明|解析)/i.test(lookupClause)
|
|
109
119
|
&& Boolean(detectFableModel(lookupClause));
|
|
110
|
-
|
|
120
|
+
const normalizedFormatRequest = normalizedLookup.replace(/\bas\s+(?:a\s+)?(?:compact|small|short)?\s*json\s+(?:record|object)\b/gi, '');
|
|
121
|
+
if (!isLookupLead || !containsOnlyLookupTerms(normalizedFormatRequest, 'explanation')) return false;
|
|
111
122
|
|
|
112
123
|
let continuationOperation = null;
|
|
113
124
|
for (const clause of continuations) {
|
|
114
|
-
const output = /^(?:please\s+)?(?:return|respond|output|show|provide)\b|^(?:請\s*)?(?:只|僅)?(?:回傳|返回|輸出|顯示|提供)/i.test(clause);
|
|
115
|
-
const negative = /^(?:please\s+)?(?:no|without|do not|don't)\b|^(?:請\s*)?(
|
|
125
|
+
const output = /^(?:please\s+)?(?:return|respond|output|show|provide)\b|^as\s+(?:a\s+)?(?:compact\s+|small\s+|short\s+)?json\s+(?:record|object)\b|^(?:請\s*)?(?:只|僅)?(?:回傳|返回|輸出|顯示|提供)/i.test(clause);
|
|
126
|
+
const negative = /^(?:please\s+)?(?:no|without|do not|don't)\b|^(?:請\s*)?(?:不要|不需要|無需|無須|不必|毋須|不用|不會|不)/i.test(clause);
|
|
116
127
|
if (output || negative) {
|
|
117
128
|
continuationOperation = output ? 'output' : 'negative';
|
|
118
129
|
const normalizedContinuation = clause.replace(/^(?:please\s+|請\s*)/i, '');
|
|
@@ -184,16 +195,6 @@ function addReason(reasonCodes, code) {
|
|
|
184
195
|
if (code && !reasonCodes.includes(code)) reasonCodes.push(code);
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
function deduplicateGuidesByPath(guides) {
|
|
188
|
-
const byPath = new Map();
|
|
189
|
-
for (const guide of guides) {
|
|
190
|
-
const match = String(guide).match(/^\s*-\s+([^\s]+)/);
|
|
191
|
-
const key = match ? match[1].replace(/\\/g, '/') : String(guide).trim();
|
|
192
|
-
if (!byPath.has(key)) byPath.set(key, guide);
|
|
193
|
-
}
|
|
194
|
-
return Array.from(byPath.values());
|
|
195
|
-
}
|
|
196
|
-
|
|
197
198
|
function countDomainSignals(promptLower) {
|
|
198
199
|
const domains = [
|
|
199
200
|
/\bsecurity\b|安全|資安/i,
|
|
@@ -238,73 +239,17 @@ function plannerInputsFromContext(context, promptLower) {
|
|
|
238
239
|
return { hostCapabilities, constraints };
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
|
|
242
|
+
// Self-evolved skills are discovered from manifest metadata only: matching ids
|
|
243
|
+
// become step inputs, and the SKILL.md path resolves when a declaring step is
|
|
244
|
+
// active. Discovery is advisory and never breaks routing.
|
|
245
|
+
function matchGeneratedSkillIds(promptLower, context) {
|
|
242
246
|
try {
|
|
243
|
-
const
|
|
247
|
+
const generated = require(path.join(getHarnessRoot(), 'scripts', 'lib', 'generated-skills.js'));
|
|
244
248
|
const { getWorkspaceRoot } = requireWorkspace();
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
path.join(workspaceRoot, '.cursor', 'harness-everything', 'manifest.json'),
|
|
250
|
-
path.join(workspaceRoot, '.github', 'harness-everything', 'manifest.json'),
|
|
251
|
-
path.join(workspaceRoot, '.codex', 'harness-everything', 'manifest.json'),
|
|
252
|
-
path.join(workspaceRoot, '.continue', 'harness-everything', 'manifest.json'),
|
|
253
|
-
path.join(userHome, '.agents', 'harness-everything', 'manifest.json'),
|
|
254
|
-
path.join(userHome, '.claude', 'harness-everything', 'manifest.json'),
|
|
255
|
-
];
|
|
256
|
-
|
|
257
|
-
const generatedSkills = new Map();
|
|
258
|
-
for (const manifestPath of manifestPaths) {
|
|
259
|
-
if (!fs.existsSync(manifestPath)) continue;
|
|
260
|
-
try {
|
|
261
|
-
const manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
262
|
-
if (Array.isArray(manifestData.generated)) {
|
|
263
|
-
for (const skill of manifestData.generated) generatedSkills.set(skill.id, skill);
|
|
264
|
-
}
|
|
265
|
-
} catch (err) {
|
|
266
|
-
// Ignore malformed generated-skill manifests; they are advisory only.
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const allDynamicSkills = Array.from(generatedSkills.values());
|
|
271
|
-
const matchedDynamicSkills = [];
|
|
272
|
-
|
|
273
|
-
for (const skill of allDynamicSkills) {
|
|
274
|
-
let score = 0;
|
|
275
|
-
for (const rawTrigger of skill.triggers || []) {
|
|
276
|
-
const trigger = rawTrigger.toLowerCase().trim();
|
|
277
|
-
if (!trigger || trigger.length < 2) continue;
|
|
278
|
-
if (['the', 'and', 'for', 'with', 'your', 'this', 'that', 'some', 'from', 'prevent', 'resolved'].includes(trigger)) continue;
|
|
279
|
-
|
|
280
|
-
if (/[\u4e00-\u9fa5]/.test(trigger)) {
|
|
281
|
-
if (promptLower.includes(trigger)) score += trigger.length * 2;
|
|
282
|
-
} else {
|
|
283
|
-
const escapedTrigger = trigger.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
284
|
-
const regex = new RegExp(`\\b${escapedTrigger}\\b`, 'i');
|
|
285
|
-
if (regex.test(promptLower)) score += trigger.length >= 4 ? 3 : 1.5;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (score >= 3) matchedDynamicSkills.push({ skill, score });
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (matchedDynamicSkills.length > 0) {
|
|
292
|
-
matchedDynamicSkills.sort((a, b) => b.score - a.score);
|
|
293
|
-
console.log(`\n=> 🎯 HIGH-RELEVANCE SELF-EVOLVED SKILL(S) DETECTED:`);
|
|
294
|
-
matchedDynamicSkills.forEach(({ skill }) => {
|
|
295
|
-
let displayPath = skill.dirPath;
|
|
296
|
-
if (displayPath.startsWith(workspaceRoot)) displayPath = path.relative(workspaceRoot, displayPath);
|
|
297
|
-
console.log(`- ${displayPath}/SKILL.md (${skill.description} [DYNAMIC SKILL])`);
|
|
298
|
-
});
|
|
299
|
-
} else if (recommendedGuides.length === 0 && allDynamicSkills.length > 0) {
|
|
300
|
-
console.log(`\n=> ℹ️ UNMATCHED PROMPT HINT:`);
|
|
301
|
-
console.log(`No standard or dynamic skills matched your prompt directly.`);
|
|
302
|
-
console.log(`However, you have ${allDynamicSkills.length} self-evolved dynamic skill(s) registered in your manifest.json.`);
|
|
303
|
-
console.log(`To ensure you don't miss past lessons, you should inspect the "generated" section of your manifest.json or check .claude/harness-everything/skills/generated/ to see if any apply to your current task.`);
|
|
304
|
-
console.log(`If genuinely nothing covers this - including nothing already fetched via find-skills - load find-skills/SKILL.md: it checks "npx skills list" for anything already installed, then searches skills.sh/npx skills if not, and always requires explicit approval before installing anything.`);
|
|
305
|
-
}
|
|
306
|
-
} catch (err) {
|
|
307
|
-
// Dynamic-skill discovery is advisory and must never break core routing.
|
|
249
|
+
const skills = generated.readGeneratedSkills(getWorkspaceRoot(context));
|
|
250
|
+
return { registered: skills.size, matched: generated.matchGeneratedSkills(promptLower, skills) };
|
|
251
|
+
} catch (_) {
|
|
252
|
+
return { registered: 0, matched: [] };
|
|
308
253
|
}
|
|
309
254
|
}
|
|
310
255
|
|
|
@@ -427,34 +372,26 @@ function run(userPrompt, context, options = {}) {
|
|
|
427
372
|
}
|
|
428
373
|
}
|
|
429
374
|
|
|
430
|
-
const
|
|
431
|
-
|
|
375
|
+
const knowledgeSignals = [];
|
|
376
|
+
const candidateBindings = [];
|
|
377
|
+
for (const group of profileLookup ? [] : routingConfig.inputSignalGroups) {
|
|
432
378
|
let matched = false;
|
|
433
379
|
if (typeof group.regex === 'string') {
|
|
434
380
|
matched = new RegExp(group.regex, 'i').test(promptLower);
|
|
435
381
|
} else if (Array.isArray(group.keywords)) {
|
|
436
|
-
matched = group.keywords.some(keyword => promptLower
|
|
382
|
+
matched = group.keywords.some(keyword => matchKeyword(promptLower, keyword));
|
|
383
|
+
}
|
|
384
|
+
if (matched && typeof group.id === 'string') {
|
|
385
|
+
knowledgeSignals.push(group.id);
|
|
386
|
+
if (Array.isArray(group.bindings)) candidateBindings.push(...group.bindings.filter(id => typeof id === 'string'));
|
|
437
387
|
}
|
|
438
|
-
if (matched && Array.isArray(group.guides)) allRecommendedGuides.push(...group.guides);
|
|
439
388
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
console.log(`\n=> RECOMMENDED KNOWLEDGE GUIDES (Auto-loaded based on keywords):`);
|
|
444
|
-
recommendedGuides.forEach(guide => {
|
|
445
|
-
const match = guide.match(/^- ([^\s]+)/);
|
|
446
|
-
if (match) {
|
|
447
|
-
const guidePath = path.join(__dirname, '..', '..', match[1]);
|
|
448
|
-
if (!fs.existsSync(guidePath)) {
|
|
449
|
-
console.log(`${guide} [NOT INSTALLED - Ignore this recommendation]`);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
console.log(guide);
|
|
454
|
-
});
|
|
389
|
+
if (memoryPersistenceRequested && !knowledgeSignals.includes('memory-persistence')) {
|
|
390
|
+
knowledgeSignals.push('memory-persistence');
|
|
391
|
+
candidateBindings.push('self-evolve');
|
|
455
392
|
}
|
|
456
|
-
|
|
457
|
-
if (
|
|
393
|
+
const generatedSkills = profileLookup ? { registered: 0, matched: [] } : matchGeneratedSkillIds(promptLower, context);
|
|
394
|
+
if (generatedSkills.matched.length > 0) knowledgeSignals.push('self-evolved-skill');
|
|
458
395
|
|
|
459
396
|
const externalClaimTriggers = routingConfig.factAudit.externalClaim || [];
|
|
460
397
|
const estimateTriggers = routingConfig.factAudit.estimate || [];
|
|
@@ -495,6 +432,7 @@ function run(userPrompt, context, options = {}) {
|
|
|
495
432
|
rationale,
|
|
496
433
|
reasonCodes,
|
|
497
434
|
requestedFableModel,
|
|
435
|
+
knowledgeSignals,
|
|
498
436
|
explicitRequest: {
|
|
499
437
|
fableModel: requestedFableModel,
|
|
500
438
|
strategy: requestedStrategy,
|
|
@@ -528,6 +466,19 @@ function run(userPrompt, context, options = {}) {
|
|
|
528
466
|
},
|
|
529
467
|
});
|
|
530
468
|
|
|
469
|
+
if (knowledgeSignals.length > 0) {
|
|
470
|
+
console.log(`\n=> KNOWLEDGE SIGNALS (STEP INPUT ONLY): ${knowledgeSignals.join(', ')}`);
|
|
471
|
+
console.log(' These normalized prompt signals help compose requirement steps; they do not select or load documents.');
|
|
472
|
+
const bindingIds = [...new Set(candidateBindings)];
|
|
473
|
+
if (bindingIds.length > 0) console.log(` Candidate step binding ids: ${bindingIds.join(', ')}`);
|
|
474
|
+
}
|
|
475
|
+
if (generatedSkills.matched.length > 0) {
|
|
476
|
+
console.log(`\n=> SELF-EVOLVED SKILL SIGNALS (STEP INPUT ONLY): ${generatedSkills.matched.join(', ')}`);
|
|
477
|
+
console.log(' Declare a matching id as a step binding; its SKILL.md resolves from the manifest only when that step is active.');
|
|
478
|
+
} else if (generatedSkills.registered > 0 && knowledgeSignals.length === 0) {
|
|
479
|
+
console.log(`\n=> SELF-EVOLVED SKILLS REGISTERED: ${generatedSkills.registered} (none matched this prompt; inspect manifest "generated" metadata if a past lesson may apply).`);
|
|
480
|
+
}
|
|
481
|
+
|
|
531
482
|
console.log(`\n=> WORKFLOW STRATEGY: ${contract.workflowPlan.strategy || 'deferred'}`);
|
|
532
483
|
if (contract.workflowPlan.memory.write !== 'none') {
|
|
533
484
|
console.log(`=> MEMORY WRITE: ${contract.workflowPlan.memory.write}`);
|
|
@@ -586,4 +537,4 @@ if (process.argv[2]) {
|
|
|
586
537
|
});
|
|
587
538
|
}
|
|
588
539
|
}
|
|
589
|
-
module.exports = { run };
|
|
540
|
+
module.exports = { run, splitProfileLookupClauses, isFableProfileLookup, containsOnlyLookupTerms, detectFableModel };
|
|
@@ -39,16 +39,26 @@ function resultState(payload) {
|
|
|
39
39
|
return { failed, exitCode, evidence };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function unresolvedBindings(contract) {
|
|
43
|
+
const bindings = [...(contract.requiredBindings || []), ...(contract.optionalBindings || [])];
|
|
44
|
+
return bindings.filter(binding => binding.required !== false
|
|
45
|
+
? binding.status !== 'loaded'
|
|
46
|
+
: !['loaded', 'not-needed'].includes(binding.status));
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
function updateRunContract(entry, payload, command, sessionId) {
|
|
43
50
|
const { failed, exitCode, evidence } = resultState(payload);
|
|
51
|
+
const unresolved = unresolvedBindings(entry.contract);
|
|
52
|
+
const status = failed ? 'fail' : unresolved.length ? 'binding-unresolved' : 'pass';
|
|
44
53
|
const observedAt = new Date().toISOString();
|
|
45
54
|
const contract = {
|
|
46
55
|
...entry.contract,
|
|
47
56
|
workerId: entry.contract.workerId || getWorkerId(payload),
|
|
48
|
-
status
|
|
57
|
+
status,
|
|
49
58
|
lastObservedCommand: command,
|
|
50
59
|
lastObservedExitCode: exitCode ?? null,
|
|
51
60
|
evidence,
|
|
61
|
+
unresolvedBindings: unresolved.map(binding => binding.id),
|
|
52
62
|
verifiedAt: observedAt,
|
|
53
63
|
updatedAt: observedAt,
|
|
54
64
|
};
|
|
@@ -63,7 +73,9 @@ function updateRunContract(entry, payload, command, sessionId) {
|
|
|
63
73
|
sessionId: sessionId || contract.sessionId || null,
|
|
64
74
|
workerId: contract.workerId || null,
|
|
65
75
|
checkCommand: command,
|
|
66
|
-
status
|
|
76
|
+
status,
|
|
77
|
+
checkStatus: failed ? 'fail' : 'pass',
|
|
78
|
+
unresolvedBindings: unresolved.map(binding => binding.id),
|
|
67
79
|
exitCode: exitCode ?? null,
|
|
68
80
|
evidence,
|
|
69
81
|
observedAt,
|
|
@@ -72,6 +84,15 @@ function updateRunContract(entry, payload, command, sessionId) {
|
|
|
72
84
|
return contract;
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
function activeFableStages(runRoot) {
|
|
88
|
+
for (const relative of ['../../fable-mode/scripts/workflow-plan-consumer.js', '../../skills/fable-mode/scripts/workflow-plan-consumer.js']) {
|
|
89
|
+
const file = path.resolve(__dirname, relative);
|
|
90
|
+
if (!fs.existsSync(file)) continue;
|
|
91
|
+
try { return require(file).activeStageBindings(runRoot); } catch (_) { return []; }
|
|
92
|
+
}
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
function resolveLegacyContract(stateRoot, command, payload) {
|
|
76
97
|
const contractsDir = path.join(stateRoot, 'contracts');
|
|
77
98
|
let files = [];
|
|
@@ -110,7 +131,7 @@ process.stdin.on('end', () => {
|
|
|
110
131
|
const workflowContext = loadWorkflow(payload);
|
|
111
132
|
const activeRun = workflowContext.workflow?.workflowId ? matchingRun(workflowContext) : null;
|
|
112
133
|
const runMatches = listRunContracts(stateRoot).filter(({ contract }) =>
|
|
113
|
-
(contract.workflowId ? ['pending', 'planned', 'running', 'fail', 'pass'] : ['pending', 'planned', 'running']).includes(contract.status) && commandMatches(contract, command) &&
|
|
134
|
+
(contract.workflowId ? ['pending', 'planned', 'running', 'fail', 'pass', 'binding-unresolved'] : ['pending', 'planned', 'running', 'fail', 'binding-unresolved']).includes(contract.status) && commandMatches(contract, command) &&
|
|
114
135
|
(!workflowContext.workflow?.workflowId || (activeRun && contract.runId === activeRun.run.runId && contract.sessionId === sessionId))
|
|
115
136
|
);
|
|
116
137
|
|
|
@@ -163,6 +184,11 @@ process.stdin.on('end', () => {
|
|
|
163
184
|
}
|
|
164
185
|
}
|
|
165
186
|
console.log(`[Contract Test] ${contract.planId}/${contract.runId}/${contract.stageId}: ${contract.status.toUpperCase()} ${contract.checkCommand}`);
|
|
187
|
+
if (contract.status === 'binding-unresolved') {
|
|
188
|
+
console.error(`Resolve declared stage bindings before this stage can pass: ${contract.unresolvedBindings.join(', ')}`);
|
|
189
|
+
}
|
|
190
|
+
const activeStages = activeFableStages(entry.runRoot);
|
|
191
|
+
if (activeStages.length) console.log('[Contract Test] Active stage bindings: ' + JSON.stringify(activeStages));
|
|
166
192
|
if (contract.status === 'fail') {
|
|
167
193
|
console.error(`Evidence: ${contract.evidence || '(no output captured)'}`);
|
|
168
194
|
console.error(`Review this failed stage before relying on its output. Artifact: ${contract.outputPath || '(no path recorded)'}`);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Resolves a declared step/stage binding to a real, readable file. A binding
|
|
4
|
+
// path is availability evidence only after it resolves under the workspace or
|
|
5
|
+
// an installed Harness skill root; path syntax alone proves nothing.
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
function loadGeneratedSkills() {
|
|
10
|
+
const file = path.resolve(__dirname, '..', '..', '..', 'scripts', 'lib', 'generated-skills.js');
|
|
11
|
+
return fs.existsSync(file) ? require(file) : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Source checkout and Claude plugin keep skills at the package root; the
|
|
15
|
+
// Codex plugin keeps them under skills/. Host-provided plugin roots win.
|
|
16
|
+
function packageRoots() {
|
|
17
|
+
const packageRoot = path.resolve(__dirname, '..', '..', '..');
|
|
18
|
+
const roots = [];
|
|
19
|
+
for (const root of [process.env.CLAUDE_PLUGIN_ROOT, process.env.PLUGIN_ROOT, packageRoot]) {
|
|
20
|
+
if (!root) continue;
|
|
21
|
+
roots.push(path.resolve(root), path.resolve(root, 'skills'));
|
|
22
|
+
}
|
|
23
|
+
return [...new Set(roots)];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readableFile(file) {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.statSync(file).isFile()) return false;
|
|
29
|
+
fs.accessSync(file, fs.constants.R_OK);
|
|
30
|
+
return true;
|
|
31
|
+
} catch (_) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Registered Harness bindings and the path each one loads.
|
|
37
|
+
const PACKAGED_BINDING_PATHS = {
|
|
38
|
+
tdd: 'tdd/SKILL.md',
|
|
39
|
+
'verification-loop': 'verification-loop/SKILL.md',
|
|
40
|
+
'git-commit': 'git-commit/SKILL.md',
|
|
41
|
+
'security-review': 'security-review/SKILL.md',
|
|
42
|
+
'review-guidance': 'harness-everything/references/triage-and-tiers.md',
|
|
43
|
+
};
|
|
44
|
+
const SKILL_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
45
|
+
|
|
46
|
+
// Only a recognized Harness binding may search installation roots: a
|
|
47
|
+
// registered id at its registered path, or a skill id naming its own
|
|
48
|
+
// `<id>/SKILL.md`. Any other path is a workspace reference and must never
|
|
49
|
+
// resolve to an unrelated file that happens to exist in the Harness package.
|
|
50
|
+
function isPackagedBinding(binding) {
|
|
51
|
+
if (!binding.path) return false;
|
|
52
|
+
if (PACKAGED_BINDING_PATHS[binding.id]) return PACKAGED_BINDING_PATHS[binding.id] === binding.path;
|
|
53
|
+
return SKILL_ID.test(binding.id || '') && binding.path === `${binding.id}/SKILL.md`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Returns { availability, file }: `available` with an absolute readable file,
|
|
57
|
+
// `missing` when a declared path resolves nowhere, or `unknown` when neither a
|
|
58
|
+
// path nor a registered generated skill names a location.
|
|
59
|
+
function resolveBinding(binding, workspaceRoot) {
|
|
60
|
+
const roots = isPackagedBinding(binding) ? [...packageRoots(), workspaceRoot] : [workspaceRoot];
|
|
61
|
+
if (binding.path) {
|
|
62
|
+
for (const root of roots.filter(Boolean)) {
|
|
63
|
+
const file = path.resolve(root, binding.path);
|
|
64
|
+
if (readableFile(file)) return { availability: 'available', file };
|
|
65
|
+
}
|
|
66
|
+
return { availability: 'missing', file: null };
|
|
67
|
+
}
|
|
68
|
+
const generated = loadGeneratedSkills()?.findGeneratedSkill(binding.id, workspaceRoot);
|
|
69
|
+
if (generated) {
|
|
70
|
+
const file = path.join(generated.dirPath, 'SKILL.md');
|
|
71
|
+
return readableFile(file) ? { availability: 'available', file } : { availability: 'missing', file: null };
|
|
72
|
+
}
|
|
73
|
+
return { availability: 'unknown', file: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { resolveBinding, packageRoots, isPackagedBinding, PACKAGED_BINDING_PATHS };
|
|
@@ -102,7 +102,7 @@ function stagesForChangedPath(filePath, contractEntries) {
|
|
|
102
102
|
|
|
103
103
|
function getActiveRunContracts(stateRoot, sessionId) {
|
|
104
104
|
return listRunContracts(stateRoot).filter(({ contract }) => {
|
|
105
|
-
if (!['pending', 'planned', 'running'].includes(contract.status)) return false;
|
|
105
|
+
if (!['pending', 'planned', 'running', 'fail', 'binding-unresolved'].includes(contract.status)) return false;
|
|
106
106
|
return !sessionId || !contract.sessionId || contract.sessionId === sessionId;
|
|
107
107
|
});
|
|
108
108
|
}
|
|
@@ -370,15 +370,16 @@ function routerState(payload) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
function withLock(root, fn) {
|
|
373
|
+
function withLock(root, fn, options = {}) {
|
|
374
374
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
375
375
|
const lock = path.join(root, '.index.lock');
|
|
376
|
-
const
|
|
376
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs >= 0 ? options.timeoutMs : 2000;
|
|
377
|
+
const deadline = Date.now() + timeoutMs;
|
|
377
378
|
let fd = null;
|
|
378
379
|
while (fd === null) {
|
|
379
380
|
try { fd = fs.openSync(lock, 'wx'); } catch (_) {
|
|
380
381
|
try { if (Date.now() - fs.statSync(lock).mtimeMs > 5000) fs.unlinkSync(lock); } catch (__) { /* raced */ }
|
|
381
|
-
if (Date.now()
|
|
382
|
+
if (Date.now() >= deadline) return false;
|
|
382
383
|
const until = Date.now() + 10;
|
|
383
384
|
while (Date.now() < until) { /* short spin; hooks are short-lived */ }
|
|
384
385
|
}
|
|
@@ -432,18 +433,88 @@ function sweep(root, now = Date.now()) {
|
|
|
432
433
|
});
|
|
433
434
|
}
|
|
434
435
|
|
|
435
|
-
function
|
|
436
|
-
|
|
436
|
+
function appendRecordDetailed(root, record, text, options = {}) {
|
|
437
|
+
let outcome = null;
|
|
438
|
+
const locked = withLock(root, () => {
|
|
439
|
+
const file = path.join(root, 'observations-index.json');
|
|
440
|
+
const index = readJson(file, null) || { schemaVersion: 1, kind: 'system-one-observations', records: [] };
|
|
441
|
+
if (options.dedupeById && index.records.some(existing => existing.id === record.id)) {
|
|
442
|
+
outcome = 'duplicate';
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (options.dedupeByTurn) {
|
|
446
|
+
const match = observationTurnMatch(index.records, record);
|
|
447
|
+
if (match) {
|
|
448
|
+
outcome = match === 'ambiguous-legacy-turn' ? 'ambiguous' : 'duplicate';
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
437
452
|
// The blob is written under the lock, so a concurrent sweep cannot remove it before its record lands.
|
|
438
453
|
const textDir = path.join(root, 'text');
|
|
439
454
|
fs.mkdirSync(textDir, { recursive: true, mode: 0o700 });
|
|
440
|
-
if (text) fs.writeFileSync(path.join(textDir,
|
|
441
|
-
const file = path.join(root, 'observations-index.json');
|
|
442
|
-
const index = readJson(file, null) || { schemaVersion: 1, kind: 'system-one-observations', records: [] };
|
|
455
|
+
if (text) fs.writeFileSync(path.join(textDir, record.contentSha256 + '.json'), JSON.stringify(text), { mode: 0o600 });
|
|
443
456
|
index.records.push(record);
|
|
444
457
|
sweepLocked(root, index, Date.now());
|
|
445
458
|
writeJsonAtomic(file, index);
|
|
446
|
-
|
|
459
|
+
outcome = 'appended';
|
|
460
|
+
}, { timeoutMs: options.lockTimeoutMs });
|
|
461
|
+
if (!locked) return { status: 'lock-timeout' };
|
|
462
|
+
return { status: outcome || 'appended' };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function appendRecord(root, record, text, options = {}) {
|
|
466
|
+
return appendRecordDetailed(root, record, text, options).status === 'appended';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function observationTurnMatch(records, record) {
|
|
470
|
+
const writer = record && record.writer || {};
|
|
471
|
+
const sessionId = String(writer.sessionId || '');
|
|
472
|
+
const turnId = String(writer.turnId || '');
|
|
473
|
+
if (!sessionId || !record.host) return null;
|
|
474
|
+
const sameSession = (Array.isArray(records) ? records : []).filter(existing =>
|
|
475
|
+
existing && existing.host === record.host &&
|
|
476
|
+
String(existing.writer && existing.writer.sessionId || '') === sessionId
|
|
477
|
+
);
|
|
478
|
+
if (!sameSession.length) return null;
|
|
479
|
+
|
|
480
|
+
if (turnId && sameSession.some(existing => String(existing.writer && existing.writer.turnId || '') === turnId)) {
|
|
481
|
+
return 'same-turn-id';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Older live records have no turn ID. Only treat matching ordinal plus text
|
|
485
|
+
// as a definite duplicate; a partial match is ambiguous and the importer
|
|
486
|
+
// skips it instead of risking a second training row for the same turn.
|
|
487
|
+
const legacy = sameSession.filter(existing => existing.captureMethod !== 'session-backfill' &&
|
|
488
|
+
(!turnId || !String(existing.writer && existing.writer.turnId || '')));
|
|
489
|
+
if (!legacy.length) return null;
|
|
490
|
+
const sameTurn = existing => Number.isInteger(existing.turn) && existing.turn === record.turn;
|
|
491
|
+
const sameText = existing => Boolean(existing.contentSha256 && existing.contentSha256 === record.contentSha256);
|
|
492
|
+
if (legacy.some(existing => sameTurn(existing) && sameText(existing))) return 'legacy-same-turn';
|
|
493
|
+
if (legacy.some(existing => sameTurn(existing) || sameText(existing))) return 'ambiguous-legacy-turn';
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function findObservationTurnMatch(root, record) {
|
|
498
|
+
const index = readJson(path.join(root, 'observations-index.json'), null);
|
|
499
|
+
return observationTurnMatch(index && index.records, record);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function markSessionBackfillBehaviorUnobserved(root, options = {}) {
|
|
503
|
+
const file = path.join(root, 'observations-index.json');
|
|
504
|
+
if (!fs.existsSync(file)) return 0;
|
|
505
|
+
let changed = 0;
|
|
506
|
+
const locked = withLock(root, () => {
|
|
507
|
+
const index = readJson(file, null);
|
|
508
|
+
if (!index || !Array.isArray(index.records)) return;
|
|
509
|
+
for (const record of index.records) {
|
|
510
|
+
if (record.captureMethod === 'session-backfill' && record.behavior !== null) {
|
|
511
|
+
record.behavior = null;
|
|
512
|
+
changed++;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (changed) writeJsonAtomic(file, index);
|
|
516
|
+
}, { timeoutMs: options.lockTimeoutMs });
|
|
517
|
+
return locked ? changed : null;
|
|
447
518
|
}
|
|
448
519
|
|
|
449
520
|
// Tool events are immutable files, one per event, so parallel hooks never
|
|
@@ -484,6 +555,7 @@ function startTurn(payload, root) {
|
|
|
484
555
|
session.turn++;
|
|
485
556
|
session.pending = {
|
|
486
557
|
turn: session.turn,
|
|
558
|
+
turnId: String(payload.turn_id || payload.turnId || ''),
|
|
487
559
|
startedAt: new Date().toISOString(),
|
|
488
560
|
prompt: redact(typeof payload.prompt === 'string' ? payload.prompt : ''),
|
|
489
561
|
previous: session.lastFinal || '',
|
|
@@ -505,6 +577,7 @@ function toolTurn(payload, root, failed, codex) {
|
|
|
505
577
|
|
|
506
578
|
function finishPending(session, payload, root, final) {
|
|
507
579
|
const pending = session.pending;
|
|
580
|
+
const turnId = String(payload.turn_id || payload.turnId || pending.turnId || '');
|
|
508
581
|
const { label, reason } = final.text === null && final.reason ? { label: null, reason: final.reason } : parseLabelLine(final.text);
|
|
509
582
|
const collected = collectEvents(root, payload.session_id, pending.turn);
|
|
510
583
|
const behavior = { ...collected };
|
|
@@ -531,7 +604,10 @@ function finishPending(session, payload, root, final) {
|
|
|
531
604
|
contentSha256,
|
|
532
605
|
host: detectHost(payload, { codex: final.codex || collected._codex }),
|
|
533
606
|
turn: pending.turn,
|
|
534
|
-
writer: {
|
|
607
|
+
writer: {
|
|
608
|
+
sessionId: String(payload.session_id || 'unknown'),
|
|
609
|
+
...(turnId ? { turnId } : {}),
|
|
610
|
+
},
|
|
535
611
|
scope: { taskTerms: normalizeTerms(pending.prompt), requirementTerms: [], roles: [] },
|
|
536
612
|
promptBytes: Buffer.byteLength(pending.prompt, 'utf8'),
|
|
537
613
|
router: routerState(payload),
|
|
@@ -575,7 +651,7 @@ function handle(payload, env = process.env) {
|
|
|
575
651
|
}
|
|
576
652
|
|
|
577
653
|
module.exports = {
|
|
578
|
-
INTENTS, TIERS, STRATEGIES, enabled, storeRoot, redact, parseLabelLine, classifyCommand, labelTier, contradictions,
|
|
579
|
-
normalizeTerms, transcriptMessage, recordTool, emptyBehavior, mergeBehavior, toolFailed, handle, readJson,
|
|
654
|
+
INTENTS, TIERS, STRATEGIES, RETENTION_MS, PREVIOUS_BYTES, enabled, storeRoot, redact, parseLabelLine, stripLabels, tailBytes, sha, classifyCommand, labelTier, contradictions,
|
|
655
|
+
normalizeTerms, transcriptMessage, recordTool, emptyBehavior, mergeBehavior, toolFailed, handle, readJson, appendRecord, appendRecordDetailed, observationTurnMatch, findObservationTurnMatch, markSessionBackfillBehaviorUnobserved,
|
|
580
656
|
expired, recordText, sweep,
|
|
581
657
|
};
|