@hecer/yoke 0.9.0 → 1.0.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/.codex-plugin/plugin.json +7 -0
- package/CHANGELOG.md +169 -149
- package/README.md +24 -16
- package/TODOS.md +8 -0
- package/agents/docs.toml +6 -0
- package/agents/implementer.toml +6 -0
- package/agents/reviewer.toml +6 -0
- package/agents/security.toml +6 -0
- package/bench/README.md +45 -42
- package/bench/RESULTS.md +46 -36
- package/bench/result-schema.mjs +12 -0
- package/bench/results/claude-2026-07-27T18-03-26.json +50 -0
- package/bench/results/codex-unavailable-1785175418318.json +15 -0
- package/bench/results/gemini-2026-07-27T18-03-44.json +46 -0
- package/bench/run-matrix.mjs +26 -0
- package/bench/run.mjs +127 -115
- package/canon/loop/prd.schema.md +5 -0
- package/canon/manifest.yaml +1 -1
- package/canon/skills/authoring-prd/SKILL.md +6 -0
- package/canon/skills/ship/SKILL.md +2 -7
- package/canon/tools/codex-rtk-hook.mjs +36 -0
- package/dist/agents/providers.js +23 -0
- package/dist/agents/telemetry.js +30 -0
- package/dist/agents/types.js +1 -0
- package/dist/audit/changes.js +6 -0
- package/dist/audit/command.js +64 -0
- package/dist/audit/dependencies.js +21 -0
- package/dist/audit/secrets.js +16 -0
- package/dist/audit/types.js +1 -0
- package/dist/cli.js +22 -4
- package/dist/loop/claims.js +57 -0
- package/dist/loop/cleanup.js +10 -4
- package/dist/loop/git.js +8 -2
- package/dist/loop/identity.js +27 -0
- package/dist/loop/loop.js +20 -2
- package/dist/loop/merge-queue.js +20 -0
- package/dist/loop/parallel.js +39 -0
- package/dist/loop/prd.js +48 -2
- package/dist/loop/run-command.js +51 -6
- package/dist/loop/runner.js +41 -29
- package/dist/loop/scheduler.js +8 -0
- package/dist/prd/command.js +6 -0
- package/dist/retrofit/config.js +12 -0
- package/dist/retrofit/planners/codex.js +64 -19
- package/dist/review/command.js +52 -12
- package/dist/review/verdict.js +45 -0
- package/docs/MIGRATING-TO-1.0.md +33 -0
- package/docs/superpowers/plans/2026-07-27-yoke-1.0-release.md +205 -0
- package/docs/superpowers/specs/2026-07-27-yoke-1.0-hardening-and-codex-parity-design.md +164 -0
- package/hooks/hooks.json +19 -0
- package/package.json +82 -67
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/config.yaml +0 -6
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/context/DECISIONS.md +0 -9
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/prd.yaml +0 -38
- package/bench/.runs/claude-2026-07-09T22-34-01/bench-verify.mjs +0 -15
- package/bench/.runs/claude-2026-07-09T22-34-01/package.json +0 -9
- package/bench/.runs/claude-2026-07-09T22-34-01/src/index.mjs +0 -48
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-1.test.mjs +0 -24
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-2.test.mjs +0 -28
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-3.test.mjs +0 -25
- package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/config.yaml +0 -6
- package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/prd.yaml +0 -32
- package/bench/.runs/gemini-2026-07-09T22-34-02/bench-verify.mjs +0 -15
- package/bench/.runs/gemini-2026-07-09T22-34-02/package.json +0 -9
- package/bench/.runs/gemini-2026-07-09T22-34-02/src/index.mjs +0 -3
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-1.test.mjs +0 -24
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-2.test.mjs +0 -28
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-3.test.mjs +0 -25
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Implement the functions required by the PRD stories here (or in modules
|
|
2
|
-
// re-exported from here). Tests import from this file.
|
|
3
|
-
|
|
4
|
-
export function slugify(text) {
|
|
5
|
-
return String(text)
|
|
6
|
-
.toLowerCase()
|
|
7
|
-
.replace(/[^a-z0-9\s-]/g, '')
|
|
8
|
-
.replace(/[\s-]+/g, '-')
|
|
9
|
-
.replace(/^-+|-+$/g, '')
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function truncate(text, max) {
|
|
13
|
-
if (max < 1) throw new RangeError('max must be >= 1')
|
|
14
|
-
|
|
15
|
-
const str = String(text)
|
|
16
|
-
if (str.length <= max) return str
|
|
17
|
-
|
|
18
|
-
const available = max - 1 // reserve one char for the ellipsis
|
|
19
|
-
const words = str.trim().split(/\s+/)
|
|
20
|
-
|
|
21
|
-
let result = ''
|
|
22
|
-
for (const word of words) {
|
|
23
|
-
const candidate = result ? `${result} ${word}` : word
|
|
24
|
-
if (candidate.length <= available) result = candidate
|
|
25
|
-
else break
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (result === '') result = str.slice(0, available) // hard-cut mid-word
|
|
29
|
-
return `${result}…`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const SMALL_WORDS = new Set([
|
|
33
|
-
'a', 'an', 'the', 'and', 'but', 'or', 'for', 'nor',
|
|
34
|
-
'of', 'on', 'in', 'to', 'at', 'by', 'up',
|
|
35
|
-
])
|
|
36
|
-
|
|
37
|
-
export function titleCase(text) {
|
|
38
|
-
const words = String(text).trim().split(/\s+/)
|
|
39
|
-
|
|
40
|
-
return words
|
|
41
|
-
.map((word, i) => {
|
|
42
|
-
const lower = word.toLowerCase()
|
|
43
|
-
const isEdge = i === 0 || i === words.length - 1
|
|
44
|
-
if (!isEdge && SMALL_WORDS.has(lower)) return lower
|
|
45
|
-
return lower.charAt(0).toUpperCase() + lower.slice(1)
|
|
46
|
-
})
|
|
47
|
-
.join(' ')
|
|
48
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { slugify } from '../src/index.mjs'
|
|
4
|
-
|
|
5
|
-
test('lowercases and replaces spaces with single dashes', () => {
|
|
6
|
-
assert.equal(slugify('Hello World'), 'hello-world')
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test('strips characters that are not alphanumeric, dash, or space', () => {
|
|
10
|
-
assert.equal(slugify("What's Up, Doc?"), 'whats-up-doc')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
test('collapses runs of spaces/dashes into one dash', () => {
|
|
14
|
-
assert.equal(slugify('a -- b'), 'a-b')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('trims leading/trailing dashes', () => {
|
|
18
|
-
assert.equal(slugify(' hello '), 'hello')
|
|
19
|
-
assert.equal(slugify('-hello-'), 'hello')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('returns empty string for input with no usable characters', () => {
|
|
23
|
-
assert.equal(slugify('!!!'), '')
|
|
24
|
-
})
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { truncate } from '../src/index.mjs'
|
|
4
|
-
|
|
5
|
-
test('returns short strings unchanged', () => {
|
|
6
|
-
assert.equal(truncate('hello', 10), 'hello')
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test('string of exactly max length is unchanged', () => {
|
|
10
|
-
assert.equal(truncate('exact', 5), 'exact')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
test('truncates at a word boundary and appends a single ellipsis character', () => {
|
|
14
|
-
assert.equal(truncate('the quick brown fox', 12), 'the quick…')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('result length never exceeds max', () => {
|
|
18
|
-
const out = truncate('the quick brown fox jumps', 10)
|
|
19
|
-
assert.ok(out.length <= 10, `expected <= 10, got ${out.length}: "${out}"`)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('falls back to a hard cut when the first word is longer than max', () => {
|
|
23
|
-
assert.equal(truncate('extraordinary', 6), 'extra…')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('throws RangeError for max < 1', () => {
|
|
27
|
-
assert.throws(() => truncate('x', 0), RangeError)
|
|
28
|
-
})
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { titleCase } from '../src/index.mjs'
|
|
4
|
-
|
|
5
|
-
test('capitalizes each significant word', () => {
|
|
6
|
-
assert.equal(titleCase('the quick brown fox'), 'The Quick Brown Fox')
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test('keeps small words lowercase mid-title', () => {
|
|
10
|
-
assert.equal(titleCase('a tale of two cities'), 'A Tale of Two Cities')
|
|
11
|
-
assert.equal(titleCase('war and peace'), 'War and Peace')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('always capitalizes the first and last word, even small ones', () => {
|
|
15
|
-
assert.equal(titleCase('of mice and men'), 'Of Mice and Men')
|
|
16
|
-
assert.equal(titleCase('what dreams may come to'), 'What Dreams May Come To')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('normalizes ALL-CAPS input', () => {
|
|
20
|
-
assert.equal(titleCase('THE GREAT GATSBY'), 'The Great Gatsby')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('single word is capitalized', () => {
|
|
24
|
-
assert.equal(titleCase('dune'), 'Dune')
|
|
25
|
-
})
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
- id: STORY-1
|
|
2
|
-
title: "slugify(text): URL-safe slugs"
|
|
3
|
-
priority: 1
|
|
4
|
-
acceptance:
|
|
5
|
-
- "Export `slugify(text)` from src/index.mjs"
|
|
6
|
-
- "Lowercases input and joins words with single dashes"
|
|
7
|
-
- "Strips characters that are not alphanumeric, dash, or space (apostrophes vanish: \"What's Up, Doc?\" -> \"whats-up-doc\")"
|
|
8
|
-
- "Collapses runs of spaces/dashes into one dash; trims leading/trailing dashes"
|
|
9
|
-
- "Input with no usable characters returns the empty string"
|
|
10
|
-
- "node bench-verify.mjs exits 0"
|
|
11
|
-
passes: false
|
|
12
|
-
- id: STORY-2
|
|
13
|
-
title: "truncate(text, max): word-boundary truncation with ellipsis"
|
|
14
|
-
priority: 2
|
|
15
|
-
acceptance:
|
|
16
|
-
- "Export `truncate(text, max)` from src/index.mjs"
|
|
17
|
-
- "Strings with length <= max are returned unchanged"
|
|
18
|
-
- "Longer strings are cut at a word boundary and end with the single character … (U+2026); result length never exceeds max"
|
|
19
|
-
- "If even the first word does not fit, hard-cut mid-word so the result (incl. …) has exactly max characters"
|
|
20
|
-
- "max < 1 throws RangeError"
|
|
21
|
-
- "node bench-verify.mjs exits 0"
|
|
22
|
-
passes: false
|
|
23
|
-
- id: STORY-3
|
|
24
|
-
title: "titleCase(text): English title casing"
|
|
25
|
-
priority: 3
|
|
26
|
-
acceptance:
|
|
27
|
-
- "Export `titleCase(text)` from src/index.mjs"
|
|
28
|
-
- "Capitalizes the first letter of each significant word; rest of each word lowercased (handles ALL-CAPS input)"
|
|
29
|
-
- "Small words (a, an, the, and, but, or, for, nor, of, on, in, to, at, by, up) stay lowercase mid-title"
|
|
30
|
-
- "The first and the last word are always capitalized, even if small"
|
|
31
|
-
- "node bench-verify.mjs exits 0"
|
|
32
|
-
passes: false
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Cumulative verify: the loop sets YOKE_STORY (e.g. STORY-2); we run the test
|
|
2
|
-
// files for that story AND all earlier ones, so later stories can't break
|
|
3
|
-
// earlier work. Without YOKE_STORY (final quality check), all tests run.
|
|
4
|
-
import { spawnSync } from 'node:child_process'
|
|
5
|
-
|
|
6
|
-
const TOTAL = 3
|
|
7
|
-
const story = process.env.YOKE_STORY
|
|
8
|
-
const n = story ? Number(story.split('-')[1]) : TOTAL
|
|
9
|
-
const upTo = Number.isFinite(n) && n >= 1 && n <= TOTAL ? n : TOTAL
|
|
10
|
-
|
|
11
|
-
const files = []
|
|
12
|
-
for (let i = 1; i <= upTo; i++) files.push(`tests/STORY-${i}.test.mjs`)
|
|
13
|
-
|
|
14
|
-
const r = spawnSync(process.execPath, ['--test', ...files], { stdio: 'inherit' })
|
|
15
|
-
process.exit(r.status ?? 1)
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { slugify } from '../src/index.mjs'
|
|
4
|
-
|
|
5
|
-
test('lowercases and replaces spaces with single dashes', () => {
|
|
6
|
-
assert.equal(slugify('Hello World'), 'hello-world')
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test('strips characters that are not alphanumeric, dash, or space', () => {
|
|
10
|
-
assert.equal(slugify("What's Up, Doc?"), 'whats-up-doc')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
test('collapses runs of spaces/dashes into one dash', () => {
|
|
14
|
-
assert.equal(slugify('a -- b'), 'a-b')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('trims leading/trailing dashes', () => {
|
|
18
|
-
assert.equal(slugify(' hello '), 'hello')
|
|
19
|
-
assert.equal(slugify('-hello-'), 'hello')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('returns empty string for input with no usable characters', () => {
|
|
23
|
-
assert.equal(slugify('!!!'), '')
|
|
24
|
-
})
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { truncate } from '../src/index.mjs'
|
|
4
|
-
|
|
5
|
-
test('returns short strings unchanged', () => {
|
|
6
|
-
assert.equal(truncate('hello', 10), 'hello')
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test('string of exactly max length is unchanged', () => {
|
|
10
|
-
assert.equal(truncate('exact', 5), 'exact')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
test('truncates at a word boundary and appends a single ellipsis character', () => {
|
|
14
|
-
assert.equal(truncate('the quick brown fox', 12), 'the quick…')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('result length never exceeds max', () => {
|
|
18
|
-
const out = truncate('the quick brown fox jumps', 10)
|
|
19
|
-
assert.ok(out.length <= 10, `expected <= 10, got ${out.length}: "${out}"`)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('falls back to a hard cut when the first word is longer than max', () => {
|
|
23
|
-
assert.equal(truncate('extraordinary', 6), 'extra…')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('throws RangeError for max < 1', () => {
|
|
27
|
-
assert.throws(() => truncate('x', 0), RangeError)
|
|
28
|
-
})
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { titleCase } from '../src/index.mjs'
|
|
4
|
-
|
|
5
|
-
test('capitalizes each significant word', () => {
|
|
6
|
-
assert.equal(titleCase('the quick brown fox'), 'The Quick Brown Fox')
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test('keeps small words lowercase mid-title', () => {
|
|
10
|
-
assert.equal(titleCase('a tale of two cities'), 'A Tale of Two Cities')
|
|
11
|
-
assert.equal(titleCase('war and peace'), 'War and Peace')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('always capitalizes the first and last word, even small ones', () => {
|
|
15
|
-
assert.equal(titleCase('of mice and men'), 'Of Mice and Men')
|
|
16
|
-
assert.equal(titleCase('what dreams may come to'), 'What Dreams May Come To')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('normalizes ALL-CAPS input', () => {
|
|
20
|
-
assert.equal(titleCase('THE GREAT GATSBY'), 'The Great Gatsby')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('single word is capitalized', () => {
|
|
24
|
-
assert.equal(titleCase('dune'), 'Dune')
|
|
25
|
-
})
|