@hecer/yoke 1.17.0 → 1.18.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 +15 -0
- package/README.md +19 -6
- package/canon/loop/loop-spec.md +20 -3
- package/canon/manifest.yaml +1 -1
- package/dist/cli.js +72 -3
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +13 -1
- package/dist/loop/prd.js +11 -0
- package/dist/loop/reporter.js +8 -6
- package/dist/loop/run-command.js +388 -15
- package/dist/prd/explore.js +441 -0
- package/docs/CONTINUOUS-EXPLORATION.md +67 -0
- package/docs/parallel-execution.md +12 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "yoke",
|
|
4
4
|
"displayName": "Yoke",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.18.0",
|
|
6
6
|
"description": "Cross-agent coding harness for Claude, Codex, Gemini, Qwen, OpenCode, Kilo, Pi and Hermes: one curated skill canon plus mechanical safety gates and an autonomous loop via the yoke CLI.",
|
|
7
7
|
"author": { "name": "HECer", "url": "https://github.com/HECer" },
|
|
8
8
|
"homepage": "https://github.com/HECer/yoke#readme",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.18.0 — 2026-09-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Add opt-in continuous exploration with evidence-filtered task discovery, validated PRD additions, isolated implementation, bounded history compaction, automatic provider recovery, stop detection, and `yoke loop pause`.
|
|
7
|
+
- Add optional `--explore-limit=<Nh|Nd|Nw>` durations. Exploration remains unbounded when omitted; when the limit expires, Yoke stops launching work and pauses after active workers finish their normal gates and integration.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Keep exploration status and narrative focused on newly accepted work, the next planned action, provider recovery, and safe stop state.
|
|
11
|
+
- Process finite-duration runs in bounded task batches so the supervisor can honor the deadline while retaining configured parallel workers.
|
|
12
|
+
|
|
13
|
+
### Migration and validation limits
|
|
14
|
+
- Exploration is opt-in and requires isolated story worktrees. Use `--explore-limit=12h`, `3d`, or `2w` to bound one invocation; expiry exits with code `3`, and a later resume starts a new duration. Without a limit or a user pause, it continues while the process and machine remain available.
|
|
15
|
+
- Auto-discovered work is limited to repository-evidenced proposals that pass confidence, risk, write-scope, acceptance-criteria and criterion-test checks. This does not guarantee project maturity or model quality; provider credentials, configured gates and machine availability still determine progress.
|
|
16
|
+
- A time limit does not interrupt an active story or integration. Active work finishes through the normal gates before the loop stops, so wall-clock completion can exceed the requested duration by the time needed for that safe boundary.
|
|
17
|
+
|
|
3
18
|
## 1.17.0 — 2026-09-27
|
|
4
19
|
|
|
5
20
|
### Added
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<h1><img src="https://raw.githubusercontent.com/HECer/yoke/v1.
|
|
3
|
+
<h1><img src="https://raw.githubusercontent.com/HECer/yoke/v1.18.0/docs/assets/yoke-logo.png" alt="Yoke" width="100" height="63"></h1>
|
|
4
4
|
|
|
5
|
-
<!-- yoke:version:start -->1.
|
|
6
|
-
<!-- yoke:tests:start -->
|
|
5
|
+
<!-- yoke:version:start -->1.18.0<!-- yoke:version:end -->
|
|
6
|
+
<!-- yoke:tests:start -->1313<!-- yoke:tests:end -->
|
|
7
7
|
<!-- yoke:skills:start -->34<!-- yoke:skills:end -->
|
|
8
8
|
<!-- yoke:agents:start -->Claude | Codex | Gemini | Qwen | OpenCode | Kilo | Pi | Hermes<!-- yoke:agents:end -->
|
|
9
9
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
[](#-license)
|
|
18
18
|

|
|
19
19
|

|
|
20
|
-

|
|
21
21
|

|
|
22
22
|

|
|
23
23
|
|
|
@@ -198,6 +198,19 @@ Yoke is meant to be operated *by* your coding agent — after a retrofit, the ag
|
|
|
198
198
|
|
|
199
199
|
> ⚠️ **Long runs from inside an agent session:** `yoke loop run` has no story cap by default; it continues until every planned story passes or a gate blocks. A multi-story run can therefore outlive most agents' shell-tool timeouts (Claude Code's Bash tool defaults to 2 minutes). If the outer tool call is killed mid-run, you get a stale lock and possibly half-finished state — which *looks* like a hang. Run the loop **in the background** (e.g. Claude Code's `run_in_background`), use `--max=3..5` only when you intentionally want a bounded batch, poll with `yoke loop status`, and after any interrupted run do `yoke loop cleanup` before the next one. A `running` status with no update for 20+ minutes on a claude runner is worth checking — since 0.5.0 the runner streams continuously, so prolonged true silence is no longer normal.
|
|
200
200
|
|
|
201
|
+
### Optional continuous exploration
|
|
202
|
+
|
|
203
|
+
`yoke loop run . --explore` keeps the supervisor alive after the PRD drains. It periodically scans
|
|
204
|
+
for evidence-backed, testable improvements, adds only bounded tasks that pass strict contract checks,
|
|
205
|
+
then implements them through the same isolated loop gates. `--explore-interval=10` changes the
|
|
206
|
+
default 30-minute rescan interval. Exploration runs indefinitely by default; set `--explore-limit=12h`,
|
|
207
|
+
`--explore-limit=3d`, or `--explore-limit=2w` to stop automatically after hours, days, or weeks. At
|
|
208
|
+
expiry, Yoke pauses at a safe task boundary, lets active workers finish their gates and integration,
|
|
209
|
+
and exits with code `3`; run it again to resume. `yoke loop pause .` also stops at a safe boundary.
|
|
210
|
+
Failed providers or stories are retried with backoff, and live status heartbeats while the supervisor
|
|
211
|
+
waits. Use `--max=N` for an intentional story-attempt cap. See the [continuous exploration guide](docs/CONTINUOUS-EXPLORATION.md)
|
|
212
|
+
for task validation, history compaction, recovery and process-lifetime limits.
|
|
213
|
+
|
|
201
214
|
> ⚠️ **Never kill agent processes by name or command-line pattern** (e.g. every process matching `dangerously-skip-permissions`): on a machine running several yoke projects, that takes down the *healthy* runners of the other projects mid-story — they stall and their loops block. `yoke loop cleanup` is the scoped alternative: each watchdog records its pids in the project's `.yoke/runner.pid`, and cleanup kills exactly those recorded trees — nothing else on the machine.
|
|
202
215
|
|
|
203
216
|
### Agent cheat sheet — every command is an exit-code contract
|
|
@@ -219,7 +232,7 @@ Yoke's CLI is deterministic and chainable by design: an agent (or a shell `&&`)
|
|
|
219
232
|
| `yoke prd check [dir]` | PRD lint gate (schema, dependencies, cycles, duplicate ids, acceptance) | `0` valid · `1` violations |
|
|
220
233
|
| `yoke change add\|status [dir] [--idea=]` | Queue a change at any time; the loop turns it into append-only stories at the next safe boundary | `0` · `1` invalid inbox/request |
|
|
221
234
|
| `yoke context init\|status [dir]` | Durable context layer (`PROJECT/DECISIONS/KNOWLEDGE/GLOSSARY.md`, optional `CONTEXT-MAP.md`) | `0` |
|
|
222
|
-
| `yoke loop on\|off\|status\|decision\|answer\|resume\|run\|cleanup [dir]` | Autonomous loop; `run`
|
|
235
|
+
| `yoke loop on\|off\|status\|pause\|decision\|answer\|resume\|run\|cleanup [dir]` | Autonomous loop; `run --explore` opts into continuous discovery, implementation and recovery after a backlog drains; `--parallel=N`, bounded reference-driven `--quality`, and blind `--candidates=N` selection remain available; `--max=N` is an intentional cap; `pause` requests a safe-boundary stop | run: `0` complete · `1` blocked/cap · `2` not runnable / already locked · `3` paused |
|
|
223
236
|
| `yoke review [dir] [--reviewer=] [--base=] [--focus=] [--json] [--allow-self-review]` | An independent model writes a schema-valid verdict | `0` approved · `1` findings/invalid verdict · `2` no independent reviewer |
|
|
224
237
|
| `yoke audit [dir] [--json]` | Dependency, high-confidence secret, and sensitive-change audit | `0` green · `1` blocking findings · `2` not runnable |
|
|
225
238
|
| `yoke design-scan [dir] [--max=N] [--report]` | Static AI-slop design gate | `0` within budget · `1` over |
|
|
@@ -939,7 +952,7 @@ release provenance.
|
|
|
939
952
|
## 🧪 Development
|
|
940
953
|
|
|
941
954
|
```bash
|
|
942
|
-
npm test # vitest (
|
|
955
|
+
npm test # vitest (1313 tests)
|
|
943
956
|
npm run build # tsc, no emit errors
|
|
944
957
|
npm run yoke -- validate canon
|
|
945
958
|
```
|
package/canon/loop/loop-spec.md
CHANGED
|
@@ -4,7 +4,8 @@ The autonomous loop is optional and toggle-able:
|
|
|
4
4
|
|
|
5
5
|
- `yoke loop on` / `yoke loop off` — enable or disable it in `.yoke/config.yaml`.
|
|
6
6
|
- `yoke loop status` — show enabled state and backlog progress.
|
|
7
|
-
- `yoke loop run [--max=N] [--parallel=N] [--isolate] [--decision-policy=auto|critical] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=blocking|advisory] [--quality-unbounded] [--candidates=N]` — run until the
|
|
7
|
+
- `yoke loop run [--max=N] [--parallel=N] [--isolate] [--explore] [--explore-interval=N] [--decision-policy=auto|critical] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=blocking|advisory] [--quality-unbounded] [--candidates=N]` — run until the backlog is green or a gate blocks; `--explore` opts into persistent discovery and recovery after each backlog drains.
|
|
8
|
+
- `yoke loop pause` — request a safe-boundary pause from a running loop or exploration supervisor.
|
|
8
9
|
- `yoke change add --idea="..."` — queue a product change at any time, including while the loop is running.
|
|
9
10
|
- `yoke loop decision` / `yoke loop answer --choice=<id>` — inspect and answer a structured critical stop.
|
|
10
11
|
|
|
@@ -13,6 +14,10 @@ story is fast-forwarded to the main tree. Pass `--review` or `--reviewer=<provid
|
|
|
13
14
|
a separate, schema-validated review. Pass `--parallel=N` to dispatch ready, non-colliding stories
|
|
14
15
|
concurrently. Pass `--json` for NDJSON status on stdout.
|
|
15
16
|
|
|
17
|
+
With `--explore`, `--explore-limit=12h|3d|2w` optionally stops one invocation after the selected
|
|
18
|
+
duration. The default is unbounded. Expiry stops new work and pauses after active stories complete
|
|
19
|
+
their normal gates and integration; exit code `3` can be resumed with another `yoke loop run`.
|
|
20
|
+
|
|
16
21
|
Stories may declare a reference, candidate artifact, rubric, and blocking/advisory quality policy.
|
|
17
22
|
`--quality` runs a read-only blind critic plus bounded repair before review; every repair reruns the
|
|
18
23
|
mechanical gates. `--candidates=N` requires quality declarations and dispatches multiple isolated
|
|
@@ -48,12 +53,24 @@ For each story:
|
|
|
48
53
|
7. Only after all gates pass, mark the story `passes: true`, log the decision, and commit
|
|
49
54
|
atomically. A failed commit restores the PRD state.
|
|
50
55
|
8. When all current stories pass, run optional `completion.command` against the integrated
|
|
51
|
-
system. Only a green result reports `complete`; otherwise the loop blocks.
|
|
52
|
-
|
|
56
|
+
system. Only a green result reports `complete`; otherwise the loop blocks. In `--explore` mode
|
|
57
|
+
a green result starts a read-only exploration pass instead of ending the supervisor. A red
|
|
58
|
+
completion gate is supplied as evidence to the explorer so it can propose work to resolve the
|
|
59
|
+
blocker; the gate is never skipped.
|
|
60
|
+
9. Exploration proposals require repository-file evidence, confidence of at least 0.8, low/medium
|
|
61
|
+
risk, disjoint declared write scopes, and two to five structured acceptance criteria with
|
|
62
|
+
criterion-targeted approved test commands. Yoke validates and commits accepted PRD changes
|
|
63
|
+
before isolated implementation. Passed exploration stories are compacted from the active PRD;
|
|
64
|
+
their full prior contracts remain in Git history and a bounded recent fingerprint ledger
|
|
65
|
+
prevents near-term duplication.
|
|
53
66
|
|
|
54
67
|
A supervisor can pause the loop by creating `.yoke/loop.pause`. The running story finishes;
|
|
55
68
|
the dispatcher latches the signal, stops launching new workers, lets active workers reach safe
|
|
56
69
|
terminal proof/cleanup, and exits with code `3` before another story is integrated.
|
|
70
|
+
`--explore` also checks this signal while waiting between scans and during retry backoff. Without
|
|
71
|
+
an explicit `--max` cap, draining the current PRD does not stop exploration; the supervisor waits,
|
|
72
|
+
retries recoverable failures with backoff, and reports the next action. It cannot survive forced
|
|
73
|
+
process or machine termination.
|
|
57
74
|
|
|
58
75
|
State lives outside model context: PRD, git, and the ignored `.yoke/changes/` inbox. All are
|
|
59
76
|
re-read at story boundaries, so a request queued mid-run becomes additional stories without a
|
package/canon/manifest.yaml
CHANGED
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createProjectGoal, readProjectGoal, runProjectGoal, pauseProjectGoal, g
|
|
|
9
9
|
import { validateCanon } from './canon/validate.js';
|
|
10
10
|
import { runRetrofit } from './retrofit/command.js';
|
|
11
11
|
import { setLoopEnabled, loopStatus, runLoopCommand } from './loop/run-command.js';
|
|
12
|
+
import { requestLoopPause } from './loop/loop.js';
|
|
12
13
|
import { runContextInit, runContextStatus } from './context/command.js';
|
|
13
14
|
import { runReview } from './review/command.js';
|
|
14
15
|
import { scanDir } from './scan/design.js';
|
|
@@ -103,6 +104,33 @@ export function parseQualityFlags(args) {
|
|
|
103
104
|
},
|
|
104
105
|
};
|
|
105
106
|
}
|
|
107
|
+
export function parseExploreLimit(value) {
|
|
108
|
+
const match = /^(\d+)\s*(h|hours?|d|days?|w|weeks?)$/i.exec(value.trim());
|
|
109
|
+
if (!match)
|
|
110
|
+
return { ok: false, error: `Invalid --explore-limit value: ${value} (expected a positive duration such as 12h, 3d, or 2w)` };
|
|
111
|
+
const amount = Number(match[1]);
|
|
112
|
+
const unit = match[2].toLowerCase();
|
|
113
|
+
const unitMs = unit.startsWith('h') ? 60 * 60_000 : unit.startsWith('d') ? 24 * 60 * 60_000 : 7 * 24 * 60 * 60_000;
|
|
114
|
+
const milliseconds = amount * unitMs;
|
|
115
|
+
if (!Number.isSafeInteger(amount) || amount <= 0 || !Number.isSafeInteger(milliseconds) || Date.now() + milliseconds > 8_640_000_000_000_000) {
|
|
116
|
+
return { ok: false, error: `Invalid --explore-limit value: ${value} (duration must be positive and within the supported date range)` };
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, milliseconds };
|
|
119
|
+
}
|
|
120
|
+
function parseExploreLimitFlag(args, explore) {
|
|
121
|
+
const values = args.filter(arg => arg.startsWith('--explore-limit='));
|
|
122
|
+
if (args.includes('--explore-limit'))
|
|
123
|
+
return { ok: false, error: '--explore-limit requires a duration, for example --explore-limit=12h' };
|
|
124
|
+
if (values.length > 1)
|
|
125
|
+
return { ok: false, error: 'Use --explore-limit only once per invocation' };
|
|
126
|
+
const raw = values[0];
|
|
127
|
+
if (!raw)
|
|
128
|
+
return { ok: true };
|
|
129
|
+
if (!explore)
|
|
130
|
+
return { ok: false, error: '--explore-limit requires --explore' };
|
|
131
|
+
const parsed = parseExploreLimit(raw.slice('--explore-limit='.length));
|
|
132
|
+
return parsed.ok ? { ok: true, milliseconds: parsed.milliseconds } : parsed;
|
|
133
|
+
}
|
|
106
134
|
export function main(argv) {
|
|
107
135
|
const [cmd, ...rest] = argv;
|
|
108
136
|
switch (cmd) {
|
|
@@ -358,6 +386,11 @@ export function main(argv) {
|
|
|
358
386
|
console.log(loopStatus(targetDir));
|
|
359
387
|
return 0;
|
|
360
388
|
}
|
|
389
|
+
if (sub === 'pause') {
|
|
390
|
+
requestLoopPause(targetDir);
|
|
391
|
+
console.log('Pause requested at the next safe story or exploration boundary.');
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
361
394
|
if (sub === 'cleanup')
|
|
362
395
|
return runLoopCleanup(targetDir, {
|
|
363
396
|
removeWorktrees: rest.includes('--remove-worktrees'),
|
|
@@ -414,7 +447,23 @@ export function main(argv) {
|
|
|
414
447
|
console.error(qualityFlags.error);
|
|
415
448
|
return 1;
|
|
416
449
|
}
|
|
417
|
-
|
|
450
|
+
const explore = rest.includes('--explore');
|
|
451
|
+
const exploreLimit = parseExploreLimitFlag(rest, explore);
|
|
452
|
+
if (!exploreLimit.ok) {
|
|
453
|
+
console.error(exploreLimit.error);
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
const exploreArg = rest.find(a => a.startsWith('--explore-interval='));
|
|
457
|
+
if (exploreArg && !explore) {
|
|
458
|
+
console.error('--explore-interval requires --explore');
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
const exploreIntervalMinutes = exploreArg ? Number(exploreArg.slice('--explore-interval='.length)) : undefined;
|
|
462
|
+
if (exploreIntervalMinutes !== undefined && (!Number.isInteger(exploreIntervalMinutes) || exploreIntervalMinutes < 1 || exploreIntervalMinutes > 1_440)) {
|
|
463
|
+
console.error(`Invalid --explore-interval value: ${exploreArg} (expected 1..1440 minutes)`);
|
|
464
|
+
return 1;
|
|
465
|
+
}
|
|
466
|
+
return runLoopCommand(targetDir, { ...resumeOptions, ...qualityFlags.options, ...(explore ? { explore: true } : {}), ...(exploreIntervalMinutes !== undefined ? { exploreIntervalMinutes } : {}), ...(exploreLimit.milliseconds !== undefined ? { exploreLimitMs: exploreLimit.milliseconds } : {}) });
|
|
418
467
|
}
|
|
419
468
|
if (sub === 'answer') {
|
|
420
469
|
const choice = rest.find(a => a.startsWith('--choice='))?.slice('--choice='.length);
|
|
@@ -543,9 +592,29 @@ export function main(argv) {
|
|
|
543
592
|
console.error(qualityFlags.error);
|
|
544
593
|
return 1;
|
|
545
594
|
}
|
|
546
|
-
|
|
595
|
+
const explore = rest.includes('--explore');
|
|
596
|
+
const exploreLimit = parseExploreLimitFlag(rest, explore);
|
|
597
|
+
if (!exploreLimit.ok) {
|
|
598
|
+
console.error(exploreLimit.error);
|
|
599
|
+
return 1;
|
|
600
|
+
}
|
|
601
|
+
const exploreArg = rest.find(a => a.startsWith('--explore-interval='));
|
|
602
|
+
if (exploreArg && !explore) {
|
|
603
|
+
console.error('--explore-interval requires --explore');
|
|
604
|
+
return 1;
|
|
605
|
+
}
|
|
606
|
+
const exploreIntervalMinutes = exploreArg ? Number(exploreArg.slice('--explore-interval='.length)) : undefined;
|
|
607
|
+
if (exploreIntervalMinutes !== undefined && (!Number.isInteger(exploreIntervalMinutes) || exploreIntervalMinutes < 1 || exploreIntervalMinutes > 1_440)) {
|
|
608
|
+
console.error(`Invalid --explore-interval value: ${exploreArg} (expected 1..1440 minutes)`);
|
|
609
|
+
return 1;
|
|
610
|
+
}
|
|
611
|
+
if (explore && rest.includes('--resume-worktree')) {
|
|
612
|
+
console.error('--explore starts its own safe resume cycle; remove --resume-worktree and re-run it after resolving the retained story.');
|
|
613
|
+
return 1;
|
|
614
|
+
}
|
|
615
|
+
return runLoopCommand(targetDir, { maxIterations: rawMax, agent, isolate, resumeWorktree: rest.includes('--resume-worktree'), parallel, parallelAuto: parallelArg === '--parallel=auto', reviewer, review, allowSelfReview, timeoutMinutes, json, routing, onAmbiguity: oaArg, decisionPolicy: dpArg, permissions, ...(explore ? { explore: true } : {}), ...(exploreIntervalMinutes !== undefined ? { exploreIntervalMinutes } : {}), ...(exploreLimit.milliseconds !== undefined ? { exploreLimitMs: exploreLimit.milliseconds } : {}), ...qualityFlags.options });
|
|
547
616
|
}
|
|
548
|
-
console.log(`usage: yoke loop <on|off|status|decision|answer|resume [--discard] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
|
|
617
|
+
console.log(`usage: yoke loop <on|off|status|pause|decision|answer|resume [--discard] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
|
|
549
618
|
return 1;
|
|
550
619
|
}
|
|
551
620
|
case 'new': {
|
package/dist/loop/git.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { sanitizeCommitMessage } from './identity.js';
|
|
3
|
-
export const RUNTIME_PATHS = [".yoke/supervision", ".yoke/provider-processes", '.yoke/artifacts', '.yoke/events', '.yoke/history', '.yoke/routing', '.yoke/checks', '.yoke/goal.json', '.yoke/goal.pause', '.yoke/loop-status.json', '.yoke/loop.lock', '.yoke/loop.lock.takeover', '.yoke/loop.lock.takeover.recovery', '.yoke/runner.pid', '.yoke/story-durations.json'];
|
|
3
|
+
export const RUNTIME_PATHS = [".yoke/supervision", ".yoke/provider-processes", '.yoke/artifacts', '.yoke/events', '.yoke/history', '.yoke/routing', '.yoke/checks', '.yoke/goal.json', '.yoke/goal.pause', '.yoke/loop.pause', '.yoke/loop-status.json', '.yoke/loop.lock', '.yoke/loop.lock.takeover', '.yoke/loop.lock.takeover.recovery', '.yoke/runner.pid', '.yoke/story-durations.json'];
|
|
4
4
|
// Directory pathspecs include descendants and also exclude the directory entry.
|
|
5
5
|
export const RUNTIME_EXCLUDES = RUNTIME_PATHS.map(path => `:(exclude)${path}`);
|
|
6
6
|
export function stageImplementation(dir) {
|
package/dist/loop/loop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { knownInfrastructureFailure } from "../routing/capability.js";
|
|
2
|
-
import { existsSync, unlinkSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, unlinkSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { acceptanceProtectionProblem } from '../check/command.js';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { isAcceptanceCriterion, loadPrd, savePrd, selectNextStory, allPass, progress, storyPathSegment } from './prd.js';
|
|
@@ -102,6 +102,18 @@ function runQualityReview(opts, executionDir, story, reporter) {
|
|
|
102
102
|
export function pauseFilePath(targetDir) {
|
|
103
103
|
return join(targetDir, '.yoke', 'loop.pause');
|
|
104
104
|
}
|
|
105
|
+
/** Request a pause at the next safe story or exploration boundary. */
|
|
106
|
+
export function requestLoopPause(targetDir) {
|
|
107
|
+
const file = pauseFilePath(targetDir);
|
|
108
|
+
mkdirSync(join(targetDir, '.yoke'), { recursive: true });
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(file, `${new Date().toISOString()}\n`, { flag: 'wx' });
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error.code !== 'EEXIST')
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
105
117
|
function consumePause(targetDir) {
|
|
106
118
|
const file = pauseFilePath(targetDir);
|
|
107
119
|
if (!existsSync(file))
|
package/dist/loop/prd.js
CHANGED
|
@@ -11,6 +11,15 @@ export const AcceptanceCriterionSchema = z.object({
|
|
|
11
11
|
text: z.string().min(1),
|
|
12
12
|
verify: z.array(z.string().min(1)).min(1),
|
|
13
13
|
});
|
|
14
|
+
export const ExplorationOriginSchema = z.object({
|
|
15
|
+
version: z.literal(1),
|
|
16
|
+
rationale: z.string().min(1).max(2_000),
|
|
17
|
+
expectedBenefit: z.string().min(1).max(2_000),
|
|
18
|
+
confidence: z.number().min(0.8).max(1),
|
|
19
|
+
risk: z.enum(['low', 'medium']),
|
|
20
|
+
evidence: z.array(z.object({ path: z.string().min(1).max(500), observation: z.string().min(1).max(1_000) }).strict()).min(1).max(6),
|
|
21
|
+
discoveredAt: z.string().datetime(),
|
|
22
|
+
}).strict();
|
|
14
23
|
export function criterionCommandProblem(criterion) {
|
|
15
24
|
const normalizedId = criterion.id.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
16
25
|
for (const raw of criterion.verify) {
|
|
@@ -46,6 +55,8 @@ export const StorySchema = z.object({
|
|
|
46
55
|
agent: AgentSchema.optional(),
|
|
47
56
|
/** Inbox request that created this story. Used for idempotent append-only intake. */
|
|
48
57
|
sourceChange: z.string().min(1).optional(),
|
|
58
|
+
/** Evidence retained for stories created by opt-in continuous exploration. */
|
|
59
|
+
exploration: ExplorationOriginSchema.optional(),
|
|
49
60
|
quality: StoryQualityDeclarationSchema.optional(),
|
|
50
61
|
assessment: AssessmentSchema.optional(),
|
|
51
62
|
/** Binding to the task, upstream contracts and approved planning brief. */
|
package/dist/loop/reporter.js
CHANGED
|
@@ -220,10 +220,12 @@ export function makeReporter(dir, opts = {}, now = () => new Date()) {
|
|
|
220
220
|
persist({ ...base, state: 'running', phase: undefined, story: story.id, storyTitle: story.title,
|
|
221
221
|
progress, ...(eta ? { eta } : {}), updatedAt: now().toISOString() }, 'story-done', `✓ ${story.id} done${took} — ${progress.passed}/${progress.total} (${percentOf(progress)}%)${hint}`);
|
|
222
222
|
},
|
|
223
|
-
phase(phase) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
phase(phase, reason, progress) {
|
|
224
|
+
const status = current ?? emptyStatus(now().toISOString());
|
|
225
|
+
const base = phase === 'exploring' || phase === 'waiting-exploration' || phase === 'waiting-recovery'
|
|
226
|
+
? (({ story: _story, storyTitle: _storyTitle, ...withoutStory }) => withoutStory)(status)
|
|
227
|
+
: status;
|
|
228
|
+
persist({ ...base, state: 'running', phase, ...(progress ? { progress } : {}), ...(reason ? { reason } : { reason: undefined }), updatedAt: now().toISOString() }, phase, ` · ${phase}…`);
|
|
227
229
|
},
|
|
228
230
|
blocked(reason) {
|
|
229
231
|
const base = current ?? emptyStatus(now().toISOString());
|
|
@@ -237,9 +239,9 @@ export function makeReporter(dir, opts = {}, now = () => new Date()) {
|
|
|
237
239
|
persist({ ...withoutParallel(current ?? emptyStatus(now().toISOString())), state: 'cap-reached', phase: undefined,
|
|
238
240
|
progress, updatedAt: now().toISOString() }, 'cap-reached', `◾ iteration cap reached — ${progress.passed}/${progress.total}`);
|
|
239
241
|
},
|
|
240
|
-
paused(progress) {
|
|
242
|
+
paused(progress, reason) {
|
|
241
243
|
persist({ ...withoutParallel(current ?? emptyStatus(now().toISOString())), state: 'paused', phase: undefined,
|
|
242
|
-
progress, updatedAt: now().toISOString() }, 'paused', `⏸ loop paused — ${progress.passed}/${progress.total}`);
|
|
244
|
+
progress, ...(reason ? { reason } : {}), updatedAt: now().toISOString() }, 'paused', `⏸ loop paused — ${progress.passed}/${progress.total}`);
|
|
243
245
|
},
|
|
244
246
|
quality(status) {
|
|
245
247
|
const base = current ?? emptyStatus(now().toISOString());
|