@hecer/yoke 1.2.1 → 1.4.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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +66 -24
- package/README.md +186 -104
- package/TODOS.md +0 -3
- package/canon/loop/loop-spec.md +53 -24
- package/canon/loop/prd.schema.md +30 -6
- package/canon/manifest.yaml +1 -1
- package/canon/skills/authoring-prd/SKILL.md +30 -31
- package/dist/agents/contracts.js +50 -0
- package/dist/agents/process-incarnation.js +15 -0
- package/dist/agents/process-record.js +65 -0
- package/dist/agents/process-streams.js +40 -0
- package/dist/agents/process.js +177 -0
- package/dist/agents/providers.js +10 -7
- package/dist/agents/telemetry.js +62 -0
- package/dist/change/inbox.js +279 -0
- package/dist/cli.js +90 -4
- package/dist/loop/candidate-boundaries.js +43 -0
- package/dist/loop/candidate-cleanup.js +98 -0
- package/dist/loop/candidate-contracts.js +1 -0
- package/dist/loop/candidate-selection.js +84 -0
- package/dist/loop/candidates.js +228 -0
- package/dist/loop/claim-lease.js +131 -0
- package/dist/loop/claims.js +177 -40
- package/dist/loop/cleanup.js +117 -15
- package/dist/loop/decision.js +31 -0
- package/dist/loop/dispatcher.js +334 -0
- package/dist/loop/evidence.js +31 -0
- package/dist/loop/gates.js +10 -1
- package/dist/loop/loop.js +236 -33
- package/dist/loop/merge-queue.js +12 -6
- package/dist/loop/parallel-adapters.js +185 -0
- package/dist/loop/parallel-command.js +287 -0
- package/dist/loop/parallel.js +2 -4
- package/dist/loop/prd.js +63 -2
- package/dist/loop/reporter.js +86 -5
- package/dist/loop/run-command.js +227 -53
- package/dist/loop/runner.js +77 -34
- package/dist/loop/verify.js +11 -0
- package/dist/loop/watchdog.js +67 -8
- package/dist/loop/worker-cancellation.js +17 -0
- package/dist/loop/worker-cleanup.js +23 -0
- package/dist/loop/worker-contracts.js +1 -0
- package/dist/loop/worker.js +254 -0
- package/dist/prd/command.js +18 -5
- package/dist/quality/artifacts.js +59 -0
- package/dist/quality/candidate-comparison.js +130 -0
- package/dist/quality/command.js +316 -0
- package/dist/quality/loop.js +86 -0
- package/dist/quality/process-command.js +57 -0
- package/dist/quality/reference.js +187 -0
- package/dist/quality/repair.js +11 -0
- package/dist/quality/runner.js +66 -0
- package/dist/quality/types.js +60 -0
- package/dist/quality/verdict.js +142 -0
- package/dist/retrofit/config.js +13 -4
- package/dist/retrofit/gitignore.js +4 -0
- package/dist/review/command.js +27 -38
- package/dist/review/verdict.js +38 -7
- package/dist/routing/router.js +4 -1
- package/docs/MIGRATING-TO-1.4.md +70 -0
- package/docs/PUBLISHING.md +16 -2
- package/docs/superpowers/plans/2026-08-13-gauntlet-quality-loop.md +537 -0
- package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -0
- package/gemini-extension.json +1 -1
- package/package.json +6 -6
package/dist/review/verdict.js
CHANGED
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { AgentSchema, PermissionProfileSchema } from '../agents/contracts.js';
|
|
4
5
|
export const ReviewFindingSchema = z.object({
|
|
6
|
+
id: z.string().min(1).optional(),
|
|
5
7
|
severity: z.enum(['blocking', 'warning', 'info']),
|
|
6
8
|
message: z.string().min(1),
|
|
7
9
|
file: z.string().min(1).optional(),
|
|
8
10
|
line: z.number().int().positive().optional(),
|
|
11
|
+
actionable: z.boolean().optional(),
|
|
12
|
+
suggestedFix: z.string().min(1).optional(),
|
|
13
|
+
evidence: z.array(z.string().min(1)).optional(),
|
|
9
14
|
});
|
|
10
15
|
export const ReviewVerdictSchema = z.object({
|
|
16
|
+
schemaVersion: z.literal(1),
|
|
11
17
|
approved: z.boolean(),
|
|
12
18
|
summary: z.string().min(1),
|
|
13
19
|
findings: z.array(ReviewFindingSchema),
|
|
20
|
+
provenance: z.object({
|
|
21
|
+
provider: AgentSchema,
|
|
22
|
+
model: z.string().min(1),
|
|
23
|
+
role: z.literal('review'),
|
|
24
|
+
promptVersion: z.literal(1),
|
|
25
|
+
permissions: PermissionProfileSchema,
|
|
26
|
+
}),
|
|
14
27
|
});
|
|
28
|
+
export function parseReviewVerdict(value, expected) {
|
|
29
|
+
const result = ReviewVerdictSchema.safeParse(value);
|
|
30
|
+
if (!result.success)
|
|
31
|
+
throw new Error(`Review verdict is invalid: ${result.error.message}`);
|
|
32
|
+
if (expected && result.data.provenance.provider !== expected.provider)
|
|
33
|
+
throw new Error(`Review verdict provider mismatch: expected ${expected.provider}, received ${result.data.provenance.provider}`);
|
|
34
|
+
if (expected?.model && result.data.provenance.model !== expected.model)
|
|
35
|
+
throw new Error(`Review verdict model mismatch: expected ${expected.model}, received ${result.data.provenance.model}`);
|
|
36
|
+
return result.data;
|
|
37
|
+
}
|
|
38
|
+
export function selectRepairFinding(verdict) {
|
|
39
|
+
if (verdict.approved)
|
|
40
|
+
return null;
|
|
41
|
+
return verdict.findings.find(candidate => candidate.severity === 'blocking' && (candidate.actionable ?? true)) ?? null;
|
|
42
|
+
}
|
|
15
43
|
export function reviewVerdictPath(targetDir) {
|
|
16
44
|
return resolve(join(targetDir, '.yoke', 'review-verdict.json'));
|
|
17
45
|
}
|
|
18
|
-
export function readReviewVerdict(path) {
|
|
46
|
+
export function readReviewVerdict(path, expected) {
|
|
19
47
|
if (!existsSync(path))
|
|
20
48
|
throw new Error(`Review verdict is missing: ${path}`);
|
|
21
49
|
try {
|
|
@@ -26,20 +54,23 @@ export function readReviewVerdict(path) {
|
|
|
26
54
|
catch (error) {
|
|
27
55
|
throw new Error(`Review verdict is malformed JSON: ${error.message}`);
|
|
28
56
|
}
|
|
29
|
-
|
|
30
|
-
if (!result.success)
|
|
31
|
-
throw new Error(`Review verdict is invalid: ${result.error.message}`);
|
|
32
|
-
return result.data;
|
|
57
|
+
return parseReviewVerdict(value, expected);
|
|
33
58
|
}
|
|
34
59
|
finally {
|
|
35
60
|
rmSync(path, { force: true });
|
|
36
61
|
}
|
|
37
62
|
}
|
|
38
|
-
export function
|
|
63
|
+
export function formatReviewStdoutContract(provider) {
|
|
64
|
+
return [
|
|
65
|
+
'Return exactly one JSON object as your final response. Do not write any file.',
|
|
66
|
+
`{"schemaVersion":1,"approved":boolean,"summary":"non-empty string","findings":[],"provenance":{"provider":"${provider}","model":"provider-reported model","role":"review","promptVersion":1,"permissions":"read-only"}}`,
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
export function formatReviewContract(path, provider) {
|
|
39
70
|
return [
|
|
40
71
|
`Write your final verdict to this absolute path: ${path}`,
|
|
41
72
|
'The file must contain exactly one JSON object with this contract:',
|
|
42
|
-
|
|
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'}","model":"provider-reported model","role":"review","promptVersion":1,"permissions":"safe"}}`,
|
|
43
74
|
'Set approved=false when any blocking finding exists. Create the file even when the process also exits non-zero.',
|
|
44
75
|
].join('\n');
|
|
45
76
|
}
|
package/dist/routing/router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildWatchdogInvocation, makeRunner, runCapturedAgent, runnerInvocation, } from '../loop/runner.js';
|
|
2
|
+
import { isAcceptanceCriterion } from '../loop/prd.js';
|
|
2
3
|
import { historyForWorkers, projectHash, recordRoutingObservation, storyHash } from './registry.js';
|
|
3
4
|
const costRank = { low: 0, medium: 1, high: 2 };
|
|
4
5
|
export function rankWorkers(workers, strategy, maxCandidates) {
|
|
@@ -40,7 +41,9 @@ export function buildRoutingPrompt(ctx, workers, strategy) {
|
|
|
40
41
|
'',
|
|
41
42
|
`Story ${ctx.story.id}: ${ctx.story.title}`,
|
|
42
43
|
'Acceptance criteria:',
|
|
43
|
-
...ctx.story.acceptance.map(item =>
|
|
44
|
+
...ctx.story.acceptance.map(item => isAcceptanceCriterion(item)
|
|
45
|
+
? `- [${item.id}] ${item.text} (proof: ${item.verify.join(' && ')})`
|
|
46
|
+
: `- ${item}`),
|
|
44
47
|
'',
|
|
45
48
|
'Allowed candidates:',
|
|
46
49
|
'- SELF: strong parent; highest confidence; highest expected cost',
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Migrating to Yoke 1.4
|
|
2
|
+
|
|
3
|
+
Yoke 1.4 adds dependency-aware parallel workers, isolated candidate selection, and a reference-driven quality gauntlet. Existing projects remain serial and skip quality comparison unless you opt in.
|
|
4
|
+
|
|
5
|
+
Upgrade and refresh generated harness files:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @hecer/yoke@1.4.0
|
|
9
|
+
yoke retrofit .
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Parallel execution
|
|
13
|
+
|
|
14
|
+
Run dependency-ready stories concurrently with an explicit worker limit:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
yoke loop run . --isolate --parallel=3
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Parallel runs use isolated worktrees, leased claims, and a FIFO integration queue. Every candidate must pass its worker gates and the merged result must pass the project gates again. Adaptive routing is disabled during parallel and multi-candidate runs so each worker has one auditable provider identity.
|
|
21
|
+
|
|
22
|
+
Use `--candidates=N` to produce and mechanically gate multiple implementations before an identity-blind comparison selects the winner. Candidate mode supports up to five candidates.
|
|
23
|
+
|
|
24
|
+
## Reference-driven quality
|
|
25
|
+
|
|
26
|
+
Quality remains disabled by default. A story must declare a trusted reference, candidate artifacts, and a rubric:
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
quality:
|
|
30
|
+
reference: { name: approved-home, source: design/home.png, kind: file }
|
|
31
|
+
candidate: { kind: screenshots, paths: [.yoke/proof/STORY-1/home.png] }
|
|
32
|
+
rubric: Match the approved layout, hierarchy, spacing, and states.
|
|
33
|
+
policy: blocking
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Project defaults can bound critic and repair work:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
quality:
|
|
40
|
+
enabled: false
|
|
41
|
+
policy: blocking
|
|
42
|
+
maxRounds: 3
|
|
43
|
+
maxMinutes: 60
|
|
44
|
+
consistencyChecks: 2
|
|
45
|
+
maxParallelCandidates: 2
|
|
46
|
+
critic: { agent: codex, model: gpt-5.6-sol } # model required for --candidates
|
|
47
|
+
repair: { agent: claude }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Enable it for one run with:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
yoke loop run . --quality
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`consistencyChecks` is fixed at `2`: Yoke runs the swapped-label pair needed to detect identity-sensitive critic output. Advisory mode records both verdicts without blocking; blocking mode permits bounded repairs and reruns all mechanical gates.
|
|
57
|
+
|
|
58
|
+
## Cleanup behavior
|
|
59
|
+
|
|
60
|
+
`yoke loop cleanup` now retains Yoke-created worktrees by default so failed candidates remain inspectable. Remove them explicitly when no longer needed:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
yoke loop cleanup . --remove-worktrees
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Cleanup still reaps only provider processes recorded for the current project and removes stale loop locks. It never kills providers by machine-wide process name.
|
|
67
|
+
|
|
68
|
+
## Review verdicts
|
|
69
|
+
|
|
70
|
+
Review verdict files now require `schemaVersion: 1` and provenance containing the provider, provider-reported model, review role, prompt version, and permission profile. Custom reviewer integrations must emit the contract printed in the reviewer prompt. Legacy verdict files without this envelope fail closed.
|
package/docs/PUBLISHING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Publishing channels — status & playbook
|
|
2
2
|
|
|
3
|
-
Where Yoke is published, and how each channel gets updated. (Reviewed 2026-
|
|
3
|
+
Where Yoke is published, and how each channel gets updated. (Reviewed 2026-08-15.)
|
|
4
4
|
|
|
5
5
|
## Live
|
|
6
6
|
|
|
@@ -14,12 +14,26 @@ Where Yoke is published, and how each channel gets updated. (Reviewed 2026-07-30
|
|
|
14
14
|
|
|
15
15
|
## GitHub release (required, not just a tag)
|
|
16
16
|
|
|
17
|
+
Before the release commit, update every user-facing version and README statistic, then require the
|
|
18
|
+
same checks npm will run:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run docs:update
|
|
22
|
+
npm run docs:check
|
|
23
|
+
npm run prepublishOnly
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`docs:update` synchronizes the README's package version, test count, skill count, and supported
|
|
27
|
+
agents from `package.json`, Vitest discovery, and `canon/manifest.yaml`. The version must also be
|
|
28
|
+
kept in sync in `package-lock.json`, `canon/manifest.yaml`, `.claude-plugin/plugin.json`,
|
|
29
|
+
`.codex-plugin/plugin.json`, and `gemini-extension.json`.
|
|
30
|
+
|
|
17
31
|
A pushed tag appears under **Tags**, but GitHub only shows an entry under **Releases** after a
|
|
18
32
|
release object is created. Use this idempotent check after the version commit reaches `main`:
|
|
19
33
|
|
|
20
34
|
```bash
|
|
21
35
|
set -euo pipefail
|
|
22
|
-
VERSION=1.
|
|
36
|
+
VERSION=1.4.0
|
|
23
37
|
TARGET=$(git rev-parse HEAD)
|
|
24
38
|
git fetch --tags origin
|
|
25
39
|
|