@hecer/yoke 1.19.0 → 1.20.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.
Files changed (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +4 -1
  5. package/bench/RESULTS.md +30 -0
  6. package/bench/analyze-codex-comparison.mjs +22 -0
  7. package/bench/compare-codex.mjs +71 -0
  8. package/bench/fixtures/independent-utils/.yoke/prd.yaml +30 -0
  9. package/bench/fixtures/independent-utils/bench-verify.mjs +4 -0
  10. package/bench/fixtures/independent-utils/package.json +1 -0
  11. package/bench/fixtures/independent-utils/src/chunk.mjs +1 -0
  12. package/bench/fixtures/independent-utils/src/range.mjs +1 -0
  13. package/bench/fixtures/independent-utils/src/unique.mjs +1 -0
  14. package/bench/fixtures/independent-utils/tests/STORY-1.test.mjs +8 -0
  15. package/bench/fixtures/independent-utils/tests/STORY-2.test.mjs +8 -0
  16. package/bench/fixtures/independent-utils/tests/STORY-3.test.mjs +8 -0
  17. package/bench/probe-goal-integration.mjs +93 -0
  18. package/bench/probe-parallel-gate-context.mjs +29 -0
  19. package/bench/results/codex-comparison-2026-09-29.json +305 -0
  20. package/bench/results/goal-integration-probes-2026-09-29.json +107 -0
  21. package/bench/results/native-goal-validation-2026-09-30.json +122 -0
  22. package/bench/results/parallel-validation-2026-09-30.json +86 -0
  23. package/dist/agents/process-record-identity.js +33 -0
  24. package/dist/check/command.js +218 -0
  25. package/dist/cli.js +11 -7
  26. package/dist/dashboard/server.js +26 -6
  27. package/dist/goals/codex-native.js +340 -0
  28. package/dist/goals/command.js +230 -29
  29. package/dist/loop/cleanup.js +10 -1
  30. package/dist/loop/dispatcher.js +82 -10
  31. package/dist/loop/git.js +1 -1
  32. package/dist/loop/parallel-adapters.js +53 -7
  33. package/dist/loop/parallel-command.js +8 -1
  34. package/dist/loop/recovery.js +81 -2
  35. package/dist/loop/reporter.js +3 -0
  36. package/dist/loop/resource-pool.js +21 -15
  37. package/dist/loop/run-command.js +91 -25
  38. package/dist/loop/run-state.js +116 -0
  39. package/dist/prd/explore.js +12 -4
  40. package/dist/retrofit/config.js +1 -0
  41. package/dist/retrofit/gitignore.js +3 -0
  42. package/docs/CODEX-COMPARISON-2026-09-29.md +27 -0
  43. package/docs/CONTINUOUS-EXPLORATION.md +6 -2
  44. package/docs/GOALS-RESOURCE-AUDIT-2026-09-29.md +92 -0
  45. package/docs/GOALS.md +109 -0
  46. package/docs/RELEASE-VALIDATION-1.20.0.md +100 -0
  47. package/docs/VERIFIED-PROJECTS.md +4 -4
  48. package/docs/parallel-execution.md +15 -0
  49. package/docs/superpowers/plans/2026-09-30-goals-resources-release.md +78 -0
  50. package/gemini-extension.json +1 -1
  51. package/package.json +5 -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.19.0",
5
+ "version": "1.20.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yoke",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "Cross-agent coding discipline, mechanical gates, and release workflows for eight supported harnesses",
5
5
  "skills": "./canon/skills/",
6
6
  "hooks": "./hooks/hooks.json"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.0 — 2026-09-30
4
+
5
+ ### Added
6
+ - Add explicit goal-to-acceptance binding with `goal set --criteria=...` and `goal bind --criteria=...`; preserve the objective and manifest digest across continuation.
7
+ - Add opt-in native Codex goal threads through `goal run --native-goal` or `goals.nativeCodex`. Yoke pauses native automatic continuation, runs explicit bounded turns, and synchronizes completion only after independent acceptance.
8
+ - Add cumulative `goal set|budget --wall-minutes=N` and a separate shared asynchronous goal-check pool, controlled by `YOKE_MAX_PARALLEL_CHECKS` (1–8, default 1).
9
+ - Add reproducible direct-Codex comparison tooling, deterministic resource/goal probes and dated evidence reports.
10
+
11
+ ### Fixed
12
+ - Preserve story context through integrated verification, design, performance, audit and quality gates, and restore it after exceptions.
13
+ - Retain rejected parallel candidates and exact rejection reasons; validate recovery before rerunning integration gates without another implementation model call.
14
+ - Shorten generated worktree names and reject unsupported Windows paths before setup.
15
+ - Exclude active claims and nested worker worktrees from cleanliness checks and implementation staging even in manually configured projects without generated ignore rules.
16
+ - Admit goal implementation and routing calls through the shared worker pool, disable unmanaged native delegation, inherit runner defaults, and report measured token overruns even when acceptance passes.
17
+ - Account for all model requests in a native turn using durable cumulative usage baselines; react to valid streamed usage with budget cancellation without double-counting progress frames.
18
+ - Persist safe run identity, execution mode, selection and consumed iteration budget; dashboard resume selects that run rather than an unrelated unfinished goal.
19
+ - Preserve absolute exploration deadlines on dashboard resume and retain exclusive supervisor ownership between batches and during idle waits.
20
+
21
+ ### Migration and validation limits
22
+ - Existing unbound goals now stop before execution. Review the acceptance contract, then run `yoke goal bind . --criteria=<ids>`. Binding retains previous attempts and does not refresh protected infrastructure.
23
+ - Goal `--minutes` remains cumulative admitted provider time; `--wall-minutes` also includes admission and verification. Providers with end-of-call telemetry can exceed token ceilings within a call; Yoke records the overrun and blocks further budgeted work. Unknown interrupted usage remains conservative.
24
+ - Native goals remain disabled by default and depend on installed Codex app-server capabilities. Unsupported goal methods fall back to ordinary Codex; other protocol/authentication failures remain visible. Native goal accounting and Yoke token totals can use different units.
25
+ - Native Codex app-server lacks exec's bare startup control. Native plus bare stops before dispatch; use `--no-native-goal` to keep bare execution or explicitly disable bare when opting into native goals.
26
+ - The new check pool covers asynchronous goal checks. Synchronous `yoke check` and story gates keep their existing execution contracts. Concurrency permits do not enforce CPU/RAM quotas or demonstrate general savings over direct Codex.
27
+ - Resume through the dashboard retains the original exploration deadline. An explicit fresh CLI invocation can establish a new duration. Parallel recovery requires a validated unchanged contract; stale candidates need reconciliation. Serial `--resume-worktree` does not adopt parallel candidates.
28
+ - Local Windows validation on Node.js 24.13.0 passed: lint, build, Canon validation, README metadata, package dry-run, dependency audit (0 vulnerabilities), and 1,418 tests across 155 files (2 skipped). Authenticated parallel validation accepted 3/3 stories with 15/15 immutable tests; a native goal completed and resumed with cumulative usage accounting. These small fixtures do not establish general efficiency superiority; live small-budget native cancellation and the cross-platform CI matrix remain unverified. See [release preparation](docs/RELEASE-VALIDATION-1.20.0.md). No publication is implied by this entry.
29
+
3
30
  ## 1.19.0 — 2026-09-28
4
31
 
5
32
  ### Added
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
12
  ![Node.js 20+](https://img.shields.io/badge/node-%E2%89%A520-339933?logo=node.js&logoColor=white)
13
13
 
14
- <!-- yoke:version:start -->1.19.0<!-- yoke:version:end --> · <!-- yoke:tests:start -->1339<!-- yoke:tests:end --> test cases · <!-- yoke:skills:start -->34<!-- yoke:skills:end --> skills
14
+ <!-- yoke:version:start -->1.20.0<!-- yoke:version:end --> · <!-- yoke:tests:start -->1420<!-- yoke:tests:end --> test cases · <!-- yoke:skills:start -->34<!-- yoke:skills:end --> skills
15
15
 
16
16
  <!-- yoke:agents:start -->Claude | Codex | Gemini | Qwen | OpenCode | Kilo | Pi | Hermes<!-- yoke:agents:end -->
17
17
 
@@ -40,6 +40,7 @@ Each story carries observable acceptance criteria and targeted test commands. Pa
40
40
  | --- | --- |
41
41
  | **Verified completion** | Checks acceptance criteria, the project verify command, and any configured completion, review, quality, or browser gates before accepting work. |
42
42
  | **Parallel execution** | Schedules independent stories in isolated worktrees, respects dependencies and write scopes, and coordinates a shared worker limit across projects. |
43
+ | **Durable goals** | Binds objectives to executable acceptance, shares worker capacity, records budgets, and optionally resumes native Codex goal threads. |
43
44
  | **Long-running autonomy** | Recovers from provider failures and blocked work. Optional exploration discovers new, repository-evidenced tasks after the planned backlog drains. |
44
45
  | **A choice of agents** | Uses the native CLI for Claude, Codex, Gemini, Qwen, OpenCode, Kilo, Pi, or Hermes. Install and authenticate the CLI you choose; Yoke does not bundle model runtimes or credentials. |
45
46
  | **Project visibility** | A local dashboard shows project status, **Workspace analytics**, **History**, and controls such as **Queue a change**, safe-boundary pause/resume, and operator notes. It runs with the local Yoke process. Dark and light themes are available. |
@@ -117,8 +118,10 @@ The dashboard is a local control room. It does not discover every process or run
117
118
  | [Parallel execution](docs/parallel-execution.md) | Worker scheduling, shared capacity, integration, and recovery |
118
119
  | [Continuous exploration](docs/CONTINUOUS-EXPLORATION.md) | Autonomous discovery, runtime limits, pause/resume, and stop detection |
119
120
  | [Project workflows](docs/VERIFIED-PROJECTS.md) | Setup, verification, goals, and execution defaults |
121
+ | [Verified goals](docs/GOALS.md) | Acceptance binding, native Codex goals, budgets, and resource admission |
120
122
  | [Code Intelligence](docs/CODE-INTELLIGENCE.md) | Optional Graphify, Serena, and Graft evidence providers |
121
123
  | [Dashboard](docs/DASHBOARD-EVOLUTION.md) | Project views, history, controls, and reporting boundaries |
124
+ | [Benchmarks](bench/RESULTS.md) | Direct Codex comparison, routing studies, sample limits, and integration findings |
122
125
  | [Changelog](CHANGELOG.md) | Release features, behavior changes, and migration notes |
123
126
 
124
127
  ## Safety and limits
package/bench/RESULTS.md CHANGED
@@ -1,11 +1,41 @@
1
1
  # Benchmark results
2
2
 
3
+ The [1.20.0 parallel acceptance smoke](results/parallel-validation-2026-09-30.json)
4
+ accepted all three independent stories and passed all 15 immutable replay tests after
5
+ the runtime claim exclusion fix. Its single corrected run took 112,949 ms. This is
6
+ validation of the corrected fixture, not a new A/B efficiency comparison; the dated
7
+ 1.19.0 comparisons below remain historical. See the
8
+ [release validation report](../docs/RELEASE-VALIDATION-1.20.0.md) for limits.
9
+
3
10
  Result schema v1 records fixture version, sample label, permission profile, telemetry/model
4
11
  availability, verdict/blocker, conflicts, wall time, iterations, and final fixture-test status.
5
12
 
6
13
  Fixture `string-kit` (3 stories, 16 pre-written assertions). Methodology and caveats:
7
14
  [README.md](README.md). One row per run — raw JSON in [`results/`](results/).
8
15
 
16
+ ## Direct Codex comparison and integration audit (2026-09-29)
17
+
18
+ Authenticated comparison of Yoke 1.19.0 against direct Codex, requesting the same
19
+ `gpt-6.1-sol / low` model and effort with routing and native delegation disabled.
20
+ Two alternating queue pairs passed the same ten original tests: median elapsed time
21
+ was **118.3 seconds for direct Codex** and **274.9 seconds for Yoke serial**.
22
+ Yoke used **91.9% more fresh input tokens** in this small sample.
23
+
24
+ One corrected independent-task run per arm passed all fifteen original tests for
25
+ direct Codex (71.3 seconds) and Yoke serial (442.2 seconds). Yoke parallel used three
26
+ workers but accepted **0/3 stories**: integration gates lost the worker's
27
+ `YOKE_STORY` context. Its 117.1-second failure is **not** a speedup.
28
+ The deterministic control probe reproduced this integration defect separately
29
+ from provider calls. Initial fixture setup/path failures are retained as diagnostics.
30
+
31
+ These measurements do not establish general superiority, USD cost, CPU/RAM savings,
32
+ or routing benefits. Host skills/plugins were not fully audited and background load
33
+ was uncontrolled. See [the complete comparison](../docs/CODEX-COMPARISON-2026-09-29.md),
34
+ [Goals/resource findings](../docs/GOALS-RESOURCE-AUDIT-2026-09-29.md), and the separately
35
+ formatted [comparison summary](results/codex-comparison-2026-09-29.json) and
36
+ [integration diagnostics](results/goal-integration-probes-2026-09-29.json).
37
+ These two aggregate audit artifacts are not individual result-schema-v1 run records.
38
+
9
39
  ## Synthetic dispatcher matrix (2026-09-27)
10
40
 
11
41
  One local run of `node bench/run-parallel-matrix.mjs` after the shared integration pipeline was
@@ -0,0 +1,22 @@
1
+ // node bench/analyze-codex-comparison.mjs <results.json> ... > summary.json
2
+ import { readFileSync } from 'node:fs'
3
+ const sources = process.argv.slice(2).map(path => JSON.parse(readFileSync(path, 'utf8')))
4
+ if (!sources.length) throw new Error('Supply comparison results.json files')
5
+ const rows = sources.flatMap(s => s.results).map(({ dir, ...row }) => ({
6
+ ...row,
7
+ freshInputTokens: Number.isFinite(row.usage?.inputTokens) && Number.isFinite(row.usage?.cachedInputTokens)
8
+ ? row.usage.inputTokens - row.usage.cachedInputTokens : null,
9
+ }))
10
+ if (!rows.length) throw new Error('No completed runs')
11
+ for (const row of rows) if (!Number.isFinite(row.wallMs) || row.wallMs <= 0 || (row.freshInputTokens !== null && row.freshInputTokens < 0)) throw new Error('Invalid measurement')
12
+ const median = values => { const sorted = [...values].sort((a,b) => a-b); const n = sorted.length; return n ? (sorted[Math.floor((n-1)/2)] + sorted[Math.floor(n/2)]) / 2 : null }
13
+ const groups = [...new Set(rows.map(r => `${r.fixture}:${r.arm}`))].map(key => {
14
+ const runs = rows.filter(r => `${r.fixture}:${r.arm}` === key)
15
+ if (new Set(runs.map(r => `${r.model}:${r.effort}:${r.routing}`)).size !== 1) throw new Error('Incompatible execution policies in one comparison group')
16
+ return { fixture: runs[0].fixture, arm: runs[0].arm, runs: runs.length, accepted: runs.filter(r => r.accepted).length,
17
+ medianWallMs: median(runs.map(r => r.wallMs)),
18
+ medianFreshInputTokens: runs.every(r => r.freshInputTokens !== null) ? median(runs.map(r => r.freshInputTokens)) : null,
19
+ medianOutputTokens: runs.every(r => Number.isFinite(r.usage?.outputTokens)) ? median(runs.map(r => r.usage.outputTokens)) : null,
20
+ medianAllInputTokens: runs.every(r => Number.isFinite(r.usage?.inputTokens)) ? median(runs.map(r => r.usage.inputTokens)) : null }
21
+ })
22
+ console.log(JSON.stringify({ auditedVersion: '1.19.0', auditedCommit: '05fac3db1a51412563005e07f80e6e5a9aab8e06', models: [...new Set(rows.map(r => r.model))], effort: 'low', groups, runs: rows, limitations: [...new Set(sources.flatMap(s => s.limits))], pricesMeasured: false, cpuMeasured: false, memoryMeasured: false }, null, 2))
@@ -0,0 +1,71 @@
1
+ // Controlled live comparison. Run after npm run build.
2
+ // node bench/compare-codex.mjs --repeats=2 --fixture=routing-queue
3
+ import { cpSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'
4
+ import { createHash } from 'node:crypto'
5
+ import { spawn, spawnSync } from 'node:child_process'
6
+ import { dirname, join, resolve } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { parse, stringify } from 'yaml'
9
+ import { buildProviderInvocation, startProviderProcess } from '../dist/agents/providers.js'
10
+ const repo = dirname(dirname(fileURLToPath(import.meta.url)))
11
+ const version = JSON.parse(readFileSync(join(repo, 'package.json'), 'utf8')).version
12
+ const opts = Object.fromEntries(process.argv.slice(2).map(x => x.replace(/^--/, '').split('=')))
13
+ const fixture = opts.fixture ?? 'routing-queue'
14
+ if (!['routing-queue', 'string-kit', 'independent-utils'].includes(fixture)) throw new Error('Unsupported fixture')
15
+ const repeats = Number(opts.repeats ?? 2)
16
+ if (!Number.isInteger(repeats) || repeats < 1 || repeats > 10) throw new Error('repeats must be 1..10')
17
+ const model = opts.model ?? 'gpt-6.1-sol'
18
+ const root = resolve(opts.root ?? `G:/NN-Developed/Yoke-Testground/b${Date.now().toString(36)}`)
19
+ mkdirSync(root, { recursive: true })
20
+ const seed = join(repo, 'bench', 'fixtures', fixture)
21
+ const stories = parse(readFileSync(join(seed, '.yoke/prd.yaml'), 'utf8'))
22
+ const originalTests = readdirSync(join(seed, 'tests')).filter(f => f.endsWith('.test.mjs')).sort()
23
+ const protectedFiles = new Map(['bench-verify.mjs', ...originalTests.map(f => join('tests', f))].map(file => [file, readFileSync(join(seed, file))]))
24
+ const acceptanceDigest = createHash('sha256').update(Buffer.concat([...protectedFiles].flatMap(([file, bytes]) => [Buffer.from(file + '\0'), bytes]))).digest('hex')
25
+ const prompt = 'Implement all requirements below. Run node bench-verify.mjs. Preserve tests and the verification script. Do not commit.\n' + stories.map(s => `${s.id}: ${s.title}\n${s.acceptance.join('\n')}`).join('\n\n')
26
+ const results = []
27
+ const requestedArms = opts.arms?.split(',')
28
+ if (requestedArms && (!requestedArms.length || requestedArms.some(arm => !['codex', 'yoke-serial', 'yoke-parallel'].includes(arm)))) throw new Error('arms must name codex,yoke-serial,yoke-parallel')
29
+ for (let repeat = 0; repeat < repeats; repeat++) {
30
+ const baseArms = fixture === 'independent-utils' ? ['codex', 'yoke-serial', 'yoke-parallel'] : ['codex', 'yoke-serial']
31
+ const selectedArms = requestedArms ? baseArms.filter(arm => requestedArms.includes(arm)) : baseArms
32
+ if (!selectedArms.length) throw new Error('No requested arms apply to this fixture')
33
+ const arms = repeat % 2 ? [...selectedArms].reverse() : selectedArms
34
+ for (const arm of arms) {
35
+ const dir = join(root, `${repeat + 1}-${arm}`)
36
+ mkdirSync(dir); cpSync(seed, dir, { recursive: true })
37
+ const parallel = arm === 'yoke-parallel' ? 3 : 1
38
+ writeFileSync(join(dir, '.yoke/config.yaml'), stringify({ canonVersion: '1.2.0', agents: ['codex'], loop: { enabled: true, parallel, isolate: true }, runner: { agent: 'codex', model, reasoningEffort: 'low', bare: true }, routing: { enabled: false }, verify: { command: 'node bench-verify.mjs' } }))
39
+ for (const args of [['init', '-q'], ['add', '-A'], ['-c', 'user.name=benchmark', '-c', 'user.email=benchmark@yoke.local', 'commit', '-qm', 'Immutable fixture seed']]) {
40
+ const r = spawnSync('git', args, { cwd: dir, encoding: 'utf8' }); if (r.status !== 0) throw new Error(r.stderr)
41
+ }
42
+ console.log(JSON.stringify({ type: 'start', fixture, repeat: repeat + 1, arm, model, dir }))
43
+ const start = Date.now(); let outcome; let events = []; let usage
44
+ if (arm === 'codex') {
45
+ const inv = buildProviderInvocation('codex', prompt, dir, 'unsafe', { model, reasoningEffort: 'low', bare: true, nativeMultiAgent: false })
46
+ inv.args.push('--ignore-rules')
47
+ const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 600_000)
48
+ try {
49
+ const r = await startProviderProcess('codex', inv, { signal: controller.signal, idleTimeoutMs: 180_000 }).completion
50
+ outcome = r.kind; usage = r.telemetry.tokens
51
+ writeFileSync(join(root, `${fixture}-${repeat + 1}-${arm}.log`), JSON.stringify(r, null, 2))
52
+ } finally { clearTimeout(timer) }
53
+ } else {
54
+ const child = spawn(process.execPath, [join(repo, 'dist/cli.js'), 'loop', 'run', dir, '--json', '--runner=codex', '--no-routing', `--parallel=${parallel}`, '--max=3', '--timeout=10', '--unsafe'], { cwd: dir, env: { ...process.env, LOCALAPPDATA: join(root, 'state') }, stdio: ['ignore', 'pipe', 'pipe'] })
55
+ let output = '', errors = ''; child.stdout.on('data', d => { output += d }); child.stderr.on('data', d => { errors += d })
56
+ outcome = await new Promise((res, rej) => { child.on('error', rej); child.on('close', res) })
57
+ events = output.split('\n').flatMap(line => { try { return [JSON.parse(line)] } catch { return [] } })
58
+ writeFileSync(join(root, `${fixture}-${repeat + 1}-${arm}.log`), output + '\nSTDERR\n' + errors)
59
+ usage = events.filter(e => e.type === 'status' && e.tokens).at(-1)?.tokens
60
+ }
61
+ const wallMs = Date.now() - start
62
+ // Replay immutable originals after the agent exits. Agent-written tests never determine quality.
63
+ mkdirSync(join(dir, 'tests'), { recursive: true })
64
+ for (const [file, bytes] of protectedFiles) writeFileSync(join(dir, file), bytes)
65
+ const testFiles = originalTests.map(f => join('tests', f))
66
+ const checkStart = Date.now(); const check = spawnSync(process.execPath, ['--test', ...testFiles], { cwd: dir, encoding: 'utf8', timeout: 60_000 })
67
+ writeFileSync(join(root, `${fixture}-${repeat + 1}-${arm}.acceptance.log`), check.stdout + check.stderr)
68
+ const result = { fixture, repeat: repeat + 1, arm, model, effort: 'low', routing: false, nativeMultiAgent: false, parallel, acceptanceDigest, wallMs, acceptanceMs: Date.now() - checkStart, accepted: check.status === 0, outcome, usage: usage ?? null, events: events.length, dir }
69
+ results.push(result); writeFileSync(join(root, 'results.json'), JSON.stringify({ version, root, results, limits: ['Small fixture sample; not representative of production projects', 'CPU and RAM not measured', 'Routing disabled; fixed model and effort', 'Immutable tests replayed after execution; not hidden from agents', 'User config disabled; not an audit of every installed plugin'] }, null, 2)); console.log(JSON.stringify({ type: 'result', ...result }))
70
+ }
71
+ }
@@ -0,0 +1,30 @@
1
+ - id: STORY-1
2
+ title: Stable unique values
3
+ priority: 1
4
+ writes: [src/unique.mjs]
5
+ needs: []
6
+ acceptance:
7
+ - Export unique(values) from src/unique.mjs, returning first occurrences in input order using SameValueZero equality.
8
+ - Never mutate the input. Non-array input throws TypeError. Preserve object identity.
9
+ - node bench-verify.mjs exits 0
10
+ passes: false
11
+ - id: STORY-2
12
+ title: Array chunking
13
+ priority: 2
14
+ writes: [src/chunk.mjs]
15
+ needs: []
16
+ acceptance:
17
+ - Export chunk(values,size) from src/chunk.mjs. Split arrays into consecutive new arrays of at most size elements.
18
+ - An empty input returns an empty array. Never mutate the input. Non-array input throws TypeError; size must be a positive integer or throw RangeError.
19
+ - node bench-verify.mjs exits 0
20
+ passes: false
21
+ - id: STORY-3
22
+ title: Numeric range
23
+ priority: 3
24
+ writes: [src/range.mjs]
25
+ needs: []
26
+ acceptance:
27
+ - Export range(start,end,step=1) from src/range.mjs. Generate start-inclusive, end-exclusive numbers with a positive or negative step.
28
+ - A step pointing away from end returns an empty array. Zero step or any non-finite argument throws RangeError. Equal endpoints return an empty array.
29
+ - node bench-verify.mjs exits 0
30
+ passes: false
@@ -0,0 +1,4 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ const ids = process.env.YOKE_STORY ? [process.env.YOKE_STORY] : ['STORY-1', 'STORY-2', 'STORY-3']
3
+ const r = spawnSync(process.execPath, ['--test', ...ids.map(id => `tests/${id}.test.mjs`)], { stdio: 'inherit' })
4
+ process.exit(r.status ?? 1)
@@ -0,0 +1 @@
1
+ {"name":"yoke-independent-utils-benchmark","private":true,"type":"module","scripts":{"test":"node bench-verify.mjs"}}
@@ -0,0 +1 @@
1
+ export function chunk() { throw new Error('Not implemented') }
@@ -0,0 +1 @@
1
+ export function range() { throw new Error('Not implemented') }
@@ -0,0 +1 @@
1
+ export function unique() { throw new Error('Not implemented') }
@@ -0,0 +1,8 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { unique } from '../src/unique.mjs'
4
+ test('deduplicates in order', () => assert.deepEqual(unique([2,1,2,3,1]), [2,1,3]))
5
+ test('empty', () => assert.deepEqual(unique([]), []))
6
+ test('SameValueZero', () => assert.deepEqual(unique([NaN,NaN,0,-0]), [NaN,0]))
7
+ test('identity and immutable input', () => { const a = {}; const b = {}; const input = Object.freeze([a,b,a]); assert.deepEqual(unique(input), [a,b]) })
8
+ test('invalid input', () => assert.throws(() => unique('abc'), TypeError))
@@ -0,0 +1,8 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { chunk } from '../src/chunk.mjs'
4
+ test('consecutive chunks', () => assert.deepEqual(chunk([1,2,3,4,5],2), [[1,2],[3,4],[5]]))
5
+ test('empty', () => assert.deepEqual(chunk([],2), []))
6
+ test('large size', () => assert.deepEqual(chunk([1,2],9), [[1,2]]))
7
+ test('immutable input and new arrays', () => { const input = Object.freeze([1,2]); assert.notEqual(chunk(input,2)[0], input) })
8
+ test('invalid input and size', () => { assert.throws(() => chunk('abc',2), TypeError); for (const size of [0,-1,1.5,NaN,Infinity]) assert.throws(() => chunk([],size), RangeError) })
@@ -0,0 +1,8 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { range } from '../src/range.mjs'
4
+ test('ascending', () => assert.deepEqual(range(1,5), [1,2,3,4]))
5
+ test('descending', () => assert.deepEqual(range(5,0,-2), [5,3,1]))
6
+ test('fractional', () => assert.deepEqual(range(0,1,0.25), [0,0.25,0.5,0.75]))
7
+ test('empty', () => { assert.deepEqual(range(1,1), []); assert.deepEqual(range(1,5,-1), []); assert.deepEqual(range(5,1,1), []) })
8
+ test('invalid numbers', () => { for (const args of [[0,1,0],[NaN,1],[0,Infinity],[0,1,Infinity]]) assert.throws(() => range(...args), RangeError) })
@@ -0,0 +1,93 @@
1
+ // Read-only protocol capability probe plus disposable local goal resource test.
2
+ import { spawn } from 'node:child_process'
3
+ import { join, resolve } from 'node:path'
4
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
5
+ import { createProjectGoal, runProjectGoal } from '../dist/goals/command.js'
6
+ import { acquireSharedWorker, sharedPoolStatus } from '../dist/loop/resource-pool.js'
7
+ const root = resolve(process.argv[2] ?? `G:/NN-Developed/Yoke-Testground/goal-probe-${Date.now()}`)
8
+ mkdirSync(join(root, '.yoke'), { recursive: true })
9
+ process.env.LOCALAPPDATA = join(root, 'isolated-state')
10
+ process.env.YOKE_MAX_PARALLEL_WORKERS = '1'
11
+ process.env.YOKE_STATE_DIR = join(root, 'yoke-state')
12
+ writeFileSync(join(root, '.yoke/acceptance.yaml'), 'version: 1\nprotected: [test.mjs]\ncriteria:\n- id: outcome\n text: Expected outcome\n commands: [node test.mjs]\n')
13
+ writeFileSync(join(root, 'test.mjs'), 'import {existsSync} from "node:fs"; process.exit(existsSync("implemented.txt") ? 0 : 1)')
14
+ createProjectGoal(root, 'Disposable test goal', { acceptanceIds: ['outcome'] })
15
+ const lease = await acquireSharedWorker({ targetDir: root, storyId: 'occupied', provider: 'codex', role: 'implementation' })
16
+ let executedWhilePoolFull = false, occupiedPermitHeld = true
17
+ let goal, queuedWhilePoolFull = false
18
+ try {
19
+ const running = runProjectGoal(root, { execute: async () => {
20
+ const pool = sharedPoolStatus(); executedWhilePoolFull = occupiedPermitHeld && pool.activeUnits === pool.limit
21
+ writeFileSync(join(root, 'implemented.txt'), 'done')
22
+ return { success: true, summary: 'Fake executor for mechanical pool test', inputTokens: 1, outputTokens: 1 }
23
+ } })
24
+ // Admission is asynchronous. Release the occupied provider permit only after
25
+ // the goal has completed its independent check and queued for implementation.
26
+ const deadline = Date.now() + 15_000
27
+ while (Date.now() < deadline) {
28
+ if (sharedPoolStatus().waitingWorkers > 0) { queuedWhilePoolFull = true; break }
29
+ await new Promise(resolve => setTimeout(resolve, 100))
30
+ }
31
+ occupiedPermitHeld = false
32
+ await lease.release()
33
+ goal = await running
34
+ } finally { await lease.release() }
35
+ const findings = { poolProbe: { fakeExecutor: true, limit: 1, queuedWhilePoolFull, executedWhilePoolFull, goalStatus: goal.status }, protocol: [] }
36
+ // A successful implementation that overshoots budget must remain blocked.
37
+ const budgetRoot = join(root, 'budget-probe'); mkdirSync(join(budgetRoot, '.yoke'), { recursive: true })
38
+ writeFileSync(join(budgetRoot, '.yoke/acceptance.yaml'), 'version: 1\nprotected: [test.mjs]\ncriteria:\n- id: outcome\n text: Expected outcome\n commands: [node test.mjs]\n')
39
+ writeFileSync(join(budgetRoot, 'test.mjs'), 'import {existsSync} from "node:fs"; await new Promise(r=>setTimeout(r,120)); process.exit(existsSync("implemented.txt") ? 0 : 1)')
40
+ createProjectGoal(budgetRoot, 'Disposable budget test', { acceptanceIds: ['outcome'], tokenBudget: 1, maxMinutes: 0.001 })
41
+ const budgetStart = Date.now()
42
+ const budgetGoal = await runProjectGoal(budgetRoot, { execute: async () => {
43
+ writeFileSync(join(budgetRoot, 'implemented.txt'), 'done')
44
+ return { success: true, summary: 'Fake executor reporting 100 tokens', inputTokens: 90, outputTokens: 10 }
45
+ } })
46
+ findings.budgetProbe = { fakeExecutor: true, tokenBudget: 1, measuredTokens: 100, timeBudgetMs: 60, totalWallMs: Date.now() - budgetStart, recordedAgentMs: budgetGoal.attempts[0]?.durationMs, status: budgetGoal.status }
47
+ const objectiveRoot = join(root, 'objective-probe'); mkdirSync(join(objectiveRoot, '.yoke'), { recursive: true })
48
+ writeFileSync(join(objectiveRoot, '.yoke/acceptance.yaml'), 'version: 1\nprotected: [test.mjs]\ncriteria:\n- id: old-feature\n text: Existing feature still works\n commands: [node test.mjs]\n')
49
+ writeFileSync(join(objectiveRoot, 'test.mjs'), 'import {existsSync} from "node:fs"; process.exit(existsSync("old-feature.txt") ? 0 : 1)')
50
+ writeFileSync(join(objectiveRoot, 'old-feature.txt'), 'already implemented')
51
+ createProjectGoal(objectiveRoot, 'Implement the new feature by creating new-feature.txt')
52
+ let objectiveExecutorCalled = false
53
+ const objectiveGoal = await runProjectGoal(objectiveRoot, { execute: async () => { objectiveExecutorCalled = true; return { success: false, summary: 'Should need implementation' } } })
54
+ findings.objectiveProbe = { requestedArtifact: 'new-feature.txt', requestedArtifactExists: existsSync(join(objectiveRoot, 'new-feature.txt')), acceptance: 'only checks old-feature.txt; intentionally unbound objective', executorCalled: objectiveExecutorCalled, attempts: objectiveGoal.attempts.length, status: objectiveGoal.status }
55
+ const cli = join(process.env.APPDATA, 'npm/node_modules/@openai/codex/bin/codex.js')
56
+ const child = spawn(process.execPath, [cli, 'app-server', '--stdio', '--enable', 'goals'], { cwd: root, stdio: ['pipe', 'pipe', 'pipe'] })
57
+ let next = 1, buffer = '', errorText = ''; const pending = new Map()
58
+ child.stderr.on('data', d => { errorText += d })
59
+ child.stdout.on('data', data => {
60
+ buffer += data
61
+ while (buffer.includes('\n')) {
62
+ const i = buffer.indexOf('\n'), line = buffer.slice(0, i); buffer = buffer.slice(i + 1)
63
+ try { const message = JSON.parse(line); if (pending.has(message.id)) { const p = pending.get(message.id); pending.delete(message.id); message.error ? p.reject(new Error(JSON.stringify(message.error))) : p.resolve(message.result) } } catch {}
64
+ }
65
+ })
66
+ function rpc(method, params) {
67
+ return new Promise((resolve, reject) => {
68
+ const id = next++; const timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout: ${method}`)) }, 30_000)
69
+ pending.set(id, { resolve: result => { clearTimeout(timer); resolve(result) }, reject: error => { clearTimeout(timer); reject(error) } })
70
+ child.stdin.write(JSON.stringify({ id, method, params }) + '\n')
71
+ })
72
+ }
73
+ try {
74
+ await rpc('initialize', { clientInfo: { name: 'yoke-goals-probe', version: '1.0.0' }, capabilities: { experimentalApi: true } })
75
+ child.stdin.write(JSON.stringify({ method: 'initialized' }) + '\n')
76
+ const start = await rpc('thread/start', { cwd: root, model: 'gpt-6.1-sol', approvalPolicy: 'never', sandbox: 'read-only' })
77
+ const threadId = start.thread.id
78
+ for (const [method, params] of [
79
+ ['thread/goal/set', { threadId, objective: 'Protocol-only goal: no development turn requested', tokenBudget: 1000 }],
80
+ ['thread/goal/get', { threadId }],
81
+ ['thread/goal/set', { threadId, status: 'paused' }],
82
+ ['thread/goal/get', { threadId }],
83
+ ['thread/goal/clear', { threadId }],
84
+ ['thread/goal/get', { threadId }],
85
+ ]) {
86
+ const result = await rpc(method, params); findings.protocol.push({ method, requestedStatus: params.status ?? null, result })
87
+ }
88
+ await rpc('thread/archive', { threadId })
89
+ } catch (error) { findings.protocolError = error.message; findings.stderrTail = errorText.slice(-2000) }
90
+ finally { child.stdin.end(); child.kill() }
91
+ writeFileSync(join(root, 'findings.json'), JSON.stringify(findings, null, 2))
92
+ console.log(JSON.stringify(findings, null, 2))
93
+ if (findings.protocolError) process.exitCode = 1
@@ -0,0 +1,29 @@
1
+ // Diagnostic using fake agents/Git adapters, exercising the real loop and dispatcher.
2
+ import { cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
+ import { join, resolve } from 'node:path'
4
+ import { runLoopCommand } from '../dist/loop/run-command.js'
5
+ import { saveConfig } from '../dist/retrofit/config.js'
6
+ import { loadPrd, savePrd } from '../dist/loop/prd.js'
7
+ const base = resolve(process.argv[2] ?? `G:/NN-Developed/Yoke-Testground/p${Date.now().toString(36)}`)
8
+ process.env.LOCALAPPDATA = join(base, 'state')
9
+ const findings = []
10
+ for (const requiresStoryContext of [false, true]) {
11
+ const root = join(base, requiresStoryContext ? 'scoped' : 'control'); mkdirSync(join(root, '.yoke'), { recursive: true })
12
+ saveConfig(root, { canonVersion: 'test', agents: ['codex'], loop: { enabled: true }, verify: { command: 'node test.mjs' } })
13
+ savePrd(join(root, '.yoke/prd.yaml'), ['A','B','C'].map((id, index) => ({ id, title: id, priority: index + 1, acceptance: ['Expected result'], writes: [`src/${id}`], passes: false })))
14
+ const observedContexts = []
15
+ const code = await runLoopCommand(root, { parallel: 3, maxIterations: 3, quiet: true,
16
+ runner: () => ({ success: true, summary: 'Fake agent completed' }), isAvailable: () => true,
17
+ verify: () => { const context = process.env.YOKE_STORY ?? null; observedContexts.push(context); return { passed: !requiresStoryContext || ['A','B','C'].includes(context), summary: context ? 'Story-scoped verification passed' : 'Missing YOKE_STORY context' } },
18
+ git: { isClean: () => true, commitAll: () => undefined,
19
+ addWorktree: (_target, worktree) => { mkdirSync(join(worktree, '.yoke'), { recursive: true }); cpSync(join(root, '.yoke/prd.yaml'), join(worktree, '.yoke/prd.yaml')) },
20
+ // Retain disposable directories for inspection; no recursive deletion.
21
+ removeWorktree: () => undefined,
22
+ integrate: (_target, worktree) => savePrd(join(root, '.yoke/prd.yaml'), loadPrd(join(worktree, '.yoke/prd.yaml'))),
23
+ },
24
+ })
25
+ findings.push({ fakeAgentsAndGit: true, requiresStoryContext, exitCode: code, observedContexts, acceptedStories: loadPrd(join(root, '.yoke/prd.yaml')).filter(s => s.passes).length })
26
+ }
27
+ writeFileSync(join(base, 'findings.json'), JSON.stringify(findings, null, 2))
28
+ console.log(JSON.stringify(findings, null, 2))
29
+ if (findings.some(row => row.exitCode !== 0 || row.acceptedStories !== 3 || row.observedContexts.includes(null))) process.exitCode = 1