@hecer/yoke 1.15.0 → 1.16.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/plugin.json +3 -3
- package/.codex-plugin/plugin.json +2 -2
- package/CHANGELOG.md +41 -0
- package/README.md +26 -21
- package/canon/AGENTS.md +7 -0
- package/canon/manifest.yaml +2 -2
- package/canon/skills/executing-plans/SKILL.md +1 -1
- package/canon/skills/requesting-code-review/SKILL.md +2 -2
- package/canon/skills/requesting-code-review/code-reviewer.md +12 -0
- package/canon/skills/subagent-driven-development/SKILL.md +7 -3
- package/canon/skills/subagent-driven-development/code-quality-reviewer-prompt.md +7 -0
- package/canon/skills/subagent-driven-development/implementer-prompt.md +7 -0
- package/canon/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
- package/canon/skills/systematic-debugging/SKILL.md +7 -11
- package/canon/skills/systematic-debugging/condition-based-waiting.md +7 -0
- package/canon/skills/systematic-debugging/defense-in-depth.md +7 -0
- package/canon/skills/systematic-debugging/root-cause-tracing.md +9 -0
- package/canon/skills/tdd/SKILL.md +1 -1
- package/canon/skills/tdd/testing-anti-patterns.md +9 -0
- package/canon/skills/yoke-retrofit/SKILL.md +1 -1
- package/dist/agents/contracts.js +1 -1
- package/dist/agents/host.js +4 -0
- package/dist/agents/pi-telemetry.js +53 -0
- package/dist/agents/process-streams.js +13 -0
- package/dist/agents/providers.js +29 -5
- package/dist/agents/supervision.js +3 -1
- package/dist/agents/telemetry.js +38 -28
- package/dist/change/inbox.js +1 -1
- package/dist/estimation/schedule.js +40 -26
- package/dist/prd/command.js +2 -2
- package/dist/retrofit/detect.js +2 -0
- package/dist/retrofit/plan.js +2 -0
- package/dist/retrofit/planners/hermes.js +32 -0
- package/dist/retrofit/planners/pi.js +1 -1
- package/dist/retrofit/skill-actions.js +2 -1
- package/dist/review/command.js +1 -1
- package/dist/review/verdict.js +1 -1
- package/dist/routing/capability.js +1 -1
- package/dist/routing/router.js +1 -1
- package/dist/setup/command.js +3 -0
- package/docs/AGENT-HARDENING-2026-09-16.md +32 -0
- package/docs/HARNESSES.md +35 -14
- package/gemini-extension.json +2 -2
- package/package.json +3 -2
package/dist/agents/providers.js
CHANGED
|
@@ -38,6 +38,13 @@ const argsFor = (agent, permissions) => {
|
|
|
38
38
|
args.push('--tools', permissions === 'read-only' ? 'read,grep,find,ls' : 'read,bash,edit,write');
|
|
39
39
|
return args;
|
|
40
40
|
}
|
|
41
|
+
if (agent === 'hermes') {
|
|
42
|
+
const args = ['chat', '--format', 'stream-json', '--query-file', '-'];
|
|
43
|
+
if (permissions === 'unsafe')
|
|
44
|
+
return [...args, '--yolo'];
|
|
45
|
+
const toolsets = permissions === 'read-only' ? 'file' : 'file,terminal';
|
|
46
|
+
return [...args, '--toolsets', toolsets];
|
|
47
|
+
}
|
|
41
48
|
if (permissions === 'unsafe')
|
|
42
49
|
return ['--yolo', '--output-format', 'stream-json'];
|
|
43
50
|
const approval = permissions === 'read-only' ? 'plan' : 'auto_edit';
|
|
@@ -45,6 +52,9 @@ const argsFor = (agent, permissions) => {
|
|
|
45
52
|
};
|
|
46
53
|
export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe', selection = {}, output = {}) {
|
|
47
54
|
const parsedSelection = ModelSelectionSchema.parse(selection);
|
|
55
|
+
if (['opencode', 'kilo', 'pi', 'hermes'].includes(agent) && parsedSelection.reasoningEffort && parsedSelection.variant && parsedSelection.reasoningEffort !== parsedSelection.variant) {
|
|
56
|
+
throw new Error(`${agent} reasoningEffort and variant selections must match`);
|
|
57
|
+
}
|
|
48
58
|
if (agent === 'gemini' && parsedSelection.bare)
|
|
49
59
|
throw new Error('Gemini does not support the bare startup selection');
|
|
50
60
|
if (agent === 'gemini' && parsedSelection.reasoningEffort)
|
|
@@ -57,9 +67,13 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
|
|
|
57
67
|
throw new Error('Qwen does not support the reasoningEffort selection');
|
|
58
68
|
if (agent === 'qwen' && parsedSelection.nativeMultiAgent === true)
|
|
59
69
|
throw new Error('Qwen does not support enabling the nativeMultiAgent selection');
|
|
60
|
-
if (
|
|
70
|
+
if (agent === 'hermes' && parsedSelection.bare)
|
|
71
|
+
throw new Error('Hermes does not support the bare startup selection');
|
|
72
|
+
if (agent === 'hermes' && parsedSelection.nativeMultiAgent === true)
|
|
73
|
+
throw new Error('Hermes does not support enabling the nativeMultiAgent selection');
|
|
74
|
+
if (parsedSelection.provider && !['opencode', 'kilo', 'pi', 'hermes'].includes(agent))
|
|
61
75
|
throw new Error(`${agent} does not support an explicit provider selection`);
|
|
62
|
-
if (parsedSelection.variant && !['opencode', 'kilo', 'pi'].includes(agent))
|
|
76
|
+
if (parsedSelection.variant && !['opencode', 'kilo', 'pi', 'hermes'].includes(agent))
|
|
63
77
|
throw new Error(`${agent} does not support a model variant selection`);
|
|
64
78
|
const args = argsFor(agent, permissions);
|
|
65
79
|
if (output.schemaFile !== undefined || output.jsonSchema !== undefined) {
|
|
@@ -74,10 +88,10 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
|
|
|
74
88
|
throw new Error('Inline structured output is unsupported by the Windows provider shell shim');
|
|
75
89
|
args.push('--json-schema', schema);
|
|
76
90
|
}
|
|
77
|
-
else if (!['opencode', 'kilo', 'pi'].includes(agent))
|
|
91
|
+
else if (!['opencode', 'kilo', 'pi', 'hermes'].includes(agent))
|
|
78
92
|
throw new Error(`${agent} structured output schema requires ${agent === 'codex' ? 'schemaFile' : agent === 'claude' ? 'jsonSchema' : 'a supported native schema option (unavailable)'}`);
|
|
79
93
|
}
|
|
80
|
-
if (parsedSelection.provider && agent === 'pi')
|
|
94
|
+
if (parsedSelection.provider && (agent === 'pi' || agent === 'hermes'))
|
|
81
95
|
args.push('--provider', parsedSelection.provider);
|
|
82
96
|
if (parsedSelection.model) {
|
|
83
97
|
const qualified = agent === 'qwen' && parsedSelection.model.includes('::')
|
|
@@ -104,9 +118,11 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
|
|
|
104
118
|
args.push('--variant', parsedSelection.reasoningEffort);
|
|
105
119
|
else if (agent === 'pi')
|
|
106
120
|
args.push('--thinking', parsedSelection.reasoningEffort);
|
|
121
|
+
else if (agent === 'hermes')
|
|
122
|
+
args.push('--reasoning-effort', parsedSelection.reasoningEffort);
|
|
107
123
|
}
|
|
108
124
|
if (parsedSelection.variant) {
|
|
109
|
-
if (agent === 'opencode' || agent === 'kilo')
|
|
125
|
+
if ((agent === 'opencode' || agent === 'kilo') && !parsedSelection.reasoningEffort)
|
|
110
126
|
args.push('--variant', parsedSelection.variant);
|
|
111
127
|
else if (agent === 'pi') {
|
|
112
128
|
if (parsedSelection.reasoningEffort && parsedSelection.reasoningEffort !== parsedSelection.variant)
|
|
@@ -114,6 +130,12 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
|
|
|
114
130
|
if (!parsedSelection.reasoningEffort)
|
|
115
131
|
args.push('--thinking', parsedSelection.variant);
|
|
116
132
|
}
|
|
133
|
+
else if (agent === 'hermes') {
|
|
134
|
+
if (parsedSelection.reasoningEffort && parsedSelection.reasoningEffort !== parsedSelection.variant)
|
|
135
|
+
throw new Error('Hermes reasoningEffort and variant selections must match');
|
|
136
|
+
if (!parsedSelection.reasoningEffort)
|
|
137
|
+
args.push('--reasoning-effort', parsedSelection.variant);
|
|
138
|
+
}
|
|
117
139
|
}
|
|
118
140
|
if (agent === 'codex' && parsedSelection.nativeMultiAgent === false)
|
|
119
141
|
args.push('--disable', 'multi_agent');
|
|
@@ -131,6 +153,8 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
|
|
|
131
153
|
args.push('--pure');
|
|
132
154
|
else if (agent === 'pi')
|
|
133
155
|
throw new Error('Pi does not support the bare startup selection');
|
|
156
|
+
else if (agent === 'hermes')
|
|
157
|
+
throw new Error('Hermes does not support the bare startup selection');
|
|
134
158
|
}
|
|
135
159
|
if (agent === 'gemini' && parsedSelection.nativeMultiAgent === false) {
|
|
136
160
|
return { command: process.execPath, args: [fileURLToPath(new URL('../../hooks/bounded-gemini.mjs', import.meta.url)), ...args], input: prompt, cwd };
|
|
@@ -36,7 +36,9 @@ export function inspectProviderEvent(line) {
|
|
|
36
36
|
return { failure: 'provider-terminal-error' };
|
|
37
37
|
}
|
|
38
38
|
return { progress: (event.type === 'item.completed' && ((item?.type === 'command_execution' && item.exit_code === 0) || (item?.type === 'file_change' && item.status === 'completed')))
|
|
39
|
-
|| (event.type === '
|
|
39
|
+
|| (event.type === 'tool_execution_end' && event.isError === false)
|
|
40
|
+
|| (event.type === 'tool_use' && event.part?.type === 'tool' && event.part.state?.status === 'completed')
|
|
41
|
+
|| (event.type === 'tool_result' && (event.status === 'success' || event.is_error === false))
|
|
40
42
|
|| (event.type === 'user' && Array.isArray(event.message?.content) && event.message.content.some((part) => part.type === 'tool_result' && part.is_error === false)) };
|
|
41
43
|
}
|
|
42
44
|
export function inspectProviderDiagnostic(line) {
|
package/dist/agents/telemetry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPiTelemetry } from './pi-telemetry.js';
|
|
1
2
|
const finite = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
2
3
|
function parseJson(value) {
|
|
3
4
|
try {
|
|
@@ -32,15 +33,21 @@ export function parseProviderResult(agent, output) {
|
|
|
32
33
|
if (agent === 'qwen')
|
|
33
34
|
return parseQwenResult(output);
|
|
34
35
|
const fragments = [];
|
|
36
|
+
let structuredResult;
|
|
35
37
|
for (const line of output.split(/\r?\n/u)) {
|
|
36
38
|
const parsed = parseJson(line);
|
|
37
39
|
if (!parsed.ok || !isRecord(parsed.value))
|
|
38
40
|
continue;
|
|
39
41
|
const event = parsed.value;
|
|
42
|
+
if (agent === 'claude' && event.parent_tool_use_id != null)
|
|
43
|
+
continue;
|
|
44
|
+
if (event.type === 'error' || event.type === 'turn.failed' ||
|
|
45
|
+
(event.type === 'result' && (event.is_error === true || event.status === 'error' || event.error != null)))
|
|
46
|
+
return null;
|
|
40
47
|
switch (agent) {
|
|
41
48
|
case 'claude':
|
|
42
49
|
if (event.type === 'result' && directMachineResult(event.structured_output) !== undefined)
|
|
43
|
-
|
|
50
|
+
structuredResult = event.structured_output;
|
|
44
51
|
if (event.type === 'result' && typeof event.result === 'string')
|
|
45
52
|
fragments.push(event.result);
|
|
46
53
|
break;
|
|
@@ -63,13 +70,28 @@ export function parseProviderResult(agent, output) {
|
|
|
63
70
|
}
|
|
64
71
|
case 'pi':
|
|
65
72
|
if (event.type === 'message_end' && isRecord(event.message) && event.message.role === 'assistant') {
|
|
73
|
+
if (event.message.stopReason === 'error' || event.message.stopReason === 'aborted')
|
|
74
|
+
return null;
|
|
66
75
|
const text = textContent(event.message.content);
|
|
67
76
|
if (text)
|
|
68
77
|
fragments.push(text);
|
|
69
78
|
}
|
|
70
79
|
break;
|
|
80
|
+
case 'hermes':
|
|
81
|
+
if (event.type === 'result' && directMachineResult(event.structured_output ?? event.structured_result) !== undefined) {
|
|
82
|
+
structuredResult = event.structured_output ?? event.structured_result;
|
|
83
|
+
}
|
|
84
|
+
if (event.type === 'text' && typeof event.text === 'string')
|
|
85
|
+
fragments.push(event.text);
|
|
86
|
+
if (event.type === 'result' && typeof event.text === 'string')
|
|
87
|
+
fragments.push(event.text);
|
|
88
|
+
if (event.type === 'result' && typeof event.result === 'string')
|
|
89
|
+
fragments.push(event.result);
|
|
90
|
+
break;
|
|
71
91
|
}
|
|
72
92
|
}
|
|
93
|
+
if (structuredResult !== undefined)
|
|
94
|
+
return structuredResult;
|
|
73
95
|
const joined = parseJson(fragments.join(''));
|
|
74
96
|
if (joined.ok) {
|
|
75
97
|
const direct = directMachineResult(joined.value);
|
|
@@ -115,6 +137,15 @@ function parseQwenResult(output) {
|
|
|
115
137
|
return candidate;
|
|
116
138
|
}
|
|
117
139
|
export function parseProviderTelemetry(agent, lines) {
|
|
140
|
+
if (agent === 'pi') {
|
|
141
|
+
const accumulator = createPiTelemetry();
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const parsed = parseJson(line);
|
|
144
|
+
if (parsed.ok && isRecord(parsed.value))
|
|
145
|
+
accumulator.consume(parsed.value);
|
|
146
|
+
}
|
|
147
|
+
return accumulator.finish();
|
|
148
|
+
}
|
|
118
149
|
let inputTokens;
|
|
119
150
|
let cachedInputTokens;
|
|
120
151
|
let cacheWriteInputTokens;
|
|
@@ -126,7 +157,6 @@ export function parseProviderTelemetry(agent, lines) {
|
|
|
126
157
|
const harnessTotals = agent === 'opencode' || agent === 'kilo'
|
|
127
158
|
? { input: 0, output: 0, cached: 0, cacheWrite: 0, reasoning: 0, cost: 0, hasInput: false, hasOutput: false, hasCached: false, hasCacheWrite: false, hasReasoning: false, hasCost: false }
|
|
128
159
|
: undefined;
|
|
129
|
-
let piUsage;
|
|
130
160
|
for (const line of lines) {
|
|
131
161
|
let parsed;
|
|
132
162
|
try {
|
|
@@ -138,7 +168,7 @@ export function parseProviderTelemetry(agent, lines) {
|
|
|
138
168
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
139
169
|
continue;
|
|
140
170
|
const event = parsed;
|
|
141
|
-
if (agent === 'qwen' && event.parent_tool_use_id != null)
|
|
171
|
+
if ((agent === 'qwen' || agent === 'claude') && event.parent_tool_use_id != null)
|
|
142
172
|
continue;
|
|
143
173
|
const message = event.message && typeof event.message === 'object' ? event.message : undefined;
|
|
144
174
|
const stats = event.stats && typeof event.stats === 'object' ? event.stats : undefined;
|
|
@@ -177,15 +207,15 @@ export function parseProviderTelemetry(agent, lines) {
|
|
|
177
207
|
harnessTotals.hasCost = true;
|
|
178
208
|
}
|
|
179
209
|
}
|
|
180
|
-
if (agent === 'pi' && event.type === 'message_update' && event.usage && typeof event.usage === 'object')
|
|
181
|
-
piUsage = event.usage;
|
|
182
210
|
const usage = (event.usage && typeof event.usage === 'object'
|
|
183
211
|
? event.usage
|
|
184
212
|
: message?.usage && typeof message.usage === 'object'
|
|
185
213
|
? message.usage
|
|
186
214
|
: stats?.usage && typeof stats.usage === 'object'
|
|
187
215
|
? stats.usage
|
|
188
|
-
:
|
|
216
|
+
: event.tokens && typeof event.tokens === 'object'
|
|
217
|
+
? event.tokens
|
|
218
|
+
: stats);
|
|
189
219
|
const models = isRecord(stats?.models) ? stats.models : undefined;
|
|
190
220
|
const modelEntries = models ? Object.entries(models) : [];
|
|
191
221
|
const firstModel = modelEntries.length === 1 ? modelEntries[0] : undefined;
|
|
@@ -225,8 +255,8 @@ export function parseProviderTelemetry(agent, lines) {
|
|
|
225
255
|
source.cached_input_tokens = aggregateCached;
|
|
226
256
|
}
|
|
227
257
|
const inValue = finite(source?.input_tokens ?? source?.inputTokens ?? source?.prompt_tokens ?? source?.promptTokenCount ?? source?.input);
|
|
228
|
-
const cachedValue = finite(source?.cached_input_tokens ?? source?.cache_read_input_tokens ?? source?.cachedInputTokens ?? source?.cacheRead ?? source?.cachedContentTokenCount ?? source?.cached);
|
|
229
|
-
const cacheWriteValue = finite(source?.cache_write_input_tokens ?? source?.cache_creation_input_tokens ?? source?.cacheWriteInputTokens ?? source?.cacheWrite);
|
|
258
|
+
const cachedValue = finite(source?.cached_input_tokens ?? source?.cache_read_input_tokens ?? source?.cachedInputTokens ?? source?.cacheRead ?? source?.cachedContentTokenCount ?? source?.cached ?? source?.cache_read);
|
|
259
|
+
const cacheWriteValue = finite(source?.cache_write_input_tokens ?? source?.cache_creation_input_tokens ?? source?.cacheWriteInputTokens ?? source?.cacheWrite ?? source?.cache_write);
|
|
230
260
|
const outValue = finite(source?.output_tokens ?? source?.outputTokens ?? source?.completion_tokens ?? source?.candidatesTokenCount ?? source?.output);
|
|
231
261
|
const reasoningValue = finite(source?.reasoning_output_tokens ?? source?.reasoningOutputTokens ?? source?.thoughtsTokenCount ?? source?.thoughts);
|
|
232
262
|
if (inValue !== undefined)
|
|
@@ -247,26 +277,6 @@ export function parseProviderTelemetry(agent, lines) {
|
|
|
247
277
|
if (typeof eventModel === 'string' && eventModel && reportedModels.length <= 1)
|
|
248
278
|
model = eventModel;
|
|
249
279
|
}
|
|
250
|
-
if (piUsage) {
|
|
251
|
-
const piInput = finite(piUsage.input);
|
|
252
|
-
const piOutput = finite(piUsage.output);
|
|
253
|
-
const piCached = finite(piUsage.cacheRead);
|
|
254
|
-
const piCacheWrite = finite(piUsage.cacheWrite);
|
|
255
|
-
const piReasoning = finite(piUsage.reasoning);
|
|
256
|
-
const piCost = isRecord(piUsage.cost) ? finite(piUsage.cost.total) : undefined;
|
|
257
|
-
if (piInput !== undefined)
|
|
258
|
-
inputTokens = piInput;
|
|
259
|
-
if (piOutput !== undefined)
|
|
260
|
-
outputTokens = piOutput;
|
|
261
|
-
if (piCached !== undefined)
|
|
262
|
-
cachedInputTokens = piCached;
|
|
263
|
-
if (piCacheWrite !== undefined)
|
|
264
|
-
cacheWriteInputTokens = piCacheWrite;
|
|
265
|
-
if (piReasoning !== undefined)
|
|
266
|
-
reasoningOutputTokens = piReasoning;
|
|
267
|
-
if (piCost !== undefined)
|
|
268
|
-
totalCostUsd = piCost;
|
|
269
|
-
}
|
|
270
280
|
if (harnessTotals) {
|
|
271
281
|
if (harnessTotals.hasInput)
|
|
272
282
|
inputTokens = harnessTotals.input;
|
package/dist/change/inbox.js
CHANGED
|
@@ -104,7 +104,7 @@ export function buildChangePrompt(request, proposalPath, stories, brief = '') {
|
|
|
104
104
|
'Every proposed story must have passes: false and 2-5 structured acceptance criteria.',
|
|
105
105
|
'Every criterion must have a stable id, behavioral text, and one or more executable verify commands.',
|
|
106
106
|
'Each criterion id must appear in every verify command; each entry must be one approved test command without shell control operators.',
|
|
107
|
-
'Use only a configured Yoke harness if an optional agent affinity is useful (claude, codex, gemini, qwen, opencode, kilo, or
|
|
107
|
+
'Use only a configured Yoke harness if an optional agent affinity is useful (claude, codex, gemini, qwen, opencode, kilo, pi, or hermes).',
|
|
108
108
|
'',
|
|
109
109
|
`Write ONLY a YAML array of the NEW stories to this exact file: ${proposalPath}`,
|
|
110
110
|
'Do not edit .yoke/prd.yaml or any source file. Do not commit.',
|
|
@@ -10,32 +10,46 @@ export function estimateSchedule(stories, maxConcurrency, history) {
|
|
|
10
10
|
if (!estimate)
|
|
11
11
|
return { available: false, reason: 'No measured duration history' };
|
|
12
12
|
const ranks = criticalPathRanks(stories);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
const durations = new Map(stories.filter(story => !story.passes).map(story => [story.id,
|
|
14
|
+
estimateDurations(history.filter(item => item.storyId === story.id).map(item => item.ms)) ?? estimate,
|
|
15
|
+
]));
|
|
16
|
+
const simulate = (field) => {
|
|
17
|
+
const pending = stories.filter(story => !story.passes).sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0) || (ranks.get(b.id) ?? 0) - (ranks.get(a.id) ?? 0) || a.id.localeCompare(b.id));
|
|
18
|
+
const complete = new Set(stories.filter(story => story.passes).map(story => story.id));
|
|
19
|
+
const active = [];
|
|
20
|
+
const tasks = [];
|
|
21
|
+
let time = 0;
|
|
22
|
+
while (pending.length || active.length) {
|
|
23
|
+
for (let index = 0; index < pending.length && active.length < maxConcurrency;) {
|
|
24
|
+
const story = pending[index];
|
|
25
|
+
if ((story.needs ?? []).every(id => complete.has(id)) && (!story.area || !active.some(item => item.story.area === story.area)) && !active.some(item => writeScopesOverlap(story.writes, item.story.writes))) {
|
|
26
|
+
const end = time + durations.get(story.id)[field];
|
|
27
|
+
active.push({ story, end });
|
|
28
|
+
tasks.push({ storyId: story.id, startMs: time, endMs: end });
|
|
29
|
+
pending.splice(index, 1);
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
index++;
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
if (!active.length)
|
|
35
|
+
return undefined;
|
|
36
|
+
time = Math.min(...active.map(item => item.end));
|
|
37
|
+
for (let index = active.length - 1; index >= 0; index--)
|
|
38
|
+
if (active[index].end === time) {
|
|
39
|
+
complete.add(active[index].story.id);
|
|
40
|
+
active.splice(index, 1);
|
|
41
|
+
}
|
|
30
42
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
return { time, tasks };
|
|
44
|
+
};
|
|
45
|
+
const typical = simulate('typicalMs'), lower = simulate('lowerMs'), upper = simulate('upperMs');
|
|
46
|
+
if (!typical || !lower || !upper)
|
|
47
|
+
return { available: false, reason: 'Cyclic or missing dependencies' };
|
|
48
|
+
// Different durations can change a greedy schedule's ordering. Keep the
|
|
49
|
+
// empirical scenario envelope ordered; this is not a calibrated interval.
|
|
50
|
+
return { available: true, etaMs: typical.time,
|
|
51
|
+
lowerMs: Math.min(lower.time, typical.time, upper.time), upperMs: Math.max(lower.time, typical.time, upper.time),
|
|
52
|
+
sampleCount: estimate.sampleCount,
|
|
53
|
+
confidence: [...durations.values()].some(duration => duration.confidence === 'low') ? 'low' : estimate.confidence,
|
|
54
|
+
tasks: typical.tasks };
|
|
41
55
|
}
|
package/dist/prd/command.js
CHANGED
|
@@ -39,7 +39,7 @@ export function buildPrdDraftPrompt(idea, planningBrief) {
|
|
|
39
39
|
if (planningBrief?.trim()) {
|
|
40
40
|
lines.push('', '## Approved planning brief (treat these decisions as settled)', planningBrief.trim(), '', 'Do not reopen settled choices or invent alternatives that contradict this brief.');
|
|
41
41
|
}
|
|
42
|
-
lines.push('', 'Break the idea into 5-12 small, independently shippable stories; each must fit one loop iteration.', 'Each story needs:', '- id: STORY-1, STORY-2, ... (unique)', '- title: one imperative sentence', '- priority: dense integers from 1 (lower = built first)', '- needs: optional list of story IDs that must pass first; the graph must be acyclic', '- area: optional collision domain for safe parallel scheduling', '- agent: optional harness affinity (claude, codex, gemini, qwen, opencode, kilo, or
|
|
42
|
+
lines.push('', 'Break the idea into 5-12 small, independently shippable stories; each must fit one loop iteration.', 'Each story needs:', '- id: STORY-1, STORY-2, ... (unique)', '- title: one imperative sentence', '- priority: dense integers from 1 (lower = built first)', '- needs: optional list of story IDs that must pass first; the graph must be acyclic', '- area: optional collision domain for safe parallel scheduling', '- agent: optional harness affinity (claude, codex, gemini, qwen, opencode, kilo, pi, or hermes)', '- acceptance: 2-5 testable, behavioral criteria (observable outcomes, never implementation steps)', ' Each criterion is an object with a stable id, behavioral text, and verify: [one or more approved test commands].', ' Every criterion id must appear in every verify command; use one test command without shell control operators.', '- passes: false', '- writes: explicit relative write scopes for safe scheduling', assessmentInstructions, 'Include a complete assessment on every story in this same planning pass. Do not choose worker model names; the scheduler does that.', '', 'If the project has no source code yet, STORY-1 must scaffold the project skeleton with a runnable', 'test suite, and its acceptance must include that the verify command (verify.command in', '.yoke/config.yaml) exits 0.', '', 'Write ONLY the file .yoke/prd.yaml as a YAML array of stories in exactly that shape.', 'Do not modify any other file. Do not commit.');
|
|
43
43
|
return lines.join('\n');
|
|
44
44
|
}
|
|
45
45
|
export function prdFile(targetDir) {
|
|
@@ -71,7 +71,7 @@ export function runPrdDraft(targetDir, opts) {
|
|
|
71
71
|
const planner = resolvePlanner(config, resolveRunnerAgent(config, undefined, detectHostAgent()), config?.runner, opts.runner);
|
|
72
72
|
const agent = planner.agent;
|
|
73
73
|
if (!available(agent)) {
|
|
74
|
-
console.error(`Agent CLI "${agent}" was not found on PATH. Install it, or pick another with --runner=<claude|codex|gemini|qwen|opencode|kilo|pi>.`);
|
|
74
|
+
console.error(`Agent CLI "${agent}" was not found on PATH. Install it, or pick another with --runner=<claude|codex|gemini|qwen|opencode|kilo|pi|hermes>.`);
|
|
75
75
|
return 2;
|
|
76
76
|
}
|
|
77
77
|
const idleMs = resolveIdleMs(opts.timeoutMinutes, undefined);
|
package/dist/retrofit/detect.js
CHANGED
|
@@ -18,6 +18,8 @@ export function detectProject(targetDir) {
|
|
|
18
18
|
agents.push('kilo');
|
|
19
19
|
if (has('.pi') || has('PI.md'))
|
|
20
20
|
agents.push('pi');
|
|
21
|
+
if (has('.hermes') || has('HERMES.md') || has('hermes.yaml') || has('hermes.json'))
|
|
22
|
+
agents.push('hermes');
|
|
21
23
|
return {
|
|
22
24
|
agents,
|
|
23
25
|
hasAgentsMd: has('AGENTS.md'),
|
package/dist/retrofit/plan.js
CHANGED
|
@@ -5,6 +5,7 @@ import { planQwen } from './planners/qwen.js';
|
|
|
5
5
|
import { planOpenCode } from './planners/opencode.js';
|
|
6
6
|
import { planKilo } from './planners/kilo.js';
|
|
7
7
|
import { planPi } from './planners/pi.js';
|
|
8
|
+
import { planHermes } from './planners/hermes.js';
|
|
8
9
|
import { baseContextActions } from './context-actions.js';
|
|
9
10
|
export function planClaudeRetrofit(canonDir, targetDir) {
|
|
10
11
|
return planClaude(canonDir, targetDir);
|
|
@@ -17,6 +18,7 @@ export const PLANNERS = {
|
|
|
17
18
|
opencode: planOpenCode,
|
|
18
19
|
kilo: planKilo,
|
|
19
20
|
pi: planPi,
|
|
21
|
+
hermes: planHermes,
|
|
20
22
|
};
|
|
21
23
|
export function planRetrofit(canonDir, targetDir, agents, codeGraph = 'graphify', codeIntelligence = 'off') {
|
|
22
24
|
const seen = new Set();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadManifest } from '../../canon/manifest.js';
|
|
4
|
+
import { rtkInstruction } from '../tools.js';
|
|
5
|
+
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
6
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
7
|
+
const reviewerAgent = `---
|
|
8
|
+
description: Read-only Yoke reviewer for correctness and acceptance criteria
|
|
9
|
+
mode: primary
|
|
10
|
+
toolsets:
|
|
11
|
+
- file
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Review the observed diff and test evidence. Do not modify files. Return only actionable findings grounded in evidence.
|
|
15
|
+
`;
|
|
16
|
+
export function planHermes(canonDir, _targetDir, _codeGraph = 'graphify', _codeIntelligence = 'off') {
|
|
17
|
+
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
18
|
+
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
19
|
+
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'hermes'));
|
|
20
|
+
actions.push({
|
|
21
|
+
kind: 'write',
|
|
22
|
+
target: 'AGENTS.md',
|
|
23
|
+
content: `${baseline.trimEnd()}\n\n${rtkInstruction()}\n\n## Hermes integration\n\nHermes loads project skills from .hermes/skills and project context from AGENTS.md.\n\n${PRESERVE_SCAFFOLD}\n`,
|
|
24
|
+
reason: 'baseline instructions (Hermes reads AGENTS.md natively)',
|
|
25
|
+
}, {
|
|
26
|
+
kind: 'write',
|
|
27
|
+
target: '.hermes/agents/yoke-reviewer.md',
|
|
28
|
+
content: reviewerAgent,
|
|
29
|
+
reason: 'Hermes read-only reviewer agent',
|
|
30
|
+
});
|
|
31
|
+
return actions;
|
|
32
|
+
}
|
|
@@ -17,7 +17,7 @@ export function planPi(canonDir, _targetDir, _codeGraph = 'graphify', _codeIntel
|
|
|
17
17
|
kind: 'write',
|
|
18
18
|
target: '.pi/settings.json',
|
|
19
19
|
merge: true,
|
|
20
|
-
content: JSON.stringify({ skills: ['
|
|
20
|
+
content: JSON.stringify({ skills: ['./skills'] }, null, 2) + '\n',
|
|
21
21
|
reason: 'Pi project skill discovery',
|
|
22
22
|
});
|
|
23
23
|
return actions;
|
|
@@ -8,6 +8,7 @@ const roots = {
|
|
|
8
8
|
opencode: '.opencode/skills',
|
|
9
9
|
kilo: '.kilo/skills',
|
|
10
10
|
pi: '.pi/skills',
|
|
11
|
+
hermes: '.hermes/skills',
|
|
11
12
|
};
|
|
12
13
|
function manualClaudeSkill(content, skill) {
|
|
13
14
|
const source = content.toString('utf8');
|
|
@@ -52,7 +53,7 @@ export function skillPackageActions(canonDir, skill, provider) {
|
|
|
52
53
|
.map(file => ({
|
|
53
54
|
kind: 'write',
|
|
54
55
|
target: `${roots[provider]}/${skill.id}/${file.relativePath}`,
|
|
55
|
-
content: (provider === 'claude' || provider === 'qwen') && skill.invocation === 'manual' && file.relativePath === 'SKILL.md'
|
|
56
|
+
content: (provider === 'claude' || provider === 'qwen' || provider === 'pi' || provider === 'hermes') && skill.invocation === 'manual' && file.relativePath === 'SKILL.md'
|
|
56
57
|
? manualClaudeSkill(file.content, skill)
|
|
57
58
|
: portableContent(file),
|
|
58
59
|
executable: file.executable,
|
package/dist/review/command.js
CHANGED
|
@@ -6,7 +6,7 @@ import { loadConfig } from '../retrofit/config.js';
|
|
|
6
6
|
import { parseReviewVerdict } from './verdict.js';
|
|
7
7
|
// Resolve to the first available agent, preferring a *second* model so the review
|
|
8
8
|
// is genuinely cross-model. claude last => a Claude-only box degrades to self-review.
|
|
9
|
-
const RESOLUTION_ORDER = ['codex', 'gemini', 'qwen', 'claude', 'opencode', 'kilo', 'pi'];
|
|
9
|
+
const RESOLUTION_ORDER = ['codex', 'gemini', 'qwen', 'claude', 'opencode', 'kilo', 'pi', 'hermes'];
|
|
10
10
|
export function runReview(targetDir, opts = {}) {
|
|
11
11
|
const available = opts.isAvailable ?? isAgentAvailable;
|
|
12
12
|
const implementer = opts.implementer ?? loadConfig(targetDir)?.agents[0] ?? 'claude';
|
package/dist/review/verdict.js
CHANGED
|
@@ -70,7 +70,7 @@ export function formatReviewContract(path, provider) {
|
|
|
70
70
|
return [
|
|
71
71
|
`Write your final verdict to this absolute path: ${path}`,
|
|
72
72
|
'The file must contain exactly one JSON object with this contract:',
|
|
73
|
-
`{"schemaVersion":1,"approved":boolean,"summary":"non-empty string","findings":[{"id":"optional id","severity":"blocking|warning|info","message":"non-empty string","file":"optional path","line":1,"actionable":true,"suggestedFix":"optional repair","evidence":["optional evidence reference"]}],"provenance":{"provider":"${provider ?? 'claude|codex|gemini|qwen|opencode|kilo|pi'}","model":"provider-reported model","role":"review","promptVersion":1,"permissions":"safe"}}`,
|
|
73
|
+
`{"schemaVersion":1,"approved":boolean,"summary":"non-empty string","findings":[{"id":"optional id","severity":"blocking|warning|info","message":"non-empty string","file":"optional path","line":1,"actionable":true,"suggestedFix":"optional repair","evidence":["optional evidence reference"]}],"provenance":{"provider":"${provider ?? 'claude|codex|gemini|qwen|opencode|kilo|pi|hermes'}","model":"provider-reported model","role":"review","promptVersion":1,"permissions":"safe"}}`,
|
|
74
74
|
'Set approved=false when any blocking finding exists. Create the file even when the process also exits non-zero.',
|
|
75
75
|
].join('\n');
|
|
76
76
|
}
|
|
@@ -68,7 +68,7 @@ export function chooseCapability(input) {
|
|
|
68
68
|
const worker = reliable[0];
|
|
69
69
|
const blocked = !worker && (input.fallback === 'block' || input.maxTier !== undefined);
|
|
70
70
|
const provider = worker?.agent ?? input.story.agent ?? input.parent;
|
|
71
|
-
const selection = worker ? { provider: worker.provider, model: worker.model, reasoningEffort: worker.reasoningEffort, variant: worker.variant, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && provider !== 'pi' && input.parentSelection?.bare !== undefined ? { bare: input.parentSelection.bare } : {}) }
|
|
71
|
+
const selection = worker ? { provider: worker.provider, model: worker.model, reasoningEffort: worker.reasoningEffort, variant: worker.variant, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && provider !== 'pi' && provider !== 'hermes' && input.parentSelection?.bare !== undefined ? { bare: input.parentSelection.bare } : {}) }
|
|
72
72
|
: { ...(provider === input.parent ? input.parentSelection : {}), nativeMultiAgent: false };
|
|
73
73
|
const reason = `${role}: ${tiers[level]}; ${input.assessment.reason}${failures.length ? `; ${failures.length} verified failure(s), ${failures.length === 1 ? 'one targeted repair' : 'escalated'}` : ''}${worker ? '' : '; no eligible profile, parent/provider fallback'}`;
|
|
74
74
|
return { worker, provider, selection, reason: blocked ? `${role}: no eligible profile within routing limits; execution blocked` : reason, blocked, requiredTier: baseTier, selectedTier: tiers[level], failures: failures.length, exhausted, next: input.maxTier && level >= tiers.indexOf(input.maxTier) ? 'stop at configured tier limit' : level < 3 ? tiers[level + 1] : 'stop after bounded attempts' };
|
package/dist/routing/router.js
CHANGED
|
@@ -249,7 +249,7 @@ function routingSteps(options) {
|
|
|
249
249
|
return blocked('Selected routing profile exceeds configured limits; execution blocked');
|
|
250
250
|
const provider = worker?.agent ?? options.parent;
|
|
251
251
|
const selection = worker
|
|
252
|
-
? { provider: worker.provider, model: worker.model, reasoningEffort: worker.reasoningEffort, variant: worker.variant, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && provider !== 'pi' ? { bare: options.parentSelection?.bare } : {}) }
|
|
252
|
+
? { provider: worker.provider, model: worker.model, reasoningEffort: worker.reasoningEffort, variant: worker.variant, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && provider !== 'pi' && provider !== 'hermes' ? { bare: options.parentSelection?.bare } : {}) }
|
|
253
253
|
: { ...(options.parentSelection ?? {}), nativeMultiAgent: false };
|
|
254
254
|
const workerStarted = now();
|
|
255
255
|
const result = yield () => makeWorker(provider, selection)(ctx);
|
package/dist/setup/command.js
CHANGED
|
@@ -42,6 +42,9 @@ export function defaultRoutingWorkers(agents) {
|
|
|
42
42
|
pi: [
|
|
43
43
|
{ id: 'pi-standard', agent: 'pi', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
44
44
|
],
|
|
45
|
+
hermes: [
|
|
46
|
+
{ id: 'hermes-standard', agent: 'hermes', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
47
|
+
],
|
|
45
48
|
};
|
|
46
49
|
return agents.flatMap(agent => workers[agent]);
|
|
47
50
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Agent and skill hardening — 2026-09-16
|
|
2
|
+
|
|
3
|
+
This is a bounded source/contract audit, not a guarantee of defect-free operation or a seven-provider model benchmark. Pi was already supported in 1.13.0. The npm release observed at the start of this audit was 1.15.0; the fixes below are included in the 1.15.1 release changeset.
|
|
4
|
+
|
|
5
|
+
## Implemented fixes
|
|
6
|
+
|
|
7
|
+
- Pi telemetry now sums finalized assistant messages across tool turns. Streaming snapshots, turn-end echoes and agent-end transcript replays are not double-counted. Model switches remain visible; missing turn measurements produce partial usage, not a complete total. Optional cost/cache fields are reported as full totals only when every finalized turn measures them. An interrupted unfinalized turn remains unknown.
|
|
8
|
+
- Pi successful tool completions and OpenCode/Kilo completed tool envelopes reset the progress watchdog. Failed tools and streaming updates do not. Total, idle and progress budgets remain enforced; no timeout defaults were relaxed.
|
|
9
|
+
- Structured results from Claude, Codex, Gemini, OpenCode and Kilo are rejected when followed by a terminal error. Pi errored/aborted assistant messages cannot supply a verdict. Claude child-agent events cannot masquerade as parent verdicts or parent usage. Qwen's existing parent/result handling remains in place.
|
|
10
|
+
- OpenCode/Kilo conflicting effort/variant aliases fail before process launch; matching aliases produce one flag.
|
|
11
|
+
- Pi settings use a skill path relative to `.pi/settings.json`. Manual-only skills receive Pi's native `disable-model-invocation` marker, just as Claude/Qwen already did.
|
|
12
|
+
- Schedule estimation simulates typical/lower/upper per-story durations with dependency and write-conflict constraints. A sparse per-story sample no longer inherits stronger confidence from unrelated pooled samples. These are empirical scenarios, not calibrated probabilities or deadlines.
|
|
13
|
+
- Eight missing supporting files are now packaged and linked: three delegation templates, a review template, three debugging guides and a testing anti-pattern guide. Shared instructions prohibit nested orchestration inside Yoke workers, explain host capability fallbacks, preserve acceptance/review gates, and distinguish budgets from time estimates. Removed unsupported claims about guaranteed subagent quality and fixed debugging speed/success rates.
|
|
14
|
+
- The Code Intelligence coordinator unit test now replaces the separate sandbox adapter too, avoiding accidental real Serena startup. It checks an actual rename in the isolated transaction output, approval gating, and that the source project remains untouched (apply does not merge).
|
|
15
|
+
|
|
16
|
+
## Compatibility and operation
|
|
17
|
+
|
|
18
|
+
Existing projects can refresh the canonical skills with `yoke retrofit . --agent=all`, or select one harness. Inspect the merge-aware retrofit diff and backups. Existing custom skill paths remain user-owned; remove an obsolete `.pi/skills` settings entry only after confirming it is the old Yoke-generated entry. The correct settings-relative entry is `./skills`.
|
|
19
|
+
|
|
20
|
+
Current Pi also requires project trust before loading project-local skills in headless runs. Trust is an explicit user decision, not something Yoke grants automatically. See [the harness guide](HARNESSES.md). Tool allowlists are not an OS sandbox: extensions and native configuration still require review.
|
|
21
|
+
|
|
22
|
+
## Evidence and limits
|
|
23
|
+
|
|
24
|
+
Final local validation: 1,281 tests passed, two platform-specific tests skipped, 140 test files passed. TypeScript lint/build, Canon validation, README metadata check and npm package dry run passed; npm audit reported zero vulnerabilities. A follow-up Canon run after the supporting-document additions passed (36 passed, one platform-specific skip). The subsequent release request targets 1.15.1; publication status is recorded by the matching GitHub release and npm registry, not inferred from these local checks.
|
|
25
|
+
|
|
26
|
+
Regression cases were observed failing before their fixes, then passing in targeted runs. Coverage includes multi-turn/chunked Pi usage, missing measurements, model changes, terminal errors, effort conflicts, sparse timing samples, manual invocation and packaged templates. Canon validation scans all registered skill packages; the content audit focused on orchestration, execution and verification instructions, not a formal proof of every skill's behavior.
|
|
27
|
+
|
|
28
|
+
The local Pi 0.85.1 CLI help confirms JSON mode, tool allowlists, provider/model/thinking selection and project-trust controls. Qwen and Kilo were not found on this machine's PATH. No paid/authenticated seven-agent task matrix, cross-platform native sandbox comparison, measured speedup or quality benchmark was performed. Faster feedback is a workflow recommendation, not a measured improvement claim. Existing full verification gates remain required.
|
|
29
|
+
|
|
30
|
+
A read-only README provenance scan found no supported C2PA structure and completed the supported scan. Cryptographic verification and signer trust remain unknown because no conforming verifier/trust policy was supplied. Metadata privacy was unsupported for this text format. Proprietary keyed watermark detection was unavailable; no authorship conclusion follows. No marks were removed.
|
|
31
|
+
|
|
32
|
+
Primary references: [Pi JSON mode](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/json.md), [Pi event types](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts), [Pi skills](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/skills.md), [OpenCode run implementation](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/cli/cmd/run.ts).
|