@hecer/yoke 1.16.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 +36 -0
- package/README.md +32 -14
- package/bench/README.md +10 -0
- package/bench/RESULTS.md +23 -0
- package/bench/fixtures/parallel-work/scenarios.json +33 -0
- package/bench/run-parallel-matrix.mjs +71 -0
- package/canon/loop/loop-spec.md +20 -3
- package/canon/manifest.yaml +1 -1
- package/dist/cli.js +96 -7
- package/dist/dashboard/panels.js +3 -1
- package/dist/dashboard/server.js +11 -2
- package/dist/estimation/schedule.js +96 -33
- package/dist/loop/claim-lease.js +1 -1
- package/dist/loop/dispatcher.js +76 -26
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +13 -1
- package/dist/loop/parallel-command.js +21 -0
- package/dist/loop/prd.js +11 -0
- package/dist/loop/reporter.js +27 -7
- package/dist/loop/resource-pool.js +369 -0
- package/dist/loop/run-command.js +436 -23
- package/dist/prd/decompose.js +232 -0
- package/dist/prd/explore.js +441 -0
- package/dist/retrofit/config.js +1 -1
- package/docs/CONTINUOUS-EXPLORATION.md +67 -0
- package/docs/VERIFIED-PROJECTS.md +4 -2
- package/docs/parallel-execution.md +130 -0
- package/docs/superpowers/plans/2026-09-27-parallel-task-efficiency.md +65 -0
- package/gemini-extension.json +1 -1
- package/package.json +2 -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,41 @@
|
|
|
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
|
+
|
|
18
|
+
## 1.17.0 — 2026-09-27
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Coordinate weighted worker reservations across concurrent Yoke projects under a user-local pool. The default is three units, `YOKE_MAX_PARALLEL_WORKERS` accepts 1–8, and candidate races consume one unit per simultaneous candidate.
|
|
22
|
+
- Add `yoke prd decompose --story=<id>` as a preview-first planner for splitting eligible stories into two independently scheduled children; `--apply` atomically replaces the parent and rewrites downstream dependencies after hash and schema checks.
|
|
23
|
+
- Report shared capacity and resource waits in loop status and the dashboard. Record integration queue wait and duration, and forecast dependency-aware implementation work alongside a serialized integration lane.
|
|
24
|
+
- Add a deterministic local dispatcher benchmark matrix for dependency chains, independent scopes and conflicting scopes.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Separate implementation slots from each project's integration queue. A completed candidate frees its implementation slot while retaining collision areas and write scopes through rebase, integrated-tree gates, commit and cleanup.
|
|
28
|
+
- Keep single-worker runs on the serial path and reserve a shared unit around each agent invocation; use the dispatcher for multiple workers and competing candidate runs.
|
|
29
|
+
- Cap explicit and configured per-project concurrency at eight. Automatic concurrency remains conservative and is now bounded by the shared cross-project limit.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Release the shared claim-operation lease when pool-state parsing throws, so corrupt records fail closed without leaving later pool access stuck behind a leaked lock.
|
|
33
|
+
|
|
34
|
+
### Migration and validation limits
|
|
35
|
+
- Existing `loop.parallel` values above 8 must be lowered. The default shared cap can queue previously simultaneous project loops; set `YOKE_MAX_PARALLEL_WORKERS` to 1–8 for the desired user-level ceiling. Existing active workers are not cancelled when the cap is lowered.
|
|
36
|
+
- Decomposition requires at least four structured criteria, at least two non-overlapping write scopes and no shared area. Child assessments/quality declarations need task-specific refresh; write scopes remain advisory.
|
|
37
|
+
- Synthetic benchmark results use fixed local delays and make no model quality, token or provider-cost claims. Local release checks: 1,296 tests passed, two skipped across 143 files; lint/build, docs checks, package dry run, Canon validation and dependency audit passed.
|
|
38
|
+
|
|
3
39
|
## 1.16.0 — 2026-09-17
|
|
4
40
|
|
|
5
41
|
### 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
|
|
|
@@ -69,7 +69,7 @@ in private, content-addressed local artifacts. Existing projects keep their seri
|
|
|
69
69
|
safe 2 KiB preview / 8 KiB artifact defaults unless configured otherwise.
|
|
70
70
|
|
|
71
71
|
Yoke 1.4 introduced opt-in parallel workers and a bounded, reference-driven quality gauntlet.
|
|
72
|
-
Yoke 1.8.0
|
|
72
|
+
Automatic parallelism was introduced in Yoke 1.8.0. This unreleased branch adds a shared cross-project worker pool. See [the 1.4 migration guide](docs/MIGRATING-TO-1.4.md)
|
|
73
73
|
for the new flags, configuration, cleanup behavior, and review-verdict contract.
|
|
74
74
|
|
|
75
75
|
Yoke 1.1 is safe-by-default: provider CLIs use autonomous sandbox profiles unless `--unsafe`
|
|
@@ -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 |
|
|
@@ -464,11 +477,15 @@ observability.
|
|
|
464
477
|
|
|
465
478
|
### Parallel workers and the quality gauntlet
|
|
466
479
|
|
|
467
|
-
`--parallel=N` dispatches dependency-ready stories concurrently.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
480
|
+
`--parallel=N` dispatches dependency-ready stories concurrently. The default maximum is three
|
|
481
|
+
shared worker units across Yoke projects; set `YOKE_MAX_PARALLEL_WORKERS=1..8` to change it.
|
|
482
|
+
`--parallel` and `loop.parallel` accept 1–8. Candidate races consume one unit per simultaneous
|
|
483
|
+
candidate. Claims carry leases, workers use isolated worktrees, collision areas and write scopes stay
|
|
484
|
+
reserved through integration, and only a mechanically green candidate enters the FIFO integration
|
|
485
|
+
queue. Integration has its own serialized lane, so it does not occupy an implementation slot while
|
|
486
|
+
unrelated work proceeds. Integrated-tree gates remain mandatory. `yoke loop status` reports local
|
|
487
|
+
workers, resource waits, shared capacity, integrations, and reopened stories. See
|
|
488
|
+
[parallel execution and safe task decomposition](docs/parallel-execution.md).
|
|
472
489
|
|
|
473
490
|
Quality is reference-driven and opt-in. Declare what one story should match:
|
|
474
491
|
|
|
@@ -655,8 +672,9 @@ cannot overwrite a shared registry file.
|
|
|
655
672
|
|
|
656
673
|
Routing is not free: stories without a matching rule can add a controller call. Measure it
|
|
657
674
|
on your own backlog rather than assuming a win. Routing now also runs within asynchronous
|
|
658
|
-
parallel workers. Automatic parallelism
|
|
659
|
-
write scopes;
|
|
675
|
+
parallel workers. Automatic parallelism uses up to the shared worker limit when pending tasks declare
|
|
676
|
+
write scopes; the scheduler still serializes dependencies, collision areas, and overlapping scopes.
|
|
677
|
+
Unknown scopes and configured tool actions keep automatic execution serial. Isolation is
|
|
660
678
|
on by default. Explicit `--parallel=N`, `--no-routing` and `--no-isolate` remain available.
|
|
661
679
|
See [execution defaults and dashboard measurement details](docs/VERIFIED-PROJECTS.md#execution-defaults-in-180).
|
|
662
680
|
|
|
@@ -934,7 +952,7 @@ release provenance.
|
|
|
934
952
|
## 🧪 Development
|
|
935
953
|
|
|
936
954
|
```bash
|
|
937
|
-
npm test # vitest (
|
|
955
|
+
npm test # vitest (1313 tests)
|
|
938
956
|
npm run build # tsc, no emit errors
|
|
939
957
|
npm run yoke -- validate canon
|
|
940
958
|
```
|
package/bench/README.md
CHANGED
|
@@ -42,6 +42,7 @@ node bench/run-large.mjs --seed=G:\NN-Developed\Yoke-Testground\yoke-codex-study
|
|
|
42
42
|
node bench/analyze-routing-study.mjs
|
|
43
43
|
node bench/run-matrix.mjs --label=release-1.0
|
|
44
44
|
node bench/output-compaction.mjs # deterministic local gate-output benchmark; no provider call
|
|
45
|
+
node bench/run-parallel-matrix.mjs # synthetic dispatcher; deterministic local delays, no provider call
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
Each run copies the fixture to `bench/.runs/<fixture>-<runner>-<routing>-<stamp>` (or the
|
|
@@ -66,6 +67,15 @@ visible test from turning into false benchmark evidence. A seed can provide `ben
|
|
|
66
67
|
to declare its fixture identity and hidden-test files. `analyze-routing-study.mjs` validates and
|
|
67
68
|
aggregates the checked-in three-pair Codex-only study.
|
|
68
69
|
|
|
70
|
+
### Parallel dispatcher matrix
|
|
71
|
+
|
|
72
|
+
`run-parallel-matrix.mjs` exercises the real dispatcher with a deterministic local worker over a
|
|
73
|
+
dependency chain, independent write scopes and conflicting scopes at concurrency 1, 2 and 3. It
|
|
74
|
+
reports wall time, summed worker time, integration queue wait, integration time, attempts and
|
|
75
|
+
accepted results. Fixed delays make scheduler comparisons repeatable; these rows do not measure
|
|
76
|
+
provider latency, token cost, model quality, global cross-project contention or large-repository
|
|
77
|
+
work. Run `npm run build` first so `dist/loop/dispatcher.js` exists.
|
|
78
|
+
|
|
69
79
|
## Caveats (read before quoting numbers)
|
|
70
80
|
|
|
71
81
|
- Agent runs are stochastic. Older rows are N=1; the 2026-08-02 Codex-only study uses three
|
package/bench/RESULTS.md
CHANGED
|
@@ -6,6 +6,29 @@ availability, verdict/blocker, conflicts, wall time, iterations, and final fixtu
|
|
|
6
6
|
Fixture `string-kit` (3 stories, 16 pre-written assertions). Methodology and caveats:
|
|
7
7
|
[README.md](README.md). One row per run — raw JSON in [`results/`](results/).
|
|
8
8
|
|
|
9
|
+
## Synthetic dispatcher matrix (2026-09-27)
|
|
10
|
+
|
|
11
|
+
One local run of `node bench/run-parallel-matrix.mjs` after the shared integration pipeline was
|
|
12
|
+
implemented. Each case uses four stories, fixed 120 ms worker and 60 ms integration delays, and no
|
|
13
|
+
provider calls. The measurements exercise dispatcher scheduling only; they are not model or
|
|
14
|
+
repository performance evidence.
|
|
15
|
+
|
|
16
|
+
| Scenario | Slots | Wall ms | Worker ms (summed) | Integration queue wait ms | Integration ms | Attempts | Accepted |
|
|
17
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
18
|
+
| Dependency chain | 1 | 788 | 511 | 0 | 271 | 4 | 4 |
|
|
19
|
+
| Dependency chain | 2 | 762 | 498 | 0 | 262 | 4 | 4 |
|
|
20
|
+
| Dependency chain | 3 | 766 | 501 | 0 | 262 | 4 | 4 |
|
|
21
|
+
| Independent scopes | 1 | 566 | 494 | 0 | 274 | 4 | 4 |
|
|
22
|
+
| Independent scopes | 2 | 396 | 512 | 170 | 274 | 4 | 4 |
|
|
23
|
+
| Independent scopes | 3 | 397 | 492 | 258 | 272 | 4 | 4 |
|
|
24
|
+
| Conflicting scopes | 1 | 743 | 492 | 0 | 251 | 4 | 4 |
|
|
25
|
+
| Conflicting scopes | 2 | 753 | 492 | 0 | 262 | 4 | 4 |
|
|
26
|
+
| Conflicting scopes | 3 | 742 | 493 | 0 | 247 | 4 | 4 |
|
|
27
|
+
|
|
28
|
+
The independent-scope case was about 30% shorter at two or three slots in this one synthetic run.
|
|
29
|
+
Dependency and conflict cases remained effectively serial, as their constraints require. Repeat the
|
|
30
|
+
matrix before drawing conclusions from small timing differences.
|
|
31
|
+
|
|
9
32
|
## Runs
|
|
10
33
|
|
|
11
34
|
| Date | Runner | Model (reported) | Result | Wall-clock | Input tok | Output tok | First-pass stories | src LOC |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"workerDurationMs": 120,
|
|
3
|
+
"integrationDurationMs": 60,
|
|
4
|
+
"scenarios": [
|
|
5
|
+
{
|
|
6
|
+
"id": "dependency-chain",
|
|
7
|
+
"stories": [
|
|
8
|
+
{ "id": "chain-1", "writes": ["src/chain/one.ts"] },
|
|
9
|
+
{ "id": "chain-2", "needs": ["chain-1"], "writes": ["src/chain/two.ts"] },
|
|
10
|
+
{ "id": "chain-3", "needs": ["chain-2"], "writes": ["src/chain/three.ts"] },
|
|
11
|
+
{ "id": "chain-4", "needs": ["chain-3"], "writes": ["src/chain/four.ts"] }
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "independent-scopes",
|
|
16
|
+
"stories": [
|
|
17
|
+
{ "id": "independent-1", "writes": ["src/independent/one.ts"] },
|
|
18
|
+
{ "id": "independent-2", "writes": ["src/independent/two.ts"] },
|
|
19
|
+
{ "id": "independent-3", "writes": ["src/independent/three.ts"] },
|
|
20
|
+
{ "id": "independent-4", "writes": ["src/independent/four.ts"] }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "conflicting-scopes",
|
|
25
|
+
"stories": [
|
|
26
|
+
{ "id": "conflict-1", "writes": ["src/shared/registry.ts"] },
|
|
27
|
+
{ "id": "conflict-2", "writes": ["src/shared/registry.ts"] },
|
|
28
|
+
{ "id": "conflict-3", "writes": ["src/shared/registry.ts"] },
|
|
29
|
+
{ "id": "conflict-4", "writes": ["src/shared/registry.ts"] }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { performance } from 'node:perf_hooks'
|
|
3
|
+
import { createDispatcher } from '../dist/loop/dispatcher.js'
|
|
4
|
+
|
|
5
|
+
const fixture = JSON.parse(readFileSync(new URL('./fixtures/parallel-work/scenarios.json', import.meta.url), 'utf8'))
|
|
6
|
+
const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
|
|
7
|
+
|
|
8
|
+
async function runCase(scenario, maxConcurrency) {
|
|
9
|
+
let workerTimeMs = 0
|
|
10
|
+
let integrationQueueWaitMs = 0
|
|
11
|
+
let integrationTimeMs = 0
|
|
12
|
+
const dispatcher = createDispatcher({
|
|
13
|
+
targetDir: process.cwd(),
|
|
14
|
+
stories: scenario.stories.map(story => ({ priority: 1, acceptance: [], passes: false, ...story })),
|
|
15
|
+
maxConcurrency,
|
|
16
|
+
maxIterations: Infinity,
|
|
17
|
+
worker: async input => {
|
|
18
|
+
const started = performance.now()
|
|
19
|
+
await delay(fixture.workerDurationMs)
|
|
20
|
+
workerTimeMs += performance.now() - started
|
|
21
|
+
return {
|
|
22
|
+
kind: 'candidate',
|
|
23
|
+
storyId: input.story.id,
|
|
24
|
+
worktree: input.worktree.path,
|
|
25
|
+
baseCommit: input.worktree.baseCommit,
|
|
26
|
+
provider: input.provider,
|
|
27
|
+
summary: 'synthetic worker complete',
|
|
28
|
+
evidence: { criteria: [], verify: { passed: true, summary: 'synthetic' } },
|
|
29
|
+
routing: { outcome: 'pending-integration', recordOutcome() {} },
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
claims: { acquire: () => true, heartbeat() {}, release: () => true },
|
|
33
|
+
worktrees: {
|
|
34
|
+
create: input => ({ path: `virtual/${input.story.id}`, baseCommit: `base-${input.story.id}` }),
|
|
35
|
+
remove() {},
|
|
36
|
+
},
|
|
37
|
+
git: {
|
|
38
|
+
isClean: () => true,
|
|
39
|
+
rebase: () => ({ kind: 'rebased', expectedHead: 'synthetic-head' }),
|
|
40
|
+
commit() {},
|
|
41
|
+
integrate: async () => { await delay(fixture.integrationDurationMs) },
|
|
42
|
+
},
|
|
43
|
+
gates: { verify: () => ({ passed: true, summary: 'synthetic gate passed' }) },
|
|
44
|
+
onIntegrationMetrics: (_worker, queueWait, integrationMs) => {
|
|
45
|
+
if (integrationMs === undefined) integrationQueueWaitMs += queueWait
|
|
46
|
+
else integrationTimeMs += integrationMs
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
const started = performance.now()
|
|
50
|
+
const result = await dispatcher.run()
|
|
51
|
+
return {
|
|
52
|
+
scenario: scenario.id,
|
|
53
|
+
workers: maxConcurrency,
|
|
54
|
+
wallMs: Math.round(performance.now() - started),
|
|
55
|
+
workerMs: Math.round(workerTimeMs),
|
|
56
|
+
queueWaitMs: Math.round(integrationQueueWaitMs),
|
|
57
|
+
integrationMs: Math.round(integrationTimeMs),
|
|
58
|
+
attempts: result.iterations,
|
|
59
|
+
accepted: result.integrated.length,
|
|
60
|
+
status: result.status,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log('Synthetic dispatcher matrix; fixed local delays, no model calls or provider quality claims.')
|
|
65
|
+
console.log('scenario,workers,wall_ms,worker_ms,integration_queue_wait_ms,integration_ms,attempts,accepted,status')
|
|
66
|
+
for (const scenario of fixture.scenarios) {
|
|
67
|
+
for (const workers of [1, 2, 3]) {
|
|
68
|
+
const result = await runCase(scenario, workers)
|
|
69
|
+
console.log([result.scenario, result.workers, result.wallMs, result.workerMs, result.queueWaitMs, result.integrationMs, result.attempts, result.accepted, result.status].join(','))
|
|
70
|
+
}
|
|
71
|
+
}
|
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,12 +9,14 @@ 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';
|
|
15
16
|
import { runNew } from './new/command.js';
|
|
16
17
|
import { runPrdDraft, runPrdCheck } from './prd/command.js';
|
|
17
18
|
import { runPrdAssess } from './prd/assess.js';
|
|
19
|
+
import { runPrdDecompose } from './prd/decompose.js';
|
|
18
20
|
import { runLoopCleanup } from './loop/cleanup.js';
|
|
19
21
|
import { runFlowSmoke } from './smoke/command.js';
|
|
20
22
|
import { maybeNotifyUpdate, currentYokeVersion } from './update/check.js';
|
|
@@ -102,6 +104,33 @@ export function parseQualityFlags(args) {
|
|
|
102
104
|
},
|
|
103
105
|
};
|
|
104
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
|
+
}
|
|
105
134
|
export function main(argv) {
|
|
106
135
|
const [cmd, ...rest] = argv;
|
|
107
136
|
switch (cmd) {
|
|
@@ -357,6 +386,11 @@ export function main(argv) {
|
|
|
357
386
|
console.log(loopStatus(targetDir));
|
|
358
387
|
return 0;
|
|
359
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
|
+
}
|
|
360
394
|
if (sub === 'cleanup')
|
|
361
395
|
return runLoopCleanup(targetDir, {
|
|
362
396
|
removeWorktrees: rest.includes('--remove-worktrees'),
|
|
@@ -413,7 +447,23 @@ export function main(argv) {
|
|
|
413
447
|
console.error(qualityFlags.error);
|
|
414
448
|
return 1;
|
|
415
449
|
}
|
|
416
|
-
|
|
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 } : {}) });
|
|
417
467
|
}
|
|
418
468
|
if (sub === 'answer') {
|
|
419
469
|
const choice = rest.find(a => a.startsWith('--choice='))?.slice('--choice='.length);
|
|
@@ -511,8 +561,8 @@ export function main(argv) {
|
|
|
511
561
|
const permissions = rest.includes('--unsafe') ? 'unsafe' : undefined;
|
|
512
562
|
const parallelArg = rest.find(a => a.startsWith('--parallel='));
|
|
513
563
|
const parallel = parallelArg && parallelArg !== '--parallel=auto' ? Number(parallelArg.slice('--parallel='.length)) : undefined;
|
|
514
|
-
if (parallel !== undefined && (!Number.isInteger(parallel) || parallel < 1)) {
|
|
515
|
-
console.error(`Invalid --parallel value: ${parallelArg}`);
|
|
564
|
+
if (parallel !== undefined && (!Number.isInteger(parallel) || parallel < 1 || parallel > 8)) {
|
|
565
|
+
console.error(`Invalid --parallel value: ${parallelArg} (expected 1..8)`);
|
|
516
566
|
return 1;
|
|
517
567
|
}
|
|
518
568
|
const json = rest.includes('--json');
|
|
@@ -542,9 +592,29 @@ export function main(argv) {
|
|
|
542
592
|
console.error(qualityFlags.error);
|
|
543
593
|
return 1;
|
|
544
594
|
}
|
|
545
|
-
|
|
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 });
|
|
546
616
|
}
|
|
547
|
-
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]`);
|
|
548
618
|
return 1;
|
|
549
619
|
}
|
|
550
620
|
case 'new': {
|
|
@@ -581,6 +651,25 @@ export function main(argv) {
|
|
|
581
651
|
}
|
|
582
652
|
return runPrdAssess(targetDir, { runner: runner, story: rest.find(a => a.startsWith('--story='))?.slice('--story='.length), reassess: rest.includes('--reassess') });
|
|
583
653
|
}
|
|
654
|
+
if (sub === 'decompose') {
|
|
655
|
+
const story = rest.find(a => a.startsWith('--story='))?.slice('--story='.length);
|
|
656
|
+
if (!story) {
|
|
657
|
+
console.error('usage: yoke prd decompose [dir] --story=<id> [--apply] [--runner=<agent>] [--timeout=<minutes>]');
|
|
658
|
+
return 1;
|
|
659
|
+
}
|
|
660
|
+
const runner = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
|
|
661
|
+
if (runner && !SUPPORTED_AGENTS.includes(runner)) {
|
|
662
|
+
console.error('Invalid planning runner');
|
|
663
|
+
return 1;
|
|
664
|
+
}
|
|
665
|
+
const timeoutArg = rest.find(a => a.startsWith('--timeout='));
|
|
666
|
+
const timeoutMinutes = timeoutArg ? Number(timeoutArg.slice('--timeout='.length)) : undefined;
|
|
667
|
+
if (timeoutMinutes !== undefined && (!Number.isFinite(timeoutMinutes) || timeoutMinutes < 0)) {
|
|
668
|
+
console.error(`Invalid --timeout value: ${timeoutArg}`);
|
|
669
|
+
return 1;
|
|
670
|
+
}
|
|
671
|
+
return runPrdDecompose(targetDir, { story, apply: rest.includes('--apply'), runner: runner, timeoutMinutes });
|
|
672
|
+
}
|
|
584
673
|
if (sub === 'draft') {
|
|
585
674
|
const idea = rest.find(a => a.startsWith('--idea='))?.slice('--idea='.length);
|
|
586
675
|
if (!idea) {
|
|
@@ -607,7 +696,7 @@ export function main(argv) {
|
|
|
607
696
|
}
|
|
608
697
|
if (sub === 'check')
|
|
609
698
|
return runPrdCheck(targetDir);
|
|
610
|
-
console.log(`usage: yoke prd <draft|check|assess> [dir] [--idea="..."] [--runner=<${AGENT_LIST}>] [--story=<id>] [--reassess] [--force] [--timeout=<minutes>]`);
|
|
699
|
+
console.log(`usage: yoke prd <draft|check|assess|decompose> [dir] [--idea="..."] [--runner=<${AGENT_LIST}>] [--story=<id>] [--apply] [--reassess] [--force] [--timeout=<minutes>]`);
|
|
611
700
|
return 1;
|
|
612
701
|
}
|
|
613
702
|
case 'context': {
|
|
@@ -671,7 +760,7 @@ export function main(argv) {
|
|
|
671
760
|
return runUpgrade();
|
|
672
761
|
default:
|
|
673
762
|
console.log('Project workflows: yoke check [dir] [--json|--protect] | goal set|run|resume|pause|status|handoff|budget [dir] | projects add|list|remove | dashboard [dir] [--port=N]');
|
|
674
|
-
console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--loop] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
|
|
763
|
+
console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--loop] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess|decompose> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
|
|
675
764
|
return cmd ? 1 : 0;
|
|
676
765
|
}
|
|
677
766
|
}
|
package/dist/dashboard/panels.js
CHANGED
|
@@ -101,7 +101,9 @@ function renderNow(p){
|
|
|
101
101
|
summary.append(el('p','Last status: '+(status.updatedAt||'Unknown')+(state.stale?' · stale; activity is not confirmed':''),'muted'));
|
|
102
102
|
if(status.progress)summary.append(el('p','Backlog: '+status.progress.passed+'/'+status.progress.total+' tasks accepted · not overall product completion','muted'));
|
|
103
103
|
for(const proc of status.supervision||[])summary.append(row('Provider PID '+(proc.childPid||'not started'),['Attempt: '+((proc.retry||0)+1),'Supervisor heartbeat: '+proc.heartbeatAt,'Last output: '+(proc.lastOutputAt||'none'),'Last successful tool/edit: '+(proc.lastProgressAt||'none'),proc.reason].filter(Boolean).join(' · '),proc.state+' · '+(proc.liveness||'identity unverified')));
|
|
104
|
-
summary.append(el('p','Reported workers: '+workers.length+' / '+(status.parallel?.maxConcurrency||1)+' ·
|
|
104
|
+
summary.append(el('p','Reported workers: '+workers.length+' / '+(status.parallel?.maxConcurrency||1)+' · '+(status.parallel?.waitingWorkers||0)+' waiting · integrations queued: '+tokenText(status.parallel?.queuedIntegrations??status.parallel?.queuedCandidates),'muted'));
|
|
105
|
+
const pool=status.parallel?.globalPool;if(pool)summary.append(el('p','Shared worker pool: '+pool.activeUnits+' / '+pool.limit+' units · implementation '+pool.activeByRole.implementation+' · integration '+pool.activeByRole.integration+' · '+pool.waitingWorkers+' waiting across Yoke projects.','muted'));
|
|
106
|
+
if(p.estimate?.available)summary.append(el('p','Forecast: up to '+p.estimate.maxConcurrency+' implementation slots · '+p.estimate.globalLimit+' shared units · '+p.estimate.workerUnitsPerStory+' unit(s) per story · one serial integration lane · '+duration(p.estimate.implementationWorkMs)+' worker time · '+duration(p.estimate.integrationLaneMs)+' integration · '+duration(p.estimate.integrationQueueWaitMs)+' queue wait'+(p.estimate.integrationSampleCount?' · '+p.estimate.integrationSampleCount+' integration samples':' · integration history unavailable')+'.','muted'));
|
|
105
107
|
if(goal.pendingAttempt)summary.append(row('Goal attempt',goal.pendingAttempt.provider+' · requested model '+(goal.pendingAttempt.model||'provider default')+' · started '+goal.pendingAttempt.startedAt,state.stale?'unconfirmed':'running'));
|
|
106
108
|
if(status.execution&&status.state==='running')summary.append(el('p',state.stale?'Execution was reported but is now unconfirmed.':status.execution.provider+' · requested model '+(status.execution.requestedModel||'provider default')+' · elapsed '+duration(Date.now()-Date.parse(status.execution.startedAt))));
|
|
107
109
|
if(status.story&&!workers.length)summary.append(row(status.storyTitle||status.story,status.phase||'Phase unknown',reportedState));
|
package/dist/dashboard/server.js
CHANGED
|
@@ -19,7 +19,8 @@ import { DASHBOARD_LIMITS, DashboardChangePayloadSchema, DashboardControlPayload
|
|
|
19
19
|
const text = z.string().max(16000);
|
|
20
20
|
const number = z.number().finite().nonnegative();
|
|
21
21
|
const Goal = z.object({ objective: text, status: z.enum(['active', 'running', 'paused', 'blocked', 'complete']), reason: text.optional(), pendingAttempt: z.object({ provider: text, model: text.optional(), startedAt: text }).optional(), attempts: z.array(z.object({ durationMs: number.optional(), provider: text.optional(), success: z.boolean().optional(), summary: text.optional(), inputTokens: number.optional(), outputTokens: number.optional() })).max(200).default([]) });
|
|
22
|
-
const
|
|
22
|
+
const PoolStatus = z.object({ limit: number, activeUnits: number, activeWorkers: number, waitingWorkers: number, activeByRole: z.object({ implementation: number, integration: number }), oldestWaitMs: number });
|
|
23
|
+
const Status = z.object({ state: text, phase: text.optional(), reason: text.optional(), progress: z.object({ passed: number, total: number }).optional(), tokens: z.object({ inputTokens: number, outputTokens: number, totalCostUsd: number.optional(), measurementComplete: z.boolean().optional(), model: text.optional(), calls: z.array(z.object({ usageAvailable: z.boolean().optional() })).max(10000).optional() }).optional(), measurement: z.object({ costAvailable: z.enum(['unknown', 'partial', 'measured']), measuredCalls: number.optional(), unknownCalls: number.optional(), unmeasuredAttempts: number.optional() }).passthrough().optional(), parallel: z.object({ maxConcurrency: number, workerUnitsPerStory: number.optional(), globalPool: PoolStatus.optional() }).passthrough().optional() }).passthrough();
|
|
23
24
|
const Stories = z.array(z.object({ id: text, title: text, passes: z.boolean(), priority: number.optional(), area: text.optional(), writes: z.array(text).optional(), needs: z.array(text).optional() })).max(2000);
|
|
24
25
|
const Check = z.object({ id: text, status: z.enum(['passed', 'failed', 'unverified']), generatedAt: text, summary: text, criteria: z.array(z.object({ id: text, text, status: z.enum(['passed', 'failed', 'unverified']), summary: text })).max(500) });
|
|
25
26
|
const Durations = z.array(z.object({ storyId: text, ms: number.positive() })).max(5000);
|
|
@@ -122,6 +123,7 @@ function snapshot(project, detail) {
|
|
|
122
123
|
const status = read('.yoke/loop-status.json', Status);
|
|
123
124
|
const stories = detail ? read('.yoke/prd.yaml', Stories, true) ?? [] : [];
|
|
124
125
|
const history = detail ? read('.yoke/story-durations.json', Durations) ?? [] : [];
|
|
126
|
+
const events = detail && !project.error ? readEvents(project.root, DASHBOARD_LIMITS.events) : [];
|
|
125
127
|
let check = null;
|
|
126
128
|
if (detail && !project.error) {
|
|
127
129
|
try {
|
|
@@ -140,7 +142,14 @@ function snapshot(project, detail) {
|
|
|
140
142
|
errors.push(error.message);
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
|
-
|
|
145
|
+
const localCapacity = Math.max(1, status?.parallel?.maxConcurrency ?? 1);
|
|
146
|
+
const estimate = estimateSchedule(stories, localCapacity, history, {
|
|
147
|
+
integrationMs: events.filter(event => event.type === 'phase-ended' && event.phase === 'integration').map(event => event.durationMs ?? 0),
|
|
148
|
+
integrationQueueWaitMs: events.filter(event => event.type === 'phase-ended' && event.phase === 'integration-queue').map(event => event.durationMs ?? 0),
|
|
149
|
+
globalLimit: Math.max(1, status?.parallel?.globalPool?.limit ?? localCapacity),
|
|
150
|
+
workerUnitsPerStory: Math.max(1, status?.parallel?.workerUnitsPerStory ?? 1),
|
|
151
|
+
});
|
|
152
|
+
return { ...project, goal, status, errors, ...(detail ? { stories, check, events, estimate } : {}) };
|
|
144
153
|
}
|
|
145
154
|
export async function startDashboard(options = {}) {
|
|
146
155
|
const token = randomBytes(32).toString('hex');
|