@hecer/yoke 1.0.0 → 1.1.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/dist/loop/loop.js CHANGED
@@ -4,6 +4,7 @@ import { loadPrd, savePrd, selectNextStory, allPass, progress } from './prd.js';
4
4
  import { stopTheLineGate, preDispatchGate } from './gates.js';
5
5
  import { appendDecision, contextDir } from '../context/context.js';
6
6
  import { noopReporter } from './reporter.js';
7
+ import { consumeDecisionRequest } from './decision.js';
7
8
  function blockReason(base, targetDir, git) {
8
9
  let dirty = false;
9
10
  try {
@@ -112,6 +113,20 @@ export function runLoop(opts) {
112
113
  iterations++;
113
114
  if (result.tokens)
114
115
  reporter.addTokens(result.tokens);
116
+ let decision;
117
+ try {
118
+ decision = consumeDecisionRequest(wt, opts.targetDir, story.id);
119
+ }
120
+ catch (error) {
121
+ const reason = `invalid critical decision request for story ${story.id}: ${error.message}`;
122
+ reporter.blocked(reason);
123
+ return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
124
+ }
125
+ if (decision) {
126
+ const reason = `critical decision required for story ${story.id}: ${decision.question}`;
127
+ reporter.blocked(reason);
128
+ return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
129
+ }
115
130
  const ambiguity = consumeAmbiguity(wt);
116
131
  if (ambiguity) {
117
132
  const reason = `story ${story.id} stopped: ambiguous acceptance criteria — ${ambiguity}`;
@@ -194,6 +209,20 @@ export function runLoop(opts) {
194
209
  iterations++;
195
210
  if (result.tokens)
196
211
  reporter.addTokens(result.tokens);
212
+ let decision;
213
+ try {
214
+ decision = consumeDecisionRequest(opts.targetDir, opts.targetDir, story.id);
215
+ }
216
+ catch (error) {
217
+ const reason = `invalid critical decision request for story ${story.id}: ${error.message}`;
218
+ reporter.blocked(reason);
219
+ return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
220
+ }
221
+ if (decision) {
222
+ const reason = `critical decision required for story ${story.id}: ${decision.question}`;
223
+ reporter.blocked(reason);
224
+ return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
225
+ }
197
226
  const ambiguity = consumeAmbiguity(opts.targetDir);
198
227
  if (ambiguity) {
199
228
  const reason = `story ${story.id} stopped: ambiguous acceptance criteria — ${ambiguity}`;
@@ -11,6 +11,8 @@ import { acquireLock, releaseLock } from './lock.js';
11
11
  import { maybeAutoUpgrade } from '../update/upgrade.js';
12
12
  import { resolveCommitIdentity } from './identity.js';
13
13
  import { runAudit } from '../audit/command.js';
14
+ import { detectHostAgent, resolveRunnerAgent } from '../agents/host.js';
15
+ import { clearDecisionResume, decisionProcessingExists, decisionRequestId, formatPendingDecision, readPendingDecision, writeDecisionResume, } from './decision.js';
14
16
  export const DEFAULT_IDLE_MINUTES = 20;
15
17
  const STALE_MINUTES = 20; // a running status older than this likely means the loop died
16
18
  export function relativeTime(fromIso, now) {
@@ -32,7 +34,7 @@ export function prdPath(targetDir) {
32
34
  export function setLoopEnabled(targetDir, enabled) {
33
35
  // TODO(C2): resolve bundled canon version instead of placeholder
34
36
  const config = loadConfig(targetDir) ?? defaultConfig('0.0.0');
35
- config.loop = { enabled };
37
+ config.loop = { ...config.loop, enabled };
36
38
  saveConfig(targetDir, config);
37
39
  }
38
40
  export function loopStatus(targetDir, now = () => new Date()) {
@@ -77,6 +79,20 @@ export function runLoopCommand(targetDir, opts) {
77
79
  console.error('Loop is disabled. Enable it with: yoke loop on');
78
80
  return 2;
79
81
  }
82
+ if (decisionProcessingExists(targetDir)) {
83
+ console.error(`A critical decision answer needs recovery. Run: yoke loop answer ${targetDir} --choice=<id>`);
84
+ return 1;
85
+ }
86
+ try {
87
+ if (readPendingDecision(targetDir)) {
88
+ console.error(`${formatPendingDecision(targetDir)}\nAnswer it with: yoke loop answer ${targetDir} --choice=<id>`);
89
+ return 1;
90
+ }
91
+ }
92
+ catch (error) {
93
+ console.error(`Invalid pending Yoke decision: ${error.message}`);
94
+ return 1;
95
+ }
80
96
  const path = prdPath(targetDir);
81
97
  if (!existsSync(path)) {
82
98
  console.error(`No PRD found at ${path}. Create one (see canon loop/prd.schema.md).`);
@@ -101,7 +117,7 @@ export function runLoopCommand(targetDir, opts) {
101
117
  // it started with; a fetched upgrade applies from the next invocation.
102
118
  maybeAutoUpgrade(config.update?.auto);
103
119
  const available = opts.isAvailable ?? isAgentAvailable;
104
- const runnerAgent = opts.agent ?? config.agents[0] ?? 'claude';
120
+ const runnerAgent = resolveRunnerAgent(config, opts.agent, detectHostAgent());
105
121
  const git = opts.git ?? realGitOps;
106
122
  let commitIdentity = opts.commitIdentity;
107
123
  if (!commitIdentity && !opts.git) {
@@ -136,7 +152,7 @@ export function runLoopCommand(targetDir, opts) {
136
152
  // runner switches to stream-json so cumulative usage rides on every status.
137
153
  runner = makeRunner(runnerAgent, idleMs, {
138
154
  tokenReport: opts.json === true,
139
- onAmbiguity: opts.onAmbiguity ?? config.loop.onAmbiguity,
155
+ onAmbiguity: opts.decisionPolicy ?? opts.onAmbiguity ?? config.loop.decisionPolicy ?? config.loop.onAmbiguity ?? 'auto',
140
156
  perfCommand: config.perf?.command,
141
157
  permissions,
142
158
  });
@@ -163,7 +179,14 @@ export function runLoopCommand(targetDir, opts) {
163
179
  }
164
180
  review = makeReviewRunner(resolvedReviewer, idleMs);
165
181
  }
166
- const lock = acquireLock(targetDir);
182
+ let lock;
183
+ try {
184
+ lock = acquireLock(targetDir);
185
+ }
186
+ catch (error) {
187
+ console.error(`Cannot acquire the Yoke loop lock: ${error.message}`);
188
+ return 2;
189
+ }
167
190
  if (!lock.acquired) {
168
191
  console.error(`Another loop is already running here (pid ${lock.holderPid}). If that is wrong, run: yoke loop cleanup`);
169
192
  return 2;
@@ -172,6 +195,7 @@ export function runLoopCommand(targetDir, opts) {
172
195
  console.warn(`Took over a stale loop lock (pid ${lock.stalePid} is gone).`);
173
196
  }
174
197
  try {
198
+ const reporter = opts.reporter ?? makeReporter(targetDir, { json: opts.json });
175
199
  const result = runLoop({
176
200
  prdPath: path,
177
201
  targetDir,
@@ -184,15 +208,52 @@ export function runLoopCommand(targetDir, opts) {
184
208
  maxIterations: opts.maxIterations,
185
209
  isolate: (opts.parallel ?? 1) > 1 ? true : (opts.isolate ?? false),
186
210
  review,
187
- reporter: opts.reporter ?? makeReporter(targetDir, { json: opts.json }),
211
+ reporter,
188
212
  });
213
+ try {
214
+ const pendingDecision = readPendingDecision(targetDir);
215
+ if (pendingDecision) {
216
+ writeDecisionResume(targetDir, {
217
+ version: 1,
218
+ storyId: pendingDecision.storyId,
219
+ requestId: decisionRequestId(pendingDecision),
220
+ answered: false,
221
+ maxIterations: opts.maxIterations,
222
+ agent: runnerAgent,
223
+ isolate: opts.isolate ?? false,
224
+ reviewer: opts.reviewer,
225
+ review: opts.review === true || opts.reviewRunner !== undefined,
226
+ allowSelfReview: opts.allowSelfReview ?? false,
227
+ timeoutMinutes: opts.timeoutMinutes ?? config.loop.timeoutMinutes,
228
+ json: opts.json ?? false,
229
+ onAmbiguity: opts.decisionPolicy
230
+ ? undefined
231
+ : opts.onAmbiguity === 'resolve' || opts.onAmbiguity === 'abort'
232
+ ? opts.onAmbiguity
233
+ : (config.loop.decisionPolicy ? undefined : config.loop.onAmbiguity),
234
+ decisionPolicy: opts.decisionPolicy
235
+ ?? (opts.onAmbiguity
236
+ ? (opts.onAmbiguity === 'auto' || opts.onAmbiguity === 'critical' ? opts.onAmbiguity : undefined)
237
+ : config.loop.decisionPolicy),
238
+ permissions,
239
+ parallel: opts.parallel ?? 1,
240
+ });
241
+ }
242
+ else
243
+ clearDecisionResume(targetDir);
244
+ }
245
+ catch (error) {
246
+ const reason = `could not persist trusted decision resume state: ${error.message}`;
247
+ reporter.blocked(reason);
248
+ return 1;
249
+ }
189
250
  // In json mode stdout belongs to the NDJSON stream — route the narrative summary to stderr.
190
251
  const say = opts.json ? (line) => console.error(line) : (line) => console.log(line);
191
252
  say(`Loop ${result.status} after ${result.iterations} iteration(s): ${result.finalProgress.passed}/${result.finalProgress.total} stories pass`);
192
253
  if (result.reason)
193
254
  say(`Reason: ${result.reason}`);
194
- if (result.reason && /api key|please run \/login|not logged in/i.test(result.reason)) {
195
- say('Hint: the agent CLI has no credentials in this environment. Set ANTHROPIC_API_KEY or log the agent in for headless use.');
255
+ if (result.reason && /api key|please run \/login|not logged in|auth/i.test(result.reason)) {
256
+ say('Hint: the agent CLI has no credentials in this environment. Set ANTHROPIC_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY, or log the agent in for headless use.');
196
257
  }
197
258
  // Exit codes: 0 complete · 1 blocked/cap-reached · 2 config error (handled above) · 3 paused (loop.pause consumed at a story boundary)
198
259
  if (result.status === 'complete')
@@ -202,6 +263,6 @@ export function runLoopCommand(targetDir, opts) {
202
263
  return 1;
203
264
  }
204
265
  finally {
205
- releaseLock(targetDir);
266
+ releaseLock(targetDir, lock.ownerToken);
206
267
  }
207
268
  }
@@ -19,7 +19,13 @@ export function buildClaudePrompt(story, context, onAmbiguity = 'resolve', perfC
19
19
  lines.push('', context);
20
20
  lines.push('', `Story ${story.id}: ${story.title}`, 'Acceptance criteria (Definition of Done):', criteria, '', "When done, ensure the project's full test suite passes.", 'Do NOT commit — the loop commits on your behalf after verifying.', '', 'Working rules:', '- Add nothing beyond what the story requires: no extra features, abstractions, comments, or defensive code for cases that cannot happen.', '- Do not create summary, plan, or analysis documents — only files the story itself needs.', '- If a check fails, fix the root cause; never bypass it (e.g. --no-verify) or pass by weakening tests.', '- Report the outcome faithfully: if a criterion is unmet or tests fail, say so plainly instead of claiming success.', '- Never ask questions or wait for input — you run unattended and nobody can answer.', onAmbiguity === 'abort'
21
21
  ? '- If an acceptance criterion is genuinely undecidable, do NOT guess: write the open question(s) to .yoke/ambiguity.md, change nothing else, and stop.'
22
- : '- If an acceptance criterion is ambiguous, resolve it yourself in the way most consistent with the other criteria and the existing code, and state your interpretation in your final message.');
22
+ : onAmbiguity === 'critical'
23
+ ? [
24
+ '- Resolve routine ambiguity yourself using the plan, acceptance criteria, existing code, and established project conventions.',
25
+ '- Stop only for a high-impact decision involving public architecture, security or privacy posture, destructive data migration or data loss, material external cost, legal/compliance exposure, or another irreversible choice.',
26
+ '- For such a critical decision, change nothing else. Write .yoke/decision-request.yaml with exactly: version: 1, storyId, question, reason, 2-4 options ({id, label, optional tradeoff}), and recommended (an option id). Then stop.',
27
+ ].join('\n')
28
+ : '- If an acceptance criterion is ambiguous, resolve it yourself in the way most consistent with the other criteria and the existing code, and state your interpretation in your final message.');
23
29
  if (perfCommand) {
24
30
  lines.push(`- This project enforces a performance budget: \`${perfCommand}\` must exit 0 or the story is blocked. Keep hot paths efficient, and never simplify away an existing optimization without re-running that benchmark.`);
25
31
  }
@@ -1,9 +1,10 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { loadConfig } from '../retrofit/config.js';
4
4
  import { loadPrd, progress } from '../loop/prd.js';
5
5
  import { agentInvocation, buildWatchdogInvocation, runAgent, isAgentAvailable, } from '../loop/runner.js';
6
6
  import { resolveIdleMs } from '../loop/run-command.js';
7
+ import { detectHostAgent, resolveRunnerAgent } from '../agents/host.js';
7
8
  export const PRD_TEMPLATE = `# Yoke PRD — the loop picks the lowest-priority open story each iteration.
8
9
  # Story format (see canon/loop/prd.schema.md):
9
10
  # - id: STORY-1
@@ -18,30 +19,19 @@ export const PRD_TEMPLATE = `# Yoke PRD — the loop picks the lowest-priority o
18
19
  # passes: false
19
20
  []
20
21
  `;
21
- export function buildPrdDraftPrompt(idea) {
22
- return [
22
+ export const MAX_PLANNING_BRIEF_CHARS = 20_000;
23
+ export const MAX_PLANNING_BRIEF_BYTES = MAX_PLANNING_BRIEF_CHARS * 4;
24
+ export function buildPrdDraftPrompt(idea, planningBrief) {
25
+ const lines = [
23
26
  'You are drafting a PRD for the Yoke autonomous loop.',
24
27
  '',
25
28
  `Product idea: ${idea}`,
26
- '',
27
- 'Break the idea into 5-12 small, independently shippable stories; each must fit one loop iteration.',
28
- 'Each story needs:',
29
- '- id: STORY-1, STORY-2, ... (unique)',
30
- '- title: one imperative sentence',
31
- '- priority: dense integers from 1 (lower = built first)',
32
- '- needs: optional list of story IDs that must pass first; the graph must be acyclic',
33
- '- area: optional collision domain for safe parallel scheduling',
34
- '- agent: optional claude|codex|gemini affinity',
35
- '- acceptance: 2-5 testable, behavioral criteria (observable outcomes, never implementation steps)',
36
- '- passes: false',
37
- '',
38
- 'If the project has no source code yet, STORY-1 must scaffold the project skeleton with a runnable',
39
- 'test suite, and its acceptance must include that the verify command (verify.command in',
40
- '.yoke/config.yaml) exits 0.',
41
- '',
42
- 'Write ONLY the file .yoke/prd.yaml as a YAML array of stories in exactly that shape.',
43
- 'Do not modify any other file. Do not commit.',
44
- ].join('\n');
29
+ ];
30
+ if (planningBrief?.trim()) {
31
+ lines.push('', '## Approved planning brief (treat these decisions as settled)', planningBrief.trim(), '', 'Do not reopen settled choices or invent alternatives that contradict this brief.');
32
+ }
33
+ lines.push('', 'Break the idea into 5-12 small, independently shippable stories; each must fit one loop iteration.', 'Each story needs:', '- id: STORY-1, STORY-2, ... (unique)', '- title: one imperative sentence', '- priority: dense integers from 1 (lower = built first)', '- needs: optional list of story IDs that must pass first; the graph must be acyclic', '- area: optional collision domain for safe parallel scheduling', '- agent: optional claude|codex|gemini affinity', '- acceptance: 2-5 testable, behavioral criteria (observable outcomes, never implementation steps)', '- passes: false', '', 'If the project has no source code yet, STORY-1 must scaffold the project skeleton with a runnable', 'test suite, and its acceptance must include that the verify command (verify.command in', '.yoke/config.yaml) exits 0.', '', 'Write ONLY the file .yoke/prd.yaml as a YAML array of stories in exactly that shape.', 'Do not modify any other file. Do not commit.');
34
+ return lines.join('\n');
45
35
  }
46
36
  export function prdFile(targetDir) {
47
37
  return join(targetDir, '.yoke', 'prd.yaml');
@@ -69,13 +59,23 @@ export function runPrdDraft(targetDir, opts) {
69
59
  }
70
60
  const available = opts.isAvailable ?? isAgentAvailable;
71
61
  const config = loadConfig(targetDir);
72
- const agent = opts.runner ?? config?.agents[0] ?? 'claude';
62
+ const agent = resolveRunnerAgent(config, opts.runner, detectHostAgent());
73
63
  if (!available(agent)) {
74
64
  console.error(`Agent CLI "${agent}" was not found on PATH. Install it, or pick another with --runner=<claude|codex|gemini>.`);
75
65
  return 2;
76
66
  }
77
67
  const idleMs = resolveIdleMs(opts.timeoutMinutes, undefined);
78
- const inv = agentInvocation(agent, buildPrdDraftPrompt(idea), targetDir);
68
+ const planPath = join(targetDir, '.yoke', 'plan.md');
69
+ if (existsSync(planPath) && statSync(planPath).size > MAX_PLANNING_BRIEF_BYTES) {
70
+ console.error(`Approved plan is too large (${statSync(planPath).size} bytes; maximum ${MAX_PLANNING_BRIEF_BYTES}). Split or condense .yoke/plan.md before drafting the PRD.`);
71
+ return 1;
72
+ }
73
+ const planningBrief = existsSync(planPath) ? readFileSync(planPath, 'utf8') : undefined;
74
+ if (planningBrief && planningBrief.length > MAX_PLANNING_BRIEF_CHARS) {
75
+ console.error(`Approved plan is too large (${planningBrief.length} characters; maximum ${MAX_PLANNING_BRIEF_CHARS}). Split or condense .yoke/plan.md before drafting the PRD.`);
76
+ return 1;
77
+ }
78
+ const inv = agentInvocation(agent, buildPrdDraftPrompt(idea, planningBrief), targetDir);
79
79
  console.log(`Drafting PRD with ${agent}...`);
80
80
  const run = opts.run ?? ((i) => runAgent(buildWatchdogInvocation(i, idleMs)));
81
81
  const result = run(inv);
@@ -123,6 +123,9 @@ export function runPrdCheck(targetDir) {
123
123
  // the schema allows [], but the loop's stop-the-line gate blocks it — fail fast here
124
124
  if (s.acceptance.length === 0)
125
125
  errors.push(`story ${s.id} has no acceptance criteria`);
126
+ if (s.acceptance.some(criterion => /\b(?:TBD|TODO|TO BE DECIDED|DECIDE LATER)\b|\?\?\?/i.test(criterion))) {
127
+ errors.push(`story ${s.id} has unresolved planning decisions in acceptance criteria`);
128
+ }
126
129
  }
127
130
  if (errors.length > 0) {
128
131
  for (const e of errors)
@@ -7,13 +7,14 @@ import { detectProject } from './detect.js';
7
7
  import { ensureGitignore } from './gitignore.js';
8
8
  import { loadConfig, saveConfig, defaultConfig } from './config.js';
9
9
  import { loadManifest } from '../canon/manifest.js';
10
+ import { detectHostAgent } from '../agents/host.js';
10
11
  export function runRetrofit(targetDir, opts) {
11
12
  const canonDir = resolveCanonDir();
12
13
  const canonVersion = loadManifest(join(canonDir, 'manifest.yaml')).version;
13
14
  const detection = detectProject(targetDir);
14
15
  const agents = opts.agents && opts.agents.length > 0
15
16
  ? opts.agents
16
- : (detection.agents.length > 0 ? detection.agents : ['claude']);
17
+ : (detection.agents.length > 0 ? detection.agents : [opts.host ?? detectHostAgent() ?? 'claude']);
17
18
  const existing = loadConfig(targetDir);
18
19
  const codeGraph = opts.codeGraph ?? existing?.codeGraph ?? 'graphify';
19
20
  const actions = planRetrofit(canonDir, targetDir, agents, codeGraph);
@@ -28,7 +29,7 @@ export function runRetrofit(targetDir, opts) {
28
29
  ...(existing ?? defaultConfig(canonVersion)),
29
30
  canonVersion,
30
31
  agents: mergedAgents,
31
- loop: { enabled: opts.loop },
32
+ loop: { ...existing?.loop, enabled: opts.loop },
32
33
  codeGraph,
33
34
  };
34
35
  saveConfig(targetDir, config);
@@ -12,11 +12,15 @@ export const YokeConfigSchema = z.object({
12
12
  loop: z.object({
13
13
  enabled: z.boolean(),
14
14
  timeoutMinutes: z.number().optional(),
15
+ decisionPolicy: z.enum(['auto', 'critical']).optional(),
15
16
  // Ambiguous acceptance criteria: 'resolve' (default — agent decides and continues)
16
17
  // or 'abort' (agent stops the story via .yoke/ambiguity.md for a human decision).
17
18
  onAmbiguity: z.enum(['resolve', 'abort']).optional(),
18
19
  }),
19
- runner: z.object({ permissions: z.enum(['safe', 'unsafe', 'read-only']) }).optional(),
20
+ runner: z.object({
21
+ agent: AgentSchema.optional(),
22
+ permissions: z.enum(['safe', 'unsafe', 'read-only']).optional(),
23
+ }).optional(),
20
24
  commit: z.object({
21
25
  authorName: z.string().min(1).optional(),
22
26
  authorEmail: z.string().email().optional(),
@@ -6,9 +6,17 @@ export const YOKE_IGNORE_LINES = [
6
6
  '.yoke/loop-status.json',
7
7
  '.yoke/loop.log',
8
8
  '.yoke/loop.lock',
9
+ '.yoke/loop.lock.takeover',
10
+ '.yoke/loop.lock.takeover.recovery',
11
+ '.yoke/loop.lock.*.tmp',
9
12
  '.yoke/loop.pause',
10
13
  '.yoke/runner.pid',
11
14
  '.yoke/ambiguity.md',
15
+ '.yoke/decision-request.yaml',
16
+ '.yoke/pending-decision.yaml',
17
+ '.yoke/decision-answering.yaml',
18
+ '.yoke/decision-resume*.yaml',
19
+ '.yoke/decision-*.yaml.*.tmp',
12
20
  '.yoke/story-durations.json',
13
21
  '.yoke/proof/',
14
22
  ];
@@ -1,7 +1,7 @@
1
1
  export function formatReport(applied, meta) {
2
2
  const count = (s) => applied.filter(a => a.status === s).length;
3
3
  const lines = [];
4
- lines.push('Yoke retrofit (Claude Code):');
4
+ lines.push('Yoke retrofit:');
5
5
  for (const a of applied) {
6
6
  const note = a.backedUp ? ` (backup: ${a.backedUp})` : '';
7
7
  lines.push(` ${a.status.padEnd(11)} ${a.target}${note}`);
@@ -0,0 +1,82 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+ import { detectHostAgent } from '../agents/host.js';
4
+ import { loadConfig, saveConfig } from '../retrofit/config.js';
5
+ import { detectProject } from '../retrofit/detect.js';
6
+ import { runRetrofit } from '../retrofit/command.js';
7
+ const ALL_AGENTS = ['claude', 'codex', 'gemini'];
8
+ function parseAgents(value, fallback) {
9
+ if (value.trim().toLowerCase() === 'all')
10
+ return [...ALL_AGENTS];
11
+ const parsed = value.split(',').map(v => v.trim().toLowerCase()).filter((v) => ALL_AGENTS.includes(v));
12
+ return parsed.length > 0 ? [...new Set(parsed)] : fallback;
13
+ }
14
+ function yes(value, fallback) {
15
+ const normalized = value.trim().toLowerCase();
16
+ if (['y', 'yes', 'j', 'ja', 'true', '1'].includes(normalized))
17
+ return true;
18
+ if (['n', 'no', 'nein', 'false', '0'].includes(normalized))
19
+ return false;
20
+ return fallback;
21
+ }
22
+ export async function runSetup(targetDir, opts = {}) {
23
+ const existing = loadConfig(targetDir);
24
+ const detected = detectProject(targetDir);
25
+ const host = opts.host ?? detectHostAgent();
26
+ const configuredAgents = existing?.agents.filter(a => ALL_AGENTS.includes(a)) ?? [];
27
+ const defaultAgents = opts.agents && opts.agents.length > 0
28
+ ? opts.agents
29
+ : configuredAgents.length > 0
30
+ ? configuredAgents
31
+ : detected.agents.length > 0
32
+ ? detected.agents
33
+ : [host ?? 'claude'];
34
+ const defaultGraph = opts.codeGraph ?? existing?.codeGraph ?? 'graphify';
35
+ const defaultLoop = opts.loop ?? existing?.loop.enabled ?? true;
36
+ const defaultRunner = opts.runner ?? existing?.runner?.agent ?? (host && defaultAgents.includes(host) ? host : defaultAgents[0] ?? host ?? 'claude');
37
+ const defaultPolicy = opts.decisionPolicy ?? existing?.loop.decisionPolicy ?? (existing?.loop.onAmbiguity === 'abort' ? 'critical' : 'auto');
38
+ const interactive = opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
39
+ let close;
40
+ let ask = opts.ask;
41
+ if (interactive && !ask) {
42
+ const rl = createInterface({ input, output });
43
+ ask = (question) => rl.question(question);
44
+ close = () => rl.close();
45
+ }
46
+ try {
47
+ let agents = defaultAgents;
48
+ let codeGraph = defaultGraph;
49
+ let loop = defaultLoop;
50
+ let runner = defaultRunner;
51
+ let decisionPolicy = defaultPolicy;
52
+ if (interactive && ask) {
53
+ agents = parseAgents(await ask(`Agents [${defaultAgents.join(',')}] (claude,codex,gemini|all): `), defaultAgents);
54
+ const graphAnswer = (await ask(`Code graph [${defaultGraph}] (graphify|serena): `)).trim().toLowerCase();
55
+ if (graphAnswer === 'graphify' || graphAnswer === 'serena')
56
+ codeGraph = graphAnswer;
57
+ loop = yes(await ask(`Enable autonomous loop? [${defaultLoop ? 'yes' : 'no'}]: `), defaultLoop);
58
+ const runnerAnswer = (await ask(`Default runner [${runner}] (claude|codex|gemini): `)).trim().toLowerCase();
59
+ if (ALL_AGENTS.includes(runnerAnswer))
60
+ runner = runnerAnswer;
61
+ const policyAnswer = (await ask(`Decision mode [${decisionPolicy}] (auto|critical): `)).trim().toLowerCase();
62
+ if (policyAnswer === 'auto' || policyAnswer === 'critical')
63
+ decisionPolicy = policyAnswer;
64
+ }
65
+ if (!agents.includes(runner))
66
+ agents = [...agents, runner];
67
+ const code = runRetrofit(targetDir, { loop, agents, codeGraph, host });
68
+ if (code !== 0)
69
+ return code;
70
+ const config = loadConfig(targetDir);
71
+ if (!config)
72
+ return 1;
73
+ config.loop = { ...config.loop, enabled: loop, decisionPolicy };
74
+ config.runner = { ...config.runner, agent: runner };
75
+ saveConfig(targetDir, config);
76
+ console.log(`Yoke setup complete: agents=${agents.join(',')} · runner=${runner} · loop=${loop ? 'on' : 'off'} · decisions=${decisionPolicy}`);
77
+ return 0;
78
+ }
79
+ finally {
80
+ close?.();
81
+ }
82
+ }
@@ -0,0 +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,41 +1,77 @@
1
- # Publishing channels — status & playbook
2
-
3
- Where Yoke is published, and how each channel gets updated. (Researched 2026-07-10.)
4
-
5
- ## Live
6
-
7
- | Channel | How | Update path |
8
- |---|---|---|
9
- | **npm** — [`@hecer/yoke`](https://www.npmjs.com/package/@hecer/yoke) | `npm publish` (2FA) | every release |
10
- | **GitHub** — [HECer/yoke](https://github.com/HECer/yoke) | push + tag | every release |
11
- | **Claude Code plugin (self-marketplace)** | `.claude-plugin/plugin.json` + `marketplace.json` in this repo; users: `/plugin marketplace add HECer/yoke` → `/plugin install yoke@yoke` | bump `version` in `plugin.json` |
12
- | **Gemini CLI extension** | `gemini-extension.json` + `GEMINI-EXTENSION.md` at repo root; users: `gemini extensions install https://github.com/HECer/yoke` | bump `version` in the manifest |
13
-
14
- ## Submitted / pending
15
-
16
- | Channel | How | Status |
17
- |---|---|---|
18
- | **Gemini extensions gallery** (geminicli.com/extensions) | automatic daily crawl: needs `gemini-extension.json` at repo root + `gemini-cli-extension` repo topic — both done | wait for crawler |
19
- | **Anthropic community plugin directory** (`claude-community`, surfaced in `/plugin > Discover`) | form at **platform.claude.com/plugins/submit** (Console account, Developer role; submit the public repo URL; `claude plugin validate` runs in their pipeline — passes locally). After approval: pinned to a commit SHA, CI auto-bumps on push, catalog syncs nightly | **needs a human login** — see below |
20
-
21
- ### Anthropic directory submission (manual step)
22
-
23
- 1. Log in at https://platform.claude.com (free Console account is enough; role Developer+).
24
- 2. Open https://platform.claude.com/plugins/submit
25
- 3. Submit the public repo: `https://github.com/HECer/yoke`
26
- 4. Suggested description: *"Cross-agent coding harness: one curated skill canon (TDD,
27
- brainstorming → spec → plan, systematic debugging, cross-model review, design
28
- verification) plus mechanical safety gates and an autonomous loop via the yoke CLI."*
29
- 5. Category: development. Plugin name (immutable): `yoke`.
30
-
31
- ## Worth doing later (community lists, PR/issue-based)
32
-
33
- - **awesome-claude-code** (hesreallyhim) — issue-form only, explicitly human-submitted, no PRs.
34
- - **ComposioHQ/awesome-claude-plugins** — PR per template (high merge latency).
35
- - **davila7/claude-code-templates** (aitmpl.com) — PR per CONTRIBUTING.md.
36
- - **Codex plugin directory** (platform.openai.com/plugins) — requires verified developer
37
- identity + test cases; medium-high effort. Codex CLI users can already consume the repo
38
- marketplace directly.
39
- - Auto-crawled directories (crossaitools.com etc.) pick the repo up on their own once the
40
- marketplace manifest exists.
41
- - Launch channels (Product Hunt, Show HN, r/ClaudeAI, r/ClaudeCode) — deliberate, human-led.
1
+ # Publishing channels — status & playbook
2
+
3
+ Where Yoke is published, and how each channel gets updated. (Reviewed 2026-07-30.)
4
+
5
+ ## Live
6
+
7
+ | Channel | How | Update path |
8
+ |---|---|---|
9
+ | **npm** — [`@hecer/yoke`](https://www.npmjs.com/package/@hecer/yoke) | `npm publish` (2FA) | every release |
10
+ | **GitHub** — [HECer/yoke](https://github.com/HECer/yoke) | push + tag + GitHub Release | every release |
11
+ | **Claude Code plugin (self-marketplace)** | `.claude-plugin/plugin.json` + `marketplace.json` in this repo; users: `/plugin marketplace add HECer/yoke` → `/plugin install yoke@yoke` | bump `version` in `plugin.json` |
12
+ | **Gemini CLI extension** | `gemini-extension.json` + `GEMINI-EXTENSION.md` at repo root; users: `gemini extensions install https://github.com/HECer/yoke` | bump `version` in the manifest |
13
+ | **Codex project skills** | `npx @hecer/yoke setup .` writes the canon to `.agents/skills/` plus native Codex config/hooks; `.codex-plugin/plugin.json` is bundled for plugin-capable hosts | every npm release |
14
+
15
+ ## GitHub release (required, not just a tag)
16
+
17
+ A pushed tag appears under **Tags**, but GitHub only shows an entry under **Releases** after a
18
+ release object is created. Use this idempotent check after the version commit reaches `main`:
19
+
20
+ ```bash
21
+ set -euo pipefail
22
+ VERSION=1.1.0
23
+ TARGET=$(git rev-parse HEAD)
24
+ git fetch --tags origin
25
+
26
+ REMOTE=$(git ls-remote origin "refs/tags/v$VERSION^{}" | awk 'NR == 1 { print $1 }')
27
+ if [ -z "$REMOTE" ]; then
28
+ REMOTE=$(git ls-remote origin "refs/tags/v$VERSION" | awk 'NR == 1 { print $1 }')
29
+ fi
30
+ if [ -n "$REMOTE" ] && [ "$REMOTE" != "$TARGET" ]; then
31
+ echo "origin/v$VERSION already points at a different commit" >&2
32
+ exit 1
33
+ fi
34
+
35
+ if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
36
+ test "$(git rev-list -n 1 "v$VERSION")" = "$TARGET" || {
37
+ echo "v$VERSION already points at a different commit" >&2
38
+ exit 1
39
+ }
40
+ else
41
+ git tag "v$VERSION"
42
+ fi
43
+ git push origin "refs/tags/v$VERSION"
44
+ gh release view "v$VERSION" >/dev/null 2>&1 || \
45
+ gh release create "v$VERSION" --title "Yoke $VERSION" --generate-notes
46
+ ```
47
+
48
+ Verify both surfaces before publishing npm: `gh release view "v$VERSION"` and
49
+ `npm view @hecer/yoke version`.
50
+
51
+ ## Submitted / pending
52
+
53
+ | Channel | How | Status |
54
+ |---|---|---|
55
+ | **Gemini extensions gallery** (geminicli.com/extensions) | automatic daily crawl: needs `gemini-extension.json` at repo root + `gemini-cli-extension` repo topic — both done | wait for crawler |
56
+ | **Anthropic community plugin directory** (`claude-community`, surfaced in `/plugin > Discover`) | form at **platform.claude.com/plugins/submit** (Console account, Developer role; submit the public repo URL; `claude plugin validate` runs in their pipeline — passes locally). After approval: pinned to a commit SHA, CI auto-bumps on push, catalog syncs nightly | **needs a human login** — see below |
57
+
58
+ ### Anthropic directory submission (manual step)
59
+
60
+ 1. Log in at https://platform.claude.com (free Console account is enough; role Developer+).
61
+ 2. Open https://platform.claude.com/plugins/submit
62
+ 3. Submit the public repo: `https://github.com/HECer/yoke`
63
+ 4. Suggested description: *"Cross-agent coding harness: one curated skill canon (TDD,
64
+ brainstorming → spec → plan, systematic debugging, cross-model review, design
65
+ verification) plus mechanical safety gates and an autonomous loop via the yoke CLI."*
66
+ 5. Category: development. Plugin name (immutable): `yoke`.
67
+
68
+ ## Worth doing later (community lists, PR/issue-based)
69
+
70
+ - **awesome-claude-code** (hesreallyhim) — issue-form only, explicitly human-submitted, no PRs.
71
+ - **ComposioHQ/awesome-claude-plugins** — PR per template (high merge latency).
72
+ - **davila7/claude-code-templates** (aitmpl.com) — PR per CONTRIBUTING.md.
73
+ - **Codex plugin directory** — public directory submission remains a separate channel. Codex
74
+ users do not need it: the npm setup path installs native project skills deterministically.
75
+ - Auto-crawled directories (crossaitools.com etc.) pick the repo up on their own once the
76
+ marketplace manifest exists.
77
+ - Launch channels (Product Hunt, Show HN, r/ClaudeAI, r/ClaudeCode) — deliberate, human-led.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "yoke",
3
+ "version": "1.1.0",
4
+ "description": "Cross-agent coding harness: curated skill canon, mechanical safety gates, autonomous loop with proof artifacts. CLI: npm i -g @hecer/yoke",
5
+ "contextFileName": "GEMINI-EXTENSION.md"
6
+ }
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@hecer/yoke",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "One harness, three agents, zero trust in \"done\" — cross-agent coding harness for Claude Code, Codex CLI, and Gemini CLI: one skill canon, mechanical safety gates, an autonomous loop with screenshot/video proofs.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "yoke": "./dist/cli.js"
7
+ "yoke": "dist/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
11
  "canon",
12
+ ".claude-plugin",
12
13
  ".codex-plugin",
14
+ "gemini-extension.json",
13
15
  "agents",
14
16
  "hooks",
15
17
  "bench/README.md",