@hecer/yoke 1.5.1 → 1.6.1
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 +13 -13
- package/.codex-plugin/plugin.json +7 -7
- package/CHANGELOG.md +280 -259
- package/README.md +855 -834
- package/TODOS.md +5 -5
- package/agents/docs.toml +6 -6
- package/agents/implementer.toml +6 -6
- package/agents/reviewer.toml +6 -6
- package/agents/security.toml +6 -6
- package/bench/README.md +86 -86
- package/bench/RESULTS.md +35 -35
- package/bench/output-compaction.mjs +65 -65
- package/bench/result-schema.mjs +12 -12
- package/bench/results/claude-2026-07-27T18-03-26.json +50 -50
- package/bench/results/codex-unavailable-1785175418318.json +15 -15
- package/bench/results/gemini-2026-07-27T18-03-44.json +46 -46
- package/bench/run-matrix.mjs +26 -26
- package/bench/run.mjs +106 -106
- package/canon/AGENTS.md +30 -30
- package/canon/context/DECISIONS.md +4 -4
- package/canon/context/GLOSSARY.md +11 -0
- package/canon/context/KNOWLEDGE.md +4 -4
- package/canon/context/PROJECT.md +15 -15
- package/canon/loop/loop-spec.md +65 -65
- package/canon/loop/prd.schema.md +43 -43
- package/canon/manifest.yaml +59 -53
- package/canon/policy/gates.md +7 -7
- package/canon/policy/roles.md +9 -9
- package/canon/skills/ATTRIBUTION.md +99 -71
- package/canon/skills/authoring-prd/SKILL.md +58 -58
- package/canon/skills/brainstorming/SKILL.md +164 -164
- package/canon/skills/codebase-design/DEEPENING.md +15 -0
- package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -0
- package/canon/skills/codebase-design/SKILL.md +39 -0
- package/canon/skills/dispatching-parallel-agents/SKILL.md +182 -182
- package/canon/skills/document-release/SKILL.md +302 -297
- package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -0
- package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -0
- package/canon/skills/domain-modeling/SKILL.md +35 -0
- package/canon/skills/executing-plans/SKILL.md +70 -70
- package/canon/skills/finishing-a-development-branch/SKILL.md +200 -200
- package/canon/skills/health/SKILL.md +177 -177
- package/canon/skills/maintaining-context/SKILL.md +34 -34
- package/canon/skills/minimal-code/SKILL.md +21 -21
- package/canon/skills/no-ai-slop/SKILL.md +103 -0
- package/canon/skills/no-ai-slop/eval.md +43 -0
- package/canon/skills/plan-ceo-review/SKILL.md +541 -541
- package/canon/skills/plan-eng-review/SKILL.md +362 -362
- package/canon/skills/receiving-code-review/SKILL.md +213 -213
- package/canon/skills/requesting-code-review/SKILL.md +105 -105
- package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -0
- package/canon/skills/retro/SKILL.md +397 -397
- package/canon/skills/review/SKILL.md +246 -246
- package/canon/skills/ship/SKILL.md +691 -691
- package/canon/skills/subagent-driven-development/SKILL.md +277 -277
- package/canon/skills/systematic-debugging/SKILL.md +296 -296
- package/canon/skills/tdd/SKILL.md +371 -371
- package/canon/skills/unslop-ui/SKILL.md +34 -34
- package/canon/skills/using-git-worktrees/SKILL.md +218 -218
- package/canon/skills/verification-before-completion/SKILL.md +139 -139
- package/canon/skills/visual-verification/SKILL.md +54 -54
- package/canon/skills/workflow/SKILL.md +22 -22
- package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -0
- package/canon/skills/writing-for-agents/SKILL.md +42 -0
- package/canon/skills/writing-plans/SKILL.md +152 -152
- package/canon/skills/writing-skills/SKILL.md +655 -655
- package/canon/skills/yoke-retrofit/SKILL.md +26 -26
- package/canon/skills/yoke-workflow/SKILL.md +20 -20
- package/canon/tools/codex-rtk-hook.mjs +35 -35
- package/canon/tools/graphify.md +3 -3
- package/canon/tools/playwright-mcp.md +3 -3
- package/canon/tools/rtk.md +7 -7
- package/canon/tools/serena.md +6 -6
- package/dist/agents/process.js +3 -0
- package/dist/canon/manifest.js +2 -0
- package/dist/canon/skill-package.js +113 -0
- package/dist/canon/validate.js +16 -1
- package/dist/context/command.js +4 -1
- package/dist/context/context.js +6 -0
- package/dist/loop/dispatcher.js +1 -1
- package/dist/loop/loop.js +26 -0
- package/dist/loop/parallel-command.js +3 -0
- package/dist/loop/run-command.js +11 -0
- package/dist/loop/watchdog.js +28 -11
- package/dist/loop/worker.js +11 -0
- package/dist/prd/command.js +17 -17
- package/dist/retrofit/apply.js +22 -7
- package/dist/retrofit/command.js +4 -1
- package/dist/retrofit/config.js +4 -0
- package/dist/retrofit/context-actions.js +1 -1
- package/dist/retrofit/detect.js +2 -0
- package/dist/retrofit/planners/claude.js +16 -20
- package/dist/retrofit/planners/codex.js +3 -7
- package/dist/retrofit/planners/gemini.js +11 -1
- package/dist/retrofit/preserve.js +2 -2
- package/dist/retrofit/report.js +5 -0
- package/dist/retrofit/skill-actions.js +66 -0
- package/dist/retrofit/ui-detect.js +83 -0
- package/dist/scan/gate.js +36 -0
- package/docs/MIGRATING-TO-1.0.md +33 -33
- package/docs/MIGRATING-TO-1.1.md +27 -27
- package/docs/MIGRATING-TO-1.4.md +70 -70
- package/docs/PUBLISHING.md +91 -91
- package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -981
- package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -258
- package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -1006
- package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -374
- package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -450
- package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -1024
- package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -574
- package/docs/superpowers/plans/2026-08-13-gauntlet-quality-loop.md +537 -537
- package/docs/superpowers/plans/2026-08-16-artifact-backed-output-compaction.md +329 -329
- package/docs/superpowers/plans/2026-08-20-automatic-ui-design-gate.md +59 -0
- package/docs/superpowers/plans/2026-08-20-capability-skills-and-context.md +51 -0
- package/docs/superpowers/plans/2026-08-20-complete-skill-packages-and-invocation.md +59 -0
- package/docs/superpowers/plans/2026-08-20-windows-reliability-and-release.md +67 -0
- package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -146
- package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -106
- package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -186
- package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -113
- package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -98
- package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -200
- package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -155
- package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -422
- package/docs/superpowers/specs/2026-08-16-artifact-backed-output-compaction-design.md +166 -166
- package/docs/superpowers/specs/2026-08-20-skill-capabilities-and-reliability-design.md +391 -0
- package/gemini-extension.json +6 -6
- package/hooks/hooks.json +19 -19
- package/package.json +84 -84
package/dist/prd/command.js
CHANGED
|
@@ -5,23 +5,23 @@ import { acceptanceText, criterionCommandProblem, isAcceptanceCriterion, loadPrd
|
|
|
5
5
|
import { agentInvocation, buildWatchdogInvocation, runAgent, isAgentAvailable, } from '../loop/runner.js';
|
|
6
6
|
import { resolveIdleMs } from '../loop/run-command.js';
|
|
7
7
|
import { detectHostAgent, resolveRunnerAgent } from '../agents/host.js';
|
|
8
|
-
export const PRD_TEMPLATE = `# Yoke PRD — the loop picks the lowest-priority open story each iteration.
|
|
9
|
-
# Story format (see canon/loop/prd.schema.md):
|
|
10
|
-
# - id: STORY-1
|
|
11
|
-
# title: scaffold the project with a runnable test suite
|
|
12
|
-
# priority: 1
|
|
13
|
-
# needs: [] # optional story IDs that must pass first
|
|
14
|
-
# area: foundation # optional collision domain for parallel runs
|
|
15
|
-
# agent: codex # optional claude|codex|gemini affinity
|
|
16
|
-
# acceptance:
|
|
17
|
-
# - id: suite-runs
|
|
18
|
-
# text: "the project test suite can run"
|
|
19
|
-
# verify: ["npm run test:suite-runs"]
|
|
20
|
-
# - id: scaffold-starts
|
|
21
|
-
# text: "the scaffolded application starts"
|
|
22
|
-
# verify: ["npm run test:scaffold-starts"]
|
|
23
|
-
# passes: false
|
|
24
|
-
[]
|
|
8
|
+
export const PRD_TEMPLATE = `# Yoke PRD — the loop picks the lowest-priority open story each iteration.
|
|
9
|
+
# Story format (see canon/loop/prd.schema.md):
|
|
10
|
+
# - id: STORY-1
|
|
11
|
+
# title: scaffold the project with a runnable test suite
|
|
12
|
+
# priority: 1
|
|
13
|
+
# needs: [] # optional story IDs that must pass first
|
|
14
|
+
# area: foundation # optional collision domain for parallel runs
|
|
15
|
+
# agent: codex # optional claude|codex|gemini affinity
|
|
16
|
+
# acceptance:
|
|
17
|
+
# - id: suite-runs
|
|
18
|
+
# text: "the project test suite can run"
|
|
19
|
+
# verify: ["npm run test:suite-runs"]
|
|
20
|
+
# - id: scaffold-starts
|
|
21
|
+
# text: "the scaffolded application starts"
|
|
22
|
+
# verify: ["npm run test:scaffold-starts"]
|
|
23
|
+
# passes: false
|
|
24
|
+
[]
|
|
25
25
|
`;
|
|
26
26
|
export const MAX_PLANNING_BRIEF_CHARS = 20_000;
|
|
27
27
|
export const MAX_PLANNING_BRIEF_BYTES = MAX_PLANNING_BRIEF_CHARS * 4;
|
package/dist/retrofit/apply.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { mergeJson } from './merge-json.js';
|
|
4
4
|
import { carryPreserved } from './preserve.js';
|
|
@@ -10,13 +10,17 @@ export function applyActions(actions, targetDir, opts) {
|
|
|
10
10
|
let backedUp;
|
|
11
11
|
let content = action.content;
|
|
12
12
|
let reason = action.reason;
|
|
13
|
+
if (action.merge && typeof action.content !== 'string') {
|
|
14
|
+
throw new Error(`yoke: binary content cannot be merged: ${action.target}`);
|
|
15
|
+
}
|
|
13
16
|
if (existsSync(dest)) {
|
|
14
17
|
if (action.ifAbsent) {
|
|
15
18
|
results.push({ target: action.target, status: 'unchanged', reason: `${action.reason} (exists, left untouched)` });
|
|
16
19
|
continue;
|
|
17
20
|
}
|
|
18
|
-
const
|
|
21
|
+
const currentBytes = readFileSync(dest);
|
|
19
22
|
if (action.merge) {
|
|
23
|
+
const current = currentBytes.toString('utf8');
|
|
20
24
|
let parsedCurrent;
|
|
21
25
|
try {
|
|
22
26
|
parsedCurrent = JSON.parse(current);
|
|
@@ -37,11 +41,18 @@ export function applyActions(actions, targetDir, opts) {
|
|
|
37
41
|
results.push({ target: action.target, status: 'merged', backedUp, reason: action.reason });
|
|
38
42
|
continue;
|
|
39
43
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
if (typeof action.content === 'string') {
|
|
45
|
+
const current = currentBytes.toString('utf8');
|
|
46
|
+
// Carry user content marked with yoke preserve markers into the new file.
|
|
47
|
+
content = carryPreserved(current, action.content);
|
|
48
|
+
if (content !== action.content)
|
|
49
|
+
reason = `${action.reason} (preserve block kept)`;
|
|
50
|
+
if (current === content) {
|
|
51
|
+
results.push({ target: action.target, status: 'unchanged', reason: action.reason });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (currentBytes.equals(Buffer.from(action.content))) {
|
|
45
56
|
results.push({ target: action.target, status: 'unchanged', reason: action.reason });
|
|
46
57
|
continue;
|
|
47
58
|
}
|
|
@@ -55,6 +66,10 @@ export function applyActions(actions, targetDir, opts) {
|
|
|
55
66
|
}
|
|
56
67
|
mkdirSync(dirname(dest), { recursive: true });
|
|
57
68
|
writeFileSync(dest, content);
|
|
69
|
+
if (action.executable !== undefined && process.platform !== 'win32') {
|
|
70
|
+
const currentMode = statSync(dest).mode;
|
|
71
|
+
chmodSync(dest, action.executable ? currentMode | 0o111 : currentMode & ~0o111);
|
|
72
|
+
}
|
|
58
73
|
results.push({ target: action.target, status, backedUp, reason });
|
|
59
74
|
}
|
|
60
75
|
return results;
|
package/dist/retrofit/command.js
CHANGED
|
@@ -31,8 +31,11 @@ export function runRetrofit(targetDir, opts) {
|
|
|
31
31
|
agents: mergedAgents,
|
|
32
32
|
loop: { ...existing?.loop, enabled: opts.loop },
|
|
33
33
|
codeGraph,
|
|
34
|
+
...(existing?.design
|
|
35
|
+
? { design: existing.design }
|
|
36
|
+
: detection.ui.detected ? { design: { mode: 'auto', max: 4 } } : {}),
|
|
34
37
|
};
|
|
35
38
|
saveConfig(targetDir, config);
|
|
36
|
-
console.log(formatReport(applied, { loopEnabled: config.loop.enabled, detectedAgents: detection.agents }));
|
|
39
|
+
console.log(formatReport(applied, { loopEnabled: config.loop.enabled, detectedAgents: detection.agents, ui: detection.ui }));
|
|
37
40
|
return 0;
|
|
38
41
|
}
|
package/dist/retrofit/config.js
CHANGED
|
@@ -80,6 +80,10 @@ export const YokeConfigSchema = z.object({
|
|
|
80
80
|
// Optional performance budget gate: a benchmark command that must exit 0 for a
|
|
81
81
|
// story to land (runs after verify). Benchmarks are noisy → retried like verify.
|
|
82
82
|
perf: z.object({ command: z.string().min(1), retries: z.number().int().nonnegative().optional() }).optional(),
|
|
83
|
+
design: z.object({
|
|
84
|
+
mode: z.enum(['off', 'auto', 'on']),
|
|
85
|
+
max: z.number().int().positive().default(4),
|
|
86
|
+
}).optional(),
|
|
83
87
|
codeGraph: CodeGraphSchema.optional(),
|
|
84
88
|
smoke: SmokeSchema.optional(),
|
|
85
89
|
quality: ProjectQualityDefaultsSchema.optional(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
const CONTEXT_FILES = ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md'];
|
|
3
|
+
const CONTEXT_FILES = ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md', 'GLOSSARY.md'];
|
|
4
4
|
export function baseContextActions(canonDir) {
|
|
5
5
|
return CONTEXT_FILES.map(name => {
|
|
6
6
|
let content;
|
package/dist/retrofit/detect.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { detectUiProject } from './ui-detect.js';
|
|
3
4
|
export function detectProject(targetDir) {
|
|
4
5
|
const has = (...parts) => existsSync(join(targetDir, ...parts));
|
|
5
6
|
const agents = [];
|
|
@@ -13,5 +14,6 @@ export function detectProject(targetDir) {
|
|
|
13
14
|
agents,
|
|
14
15
|
hasAgentsMd: has('AGENTS.md'),
|
|
15
16
|
hasYokeConfig: has('.yoke', 'config.yaml'),
|
|
17
|
+
ui: detectUiProject(targetDir),
|
|
16
18
|
};
|
|
17
19
|
}
|
|
@@ -5,32 +5,28 @@ import { mcpServers, rtkInstruction } from '../tools.js';
|
|
|
5
5
|
import { hasWsl } from '../wsl.js';
|
|
6
6
|
import { detectGstack } from '../gstack.js';
|
|
7
7
|
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
8
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
9
|
+
const GSTACK_COMPOSE = `## Composed tools (gstack detected)
|
|
10
|
+
|
|
11
|
+
This project also has [gstack](https://github.com/garrytan/gstack) installed. For capabilities Yoke does not ship, prefer gstack's skills:
|
|
12
|
+
|
|
13
|
+
- Live-browser QA → \`/qa\`
|
|
14
|
+
- Security audit → \`/cso\`
|
|
15
|
+
- Ship / deploy → \`/ship\`, \`/land-and-deploy\`
|
|
15
16
|
`;
|
|
16
|
-
const claudeMd = (rtkNote, composeNote) => `# Project Instructions
|
|
17
|
-
|
|
18
|
-
This project uses the Yoke harness. Baseline instructions:
|
|
19
|
-
|
|
20
|
-
@AGENTS.md
|
|
21
|
-
${rtkNote ? `\n${rtkNote}\n` : ''}${composeNote ? `\n${composeNote}\n` : ''}
|
|
22
|
-
${PRESERVE_SCAFFOLD}
|
|
17
|
+
const claudeMd = (rtkNote, composeNote) => `# Project Instructions
|
|
18
|
+
|
|
19
|
+
This project uses the Yoke harness. Baseline instructions:
|
|
20
|
+
|
|
21
|
+
@AGENTS.md
|
|
22
|
+
${rtkNote ? `\n${rtkNote}\n` : ''}${composeNote ? `\n${composeNote}\n` : ''}
|
|
23
|
+
${PRESERVE_SCAFFOLD}
|
|
23
24
|
`;
|
|
24
25
|
export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGraph = 'graphify', gstackDetected = detectGstack(targetDir)) {
|
|
25
26
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
26
27
|
const actions = [];
|
|
27
28
|
for (const skill of manifest.skills) {
|
|
28
|
-
actions.push(
|
|
29
|
-
kind: 'write',
|
|
30
|
-
target: `.claude/skills/${skill.id}/SKILL.md`,
|
|
31
|
-
content: readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8'),
|
|
32
|
-
reason: `skill: ${skill.id}`,
|
|
33
|
-
});
|
|
29
|
+
actions.push(...skillPackageActions(canonDir, skill, 'claude'));
|
|
34
30
|
}
|
|
35
31
|
actions.push({
|
|
36
32
|
kind: 'write',
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { loadManifest } from '../../canon/manifest.js';
|
|
4
4
|
import { mcpServers, rtkInstruction } from '../tools.js';
|
|
5
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
5
6
|
function tomlMcp(codeGraph) {
|
|
6
7
|
const servers = mcpServers(codeGraph);
|
|
7
8
|
// Codex reads MCP servers from ~/.codex/config.toml. This project-level file is a
|
|
@@ -16,17 +17,12 @@ function tomlMcp(codeGraph) {
|
|
|
16
17
|
export function planCodex(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
17
18
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
18
19
|
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
19
|
-
const actions = manifest.skills.
|
|
20
|
-
kind: 'write',
|
|
21
|
-
target: `.agents/skills/${skill.id}/SKILL.md`,
|
|
22
|
-
content: readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8'),
|
|
23
|
-
reason: `skill: ${skill.id}`,
|
|
24
|
-
}));
|
|
20
|
+
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'codex'));
|
|
25
21
|
const roles = [
|
|
26
22
|
['implementer', 'Implementation specialist for one scoped story.', 'workspace-write', 'Implement only the assigned scope. Use tests first, run verification, and do not review or commit your own work.'],
|
|
27
23
|
['reviewer', 'Read-only reviewer for correctness and acceptance criteria.', 'read-only', 'Review observed diffs and test evidence. Do not modify files. Return only findings grounded in evidence.'],
|
|
28
24
|
['security', 'Read-only security reviewer for changed code.', 'read-only', 'Inspect changed code for exploitable security regressions. Do not modify files and avoid speculative findings.'],
|
|
29
|
-
['docs', 'Documentation specialist for release and API consistency.', 'workspace-write', 'Update only documentation required by the assigned change. Verify commands and version references against the repository.'],
|
|
25
|
+
['docs', 'Documentation specialist for release and API consistency.', 'workspace-write', 'Update only documentation required by the assigned change. Verify commands and version references against the repository. Use no-ai-slop Detect mode before prose edits, preserve the writer\'s voice, and apply only observed fixes.'],
|
|
30
26
|
];
|
|
31
27
|
actions.push({
|
|
32
28
|
kind: 'write',
|
|
@@ -4,6 +4,7 @@ import { loadManifest } from '../../canon/manifest.js';
|
|
|
4
4
|
import { parseFrontmatter } from '../../canon/frontmatter.js';
|
|
5
5
|
import { mcpServers, rtkInstruction } from '../tools.js';
|
|
6
6
|
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
7
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
7
8
|
function tomlString(s) {
|
|
8
9
|
return '"""\n' + s.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"') + '\n"""';
|
|
9
10
|
}
|
|
@@ -12,10 +13,18 @@ export function planGemini(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
12
13
|
const actions = [];
|
|
13
14
|
// GEMINI.md: baseline + rtk instruction (Gemini has no rewrite hook).
|
|
14
15
|
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
16
|
+
const autoSkillIndex = manifest.skills
|
|
17
|
+
.filter(skill => skill.invocation === 'auto')
|
|
18
|
+
.map(skill => {
|
|
19
|
+
const source = readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8');
|
|
20
|
+
const description = String(parseFrontmatter(source)?.description ?? skill.id).replace(/\s+/gu, ' ').trim();
|
|
21
|
+
return `- \`/${skill.id}\` — ${description}`;
|
|
22
|
+
})
|
|
23
|
+
.join('\n');
|
|
15
24
|
actions.push({
|
|
16
25
|
kind: 'write',
|
|
17
26
|
target: 'GEMINI.md',
|
|
18
|
-
content: `${baseline}\n${rtkInstruction()}\n\n${PRESERVE_SCAFFOLD}\n`,
|
|
27
|
+
content: `${baseline}\n${rtkInstruction()}\n\n## Yoke automatic skills\n\nUse the matching command when its capability is relevant:\n\n${autoSkillIndex}\n\n${PRESERVE_SCAFFOLD}\n`,
|
|
19
28
|
reason: 'baseline + rtk instruction (no hook on Gemini)',
|
|
20
29
|
});
|
|
21
30
|
// One TOML slash command per skill.
|
|
@@ -33,6 +42,7 @@ export function planGemini(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
33
42
|
content: `description = "${description.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\nprompt = ${tomlString(prompt)}\n`,
|
|
34
43
|
reason: `gemini command: ${skill.id}`,
|
|
35
44
|
});
|
|
45
|
+
actions.push(...skillPackageActions(canonDir, skill, 'gemini'));
|
|
36
46
|
}
|
|
37
47
|
// settings.json: MCP servers + read AGENTS.md as context.
|
|
38
48
|
actions.push({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const PRESERVE_START = '<!-- yoke:preserve:start -->';
|
|
2
2
|
export const PRESERVE_END = '<!-- yoke:preserve:end -->';
|
|
3
|
-
export const PRESERVE_SCAFFOLD = `${PRESERVE_START}
|
|
4
|
-
<!-- Project-specific instructions go here. Yoke keeps this block across retrofits. -->
|
|
3
|
+
export const PRESERVE_SCAFFOLD = `${PRESERVE_START}
|
|
4
|
+
<!-- Project-specific instructions go here. Yoke keeps this block across retrofits. -->
|
|
5
5
|
${PRESERVE_END}`;
|
|
6
6
|
/**
|
|
7
7
|
* Extract the inner content of every balanced preserve-marker pair, in order.
|
package/dist/retrofit/report.js
CHANGED
|
@@ -8,6 +8,11 @@ export function formatReport(applied, meta) {
|
|
|
8
8
|
}
|
|
9
9
|
lines.push('');
|
|
10
10
|
lines.push(`Detected agents: ${meta.detectedAgents.length ? meta.detectedAgents.join(', ') : 'none'}`);
|
|
11
|
+
if (meta.ui) {
|
|
12
|
+
lines.push(meta.ui.detected
|
|
13
|
+
? `UI project: detected (${meta.ui.signals.join('; ')})`
|
|
14
|
+
: 'UI project: not detected');
|
|
15
|
+
}
|
|
11
16
|
lines.push(`Summary: ${count('created')} created, ${count('overwritten')} overwritten, ${count('merged')} merged, ${count('unchanged')} unchanged`);
|
|
12
17
|
lines.push(`Loop: ${meta.loopEnabled ? 'enabled' : 'disabled'}`);
|
|
13
18
|
return lines.join('\n');
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parse, stringify } from 'yaml';
|
|
2
|
+
import { enumerateSkillPackage } from '../canon/skill-package.js';
|
|
3
|
+
const roots = {
|
|
4
|
+
claude: '.claude/skills',
|
|
5
|
+
codex: '.agents/skills',
|
|
6
|
+
gemini: '.gemini/skills',
|
|
7
|
+
};
|
|
8
|
+
function manualClaudeSkill(content, skill) {
|
|
9
|
+
const source = content.toString('utf8');
|
|
10
|
+
const match = source.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/u);
|
|
11
|
+
if (!match)
|
|
12
|
+
throw new Error(`skill ${skill.id}: cannot adapt manual invocation without frontmatter`);
|
|
13
|
+
const newline = match[1].includes('\r\n') ? '\r\n' : '\n';
|
|
14
|
+
const frontmatter = match[2];
|
|
15
|
+
const adapted = /(?:^|\r?\n)disable-model-invocation:/u.test(frontmatter)
|
|
16
|
+
? frontmatter.replace(/(^|\r?\n)disable-model-invocation:\s*[^\r\n]*/u, `$1disable-model-invocation: true`)
|
|
17
|
+
: `${frontmatter}${newline}disable-model-invocation: true`;
|
|
18
|
+
return `${match[1]}${adapted}${match[3]}${source.slice(match[0].length)}`;
|
|
19
|
+
}
|
|
20
|
+
function codexPolicy(skill, existing) {
|
|
21
|
+
const allow = skill.invocation === 'auto';
|
|
22
|
+
const document = existing ? parse(existing.content.toString('utf8')) : {};
|
|
23
|
+
if (document === null || typeof document !== 'object' || Array.isArray(document)) {
|
|
24
|
+
throw new Error(`skill ${skill.id}: agents/openai.yaml must contain a YAML object`);
|
|
25
|
+
}
|
|
26
|
+
const record = document;
|
|
27
|
+
const policy = record.policy;
|
|
28
|
+
if (policy !== undefined && (typeof policy !== 'object' || policy === null || Array.isArray(policy))) {
|
|
29
|
+
throw new Error(`skill ${skill.id}: agents/openai.yaml policy must be a YAML object`);
|
|
30
|
+
}
|
|
31
|
+
const policyRecord = (policy ?? {});
|
|
32
|
+
const declared = policyRecord.allow_implicit_invocation;
|
|
33
|
+
if (declared !== undefined && declared !== allow) {
|
|
34
|
+
throw new Error(`skill ${skill.id}: agents/openai.yaml invocation policy conflicts with manifest`);
|
|
35
|
+
}
|
|
36
|
+
return stringify({ ...record, policy: { ...policyRecord, allow_implicit_invocation: allow } });
|
|
37
|
+
}
|
|
38
|
+
function portableContent(file) {
|
|
39
|
+
const text = file.content.toString('utf8');
|
|
40
|
+
return !text.includes('\0') && Buffer.from(text, 'utf8').equals(file.content) ? text : file.content;
|
|
41
|
+
}
|
|
42
|
+
export function skillPackageActions(canonDir, skill, provider) {
|
|
43
|
+
const files = enumerateSkillPackage(canonDir, skill);
|
|
44
|
+
const policyPath = 'agents/openai.yaml';
|
|
45
|
+
const existingPolicy = provider === 'codex' ? files.find(file => file.relativePath === policyPath) : undefined;
|
|
46
|
+
const actions = files
|
|
47
|
+
.filter(file => provider !== 'codex' || file.relativePath !== policyPath)
|
|
48
|
+
.map(file => ({
|
|
49
|
+
kind: 'write',
|
|
50
|
+
target: `${roots[provider]}/${skill.id}/${file.relativePath}`,
|
|
51
|
+
content: provider === 'claude' && skill.invocation === 'manual' && file.relativePath === 'SKILL.md'
|
|
52
|
+
? manualClaudeSkill(file.content, skill)
|
|
53
|
+
: portableContent(file),
|
|
54
|
+
executable: file.executable,
|
|
55
|
+
reason: `skill: ${skill.id} (${file.relativePath})`,
|
|
56
|
+
}));
|
|
57
|
+
if (provider === 'codex') {
|
|
58
|
+
actions.push({
|
|
59
|
+
kind: 'write',
|
|
60
|
+
target: `${roots.codex}/${skill.id}/${policyPath}`,
|
|
61
|
+
content: codexPolicy(skill, existingPolicy),
|
|
62
|
+
reason: `skill invocation policy: ${skill.id}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return actions;
|
|
66
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
const UI_DEPENDENCIES = new Set([
|
|
5
|
+
'@angular/core', '@astrojs/react', '@astrojs/svelte', '@astrojs/vue',
|
|
6
|
+
'@sveltejs/kit', '@vitejs/plugin-react', '@vitejs/plugin-react-swc',
|
|
7
|
+
'@vitejs/plugin-vue', '@vitejs/plugin-vue-jsx', 'astro', 'next', 'nuxt',
|
|
8
|
+
'react', 'react-dom', 'svelte', 'vue',
|
|
9
|
+
]);
|
|
10
|
+
const UI_EXTENSIONS = new Set(['.tsx', '.jsx', '.vue', '.svelte', '.astro']);
|
|
11
|
+
const SOURCE_ROOTS = ['src', 'app', 'pages', 'components', 'web', 'frontend'];
|
|
12
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
13
|
+
'.git', '.next', '.nuxt', '.svelte-kit', '.yoke', '__fixtures__', 'build',
|
|
14
|
+
'coverage', 'dist', 'fixtures', 'node_modules', 'test', 'tests',
|
|
15
|
+
]);
|
|
16
|
+
function packageSignals(targetDir) {
|
|
17
|
+
const file = join(targetDir, 'package.json');
|
|
18
|
+
if (!existsSync(file))
|
|
19
|
+
return [];
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(file, 'utf8'));
|
|
22
|
+
const names = new Set();
|
|
23
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
24
|
+
const section = pkg[key];
|
|
25
|
+
if (section && typeof section === 'object' && !Array.isArray(section)) {
|
|
26
|
+
for (const name of Object.keys(section))
|
|
27
|
+
if (UI_DEPENDENCIES.has(name))
|
|
28
|
+
names.add(name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...names].sort().map(name => `dependency: ${name}`);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function sourceSignals(targetDir) {
|
|
38
|
+
const signals = [];
|
|
39
|
+
let visited = 0;
|
|
40
|
+
const visit = (directory) => {
|
|
41
|
+
if (visited >= 10_000)
|
|
42
|
+
return;
|
|
43
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
44
|
+
if (visited++ >= 10_000)
|
|
45
|
+
return;
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
if (!IGNORED_DIRECTORIES.has(entry.name))
|
|
48
|
+
visit(join(directory, entry.name));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!entry.isFile())
|
|
52
|
+
continue;
|
|
53
|
+
const extension = entry.name.slice(entry.name.lastIndexOf('.')).toLowerCase();
|
|
54
|
+
if (UI_EXTENSIONS.has(extension)) {
|
|
55
|
+
signals.push(`source: ${relative(targetDir, join(directory, entry.name)).replaceAll('\\', '/')}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
for (const root of SOURCE_ROOTS) {
|
|
60
|
+
const directory = join(targetDir, root);
|
|
61
|
+
if (existsSync(directory))
|
|
62
|
+
visit(directory);
|
|
63
|
+
}
|
|
64
|
+
return signals.sort();
|
|
65
|
+
}
|
|
66
|
+
function hasSmokeFlows(targetDir) {
|
|
67
|
+
const file = join(targetDir, '.yoke', 'config.yaml');
|
|
68
|
+
if (!existsSync(file))
|
|
69
|
+
return false;
|
|
70
|
+
try {
|
|
71
|
+
const document = parse(readFileSync(file, 'utf8'));
|
|
72
|
+
return Array.isArray(document?.smoke?.flows) && document.smoke.flows.length > 0;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function detectUiProject(targetDir) {
|
|
79
|
+
const signals = [...packageSignals(targetDir), ...sourceSignals(targetDir)];
|
|
80
|
+
if (hasSmokeFlows(targetDir))
|
|
81
|
+
signals.push('config: smoke flows');
|
|
82
|
+
return { detected: signals.length > 0, signals };
|
|
83
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { compactCommandOutput } from '../output/compact.js';
|
|
3
|
+
import { writeOutputArtifact } from '../output/artifact.js';
|
|
4
|
+
import { DEFAULT_OUTPUT_POLICY } from '../output/types.js';
|
|
5
|
+
import { scanDir } from './design.js';
|
|
6
|
+
export function designVerifier(max, options = {}) {
|
|
7
|
+
return (targetDir) => {
|
|
8
|
+
const { findings, score } = scanDir(targetDir);
|
|
9
|
+
const label = `design ${score <= max ? 'passed' : 'failed'}: score ${score} (${findings.length} tell${findings.length === 1 ? '' : 's'}), budget ${max}`;
|
|
10
|
+
if (score <= max)
|
|
11
|
+
return { passed: true, summary: label };
|
|
12
|
+
const raw = findings.map(finding => {
|
|
13
|
+
const file = relative(targetDir, finding.file).replaceAll('\\', '/');
|
|
14
|
+
return `${file}:${finding.line} ${finding.tell} — ${finding.hint}\n ${finding.text}`;
|
|
15
|
+
}).join('\n');
|
|
16
|
+
const policy = options.policy ?? DEFAULT_OUTPUT_POLICY;
|
|
17
|
+
const compacted = compactCommandOutput(raw, { previewBytes: policy.previewBytes });
|
|
18
|
+
const parts = [label];
|
|
19
|
+
if (compacted.preview)
|
|
20
|
+
parts.push(compacted.preview);
|
|
21
|
+
if (compacted.originalBytes > policy.artifactThresholdBytes) {
|
|
22
|
+
try {
|
|
23
|
+
const artifact = (options.artifactWriter ?? writeOutputArtifact)(targetDir, raw, {
|
|
24
|
+
phase: 'design',
|
|
25
|
+
storyId: process.env.YOKE_STORY,
|
|
26
|
+
});
|
|
27
|
+
parts.push(artifact.marker);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
parts.push(`[artifact unavailable: ${message.replace(/[\u0000-\u001F\u007F]/gu, ' ').slice(0, 240)}]`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { passed: false, summary: parts.join('\n') };
|
|
35
|
+
};
|
|
36
|
+
}
|
package/docs/MIGRATING-TO-1.0.md
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
# Migrating to Yoke 1.0
|
|
2
|
-
|
|
3
|
-
Yoke 1.0 changes unsafe implicit behavior into explicit policy.
|
|
4
|
-
|
|
5
|
-
## Runner permissions
|
|
6
|
-
|
|
7
|
-
The default is `runner.permissions: safe`. Automation that intentionally requires a full
|
|
8
|
-
sandbox bypass must pass `--unsafe` or configure `runner.permissions: unsafe`. Use
|
|
9
|
-
`read-only` for planning and probing.
|
|
10
|
-
|
|
11
|
-
## Reviews
|
|
12
|
-
|
|
13
|
-
Reviewers write `.yoke/review-verdict.json`; Yoke validates and consumes it. The reviewer must
|
|
14
|
-
differ from the implementer unless `--allow-self-review` is explicit. CI can add `--json`.
|
|
15
|
-
|
|
16
|
-
## Commit ownership
|
|
17
|
-
|
|
18
|
-
Yoke resolves identity before implementation. Configure it when Git has no identity:
|
|
19
|
-
|
|
20
|
-
```yaml
|
|
21
|
-
commit:
|
|
22
|
-
authorName: HECer
|
|
23
|
-
authorEmail: hec_er@web.de
|
|
24
|
-
allowCoAuthors: false
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## PRDs, audit, and cleanup
|
|
28
|
-
|
|
29
|
-
Existing PRDs remain valid. Optional `needs`, `area`, and `agent` fields add dependencies,
|
|
30
|
-
collision domains, and affinity. Enable the story audit gate with `audit.enabled: true` and
|
|
31
|
-
version suppressions with `suppressionsVersion: 1`.
|
|
32
|
-
|
|
33
|
-
`yoke loop cleanup` now reports retained worktrees. Add `--remove-worktrees` for deletion.
|
|
1
|
+
# Migrating to Yoke 1.0
|
|
2
|
+
|
|
3
|
+
Yoke 1.0 changes unsafe implicit behavior into explicit policy.
|
|
4
|
+
|
|
5
|
+
## Runner permissions
|
|
6
|
+
|
|
7
|
+
The default is `runner.permissions: safe`. Automation that intentionally requires a full
|
|
8
|
+
sandbox bypass must pass `--unsafe` or configure `runner.permissions: unsafe`. Use
|
|
9
|
+
`read-only` for planning and probing.
|
|
10
|
+
|
|
11
|
+
## Reviews
|
|
12
|
+
|
|
13
|
+
Reviewers write `.yoke/review-verdict.json`; Yoke validates and consumes it. The reviewer must
|
|
14
|
+
differ from the implementer unless `--allow-self-review` is explicit. CI can add `--json`.
|
|
15
|
+
|
|
16
|
+
## Commit ownership
|
|
17
|
+
|
|
18
|
+
Yoke resolves identity before implementation. Configure it when Git has no identity:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
commit:
|
|
22
|
+
authorName: HECer
|
|
23
|
+
authorEmail: hec_er@web.de
|
|
24
|
+
allowCoAuthors: false
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## PRDs, audit, and cleanup
|
|
28
|
+
|
|
29
|
+
Existing PRDs remain valid. Optional `needs`, `area`, and `agent` fields add dependencies,
|
|
30
|
+
collision domains, and affinity. Enable the story audit gate with `audit.enabled: true` and
|
|
31
|
+
version suppressions with `suppressionsVersion: 1`.
|
|
32
|
+
|
|
33
|
+
`yoke loop cleanup` now reports retained worktrees. Add `--remove-worktrees` for deletion.
|
package/docs/MIGRATING-TO-1.1.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# Migrating to Yoke 1.1
|
|
2
|
-
|
|
3
|
-
Yoke 1.1 makes Claude, Codex, and Gemini share the same setup, planning, runner-selection, and decision behavior.
|
|
4
|
-
|
|
5
|
-
Run the setup wizard once in an existing project:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npx @hecer/yoke@1.1.0 setup .
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
It preserves existing Yoke configuration and asks for target agents, code graph, loop state, default runner, and decision mode. A non-interactive agent can apply explicit choices with `--yes`.
|
|
12
|
-
|
|
13
|
-
The new config fields are optional and backward-compatible:
|
|
14
|
-
|
|
15
|
-
```yaml
|
|
16
|
-
runner:
|
|
17
|
-
agent: codex
|
|
18
|
-
permissions: safe
|
|
19
|
-
loop:
|
|
20
|
-
enabled: true
|
|
21
|
-
timeoutMinutes: 30
|
|
22
|
-
decisionPolicy: critical # auto | critical
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`loop.onAmbiguity: resolve|abort` and `--on-ambiguity=` still work. Prefer `decisionPolicy` for new projects: `auto` resolves routine choices without asking; `critical` pauses only for high-impact decisions and continues after `yoke loop answer`. Resume restores the original isolation, review, runner, permissions, timeout, and policy flags instead of silently weakening the run. If the restart cannot begin, `yoke loop resume` retries from the request-bound state stored under Git's private state directory.
|
|
26
|
-
|
|
27
|
-
Restart an already-open Codex task after retrofit if it does not discover the newly generated `.agents/skills/yoke-workflow/SKILL.md`.
|
|
1
|
+
# Migrating to Yoke 1.1
|
|
2
|
+
|
|
3
|
+
Yoke 1.1 makes Claude, Codex, and Gemini share the same setup, planning, runner-selection, and decision behavior.
|
|
4
|
+
|
|
5
|
+
Run the setup wizard once in an existing project:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @hecer/yoke@1.1.0 setup .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It preserves existing Yoke configuration and asks for target agents, code graph, loop state, default runner, and decision mode. A non-interactive agent can apply explicit choices with `--yes`.
|
|
12
|
+
|
|
13
|
+
The new config fields are optional and backward-compatible:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
runner:
|
|
17
|
+
agent: codex
|
|
18
|
+
permissions: safe
|
|
19
|
+
loop:
|
|
20
|
+
enabled: true
|
|
21
|
+
timeoutMinutes: 30
|
|
22
|
+
decisionPolicy: critical # auto | critical
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`loop.onAmbiguity: resolve|abort` and `--on-ambiguity=` still work. Prefer `decisionPolicy` for new projects: `auto` resolves routine choices without asking; `critical` pauses only for high-impact decisions and continues after `yoke loop answer`. Resume restores the original isolation, review, runner, permissions, timeout, and policy flags instead of silently weakening the run. If the restart cannot begin, `yoke loop resume` retries from the request-bound state stored under Git's private state directory.
|
|
26
|
+
|
|
27
|
+
Restart an already-open Codex task after retrofit if it does not discover the newly generated `.agents/skills/yoke-workflow/SKILL.md`.
|