@chrono-meta/fh-gate 1.4.52 → 1.4.54

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.
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ function usage() {
8
+ process.stdout.write(`Usage:
9
+ fh-codex-doctor [--root <path>] [--json] [--strict]
10
+
11
+ Scans FH skills and agents for Codex runtime compatibility drift.
12
+
13
+ Options:
14
+ --root <path> Repository/package root. Defaults to cwd when it looks like FH,
15
+ otherwise this package root.
16
+ --json Emit machine-readable JSON.
17
+ --strict Exit 1 when high-severity drift is found.
18
+ --help Show this help.
19
+ `);
20
+ }
21
+
22
+ function parseArgs(argv) {
23
+ const out = { root: defaultRoot(), json: false, strict: false };
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const arg = argv[i];
26
+ if (arg === '--help' || arg === '-h') {
27
+ out.help = true;
28
+ } else if (arg === '--json') {
29
+ out.json = true;
30
+ } else if (arg === '--strict') {
31
+ out.strict = true;
32
+ } else if (arg === '--root') {
33
+ i += 1;
34
+ if (!argv[i]) throw new Error('--root requires a path');
35
+ out.root = path.resolve(argv[i]);
36
+ } else {
37
+ throw new Error(`unknown argument: ${arg}`);
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function looksLikeFHRoot(dir) {
44
+ return exists(path.join(dir, 'package.json')) &&
45
+ exists(path.join(dir, 'plugins')) &&
46
+ exists(path.join(dir, 'AGENTS.md'));
47
+ }
48
+
49
+ function defaultRoot() {
50
+ const cwd = process.cwd();
51
+ if (looksLikeFHRoot(cwd)) return cwd;
52
+ return path.resolve(__dirname, '..');
53
+ }
54
+
55
+ function readText(file) {
56
+ return fs.readFileSync(file, 'utf8');
57
+ }
58
+
59
+ function maybeReadText(file) {
60
+ try {
61
+ return readText(file);
62
+ } catch (_err) {
63
+ return '';
64
+ }
65
+ }
66
+
67
+ function exists(file) {
68
+ try {
69
+ fs.accessSync(file, fs.constants.F_OK);
70
+ return true;
71
+ } catch (_err) {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function walk(dir, predicate) {
77
+ const results = [];
78
+ if (!exists(dir)) return results;
79
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
80
+ for (const entry of entries) {
81
+ const p = path.join(dir, entry.name);
82
+ if (entry.isDirectory()) {
83
+ results.push(...walk(p, predicate));
84
+ } else if (!predicate || predicate(p)) {
85
+ results.push(p);
86
+ }
87
+ }
88
+ return results;
89
+ }
90
+
91
+ function rel(root, file) {
92
+ return path.relative(root, file).split(path.sep).join('/');
93
+ }
94
+
95
+ function parseFrontmatter(text) {
96
+ if (!text.startsWith('---\n')) return {};
97
+ const end = text.indexOf('\n---', 4);
98
+ if (end === -1) return {};
99
+ const block = text.slice(4, end).split('\n');
100
+ const out = {};
101
+ for (const line of block) {
102
+ const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
103
+ if (!m) continue;
104
+ out[m[1]] = m[2].replace(/^["']|["']$/g, '');
105
+ }
106
+ return out;
107
+ }
108
+
109
+ function extractBacktickNames(line) {
110
+ const names = [];
111
+ const re = /`([^`]+)`/g;
112
+ let m;
113
+ while ((m = re.exec(line)) !== null) {
114
+ const value = m[1].trim();
115
+ if (/^[a-z0-9][a-z0-9-]*$/.test(value)) names.push(value);
116
+ }
117
+ return names;
118
+ }
119
+
120
+ function documentedTiers(root) {
121
+ const sources = [
122
+ path.join(root, 'AGENTS.md'),
123
+ ];
124
+ const tiers = new Map();
125
+ const evidence = [];
126
+ for (const source of sources) {
127
+ const text = maybeReadText(source);
128
+ if (!text) continue;
129
+ const lines = text.split('\n');
130
+ lines.forEach((line, index) => {
131
+ const tierMatch = line.match(/\|\s*\*\*(M[123])\b/);
132
+ if (!tierMatch) return;
133
+ const tier = tierMatch[1];
134
+ for (const name of extractBacktickNames(line)) {
135
+ tiers.set(name, { tier, source: rel(root, source), line: index + 1 });
136
+ evidence.push({ name, tier, source: rel(root, source), line: index + 1 });
137
+ }
138
+ });
139
+ }
140
+ return { tiers, evidence };
141
+ }
142
+
143
+ function compatDocTierMentions(root, skillNames) {
144
+ const source = path.join(root, 'docs', 'codex-compat.md');
145
+ const text = maybeReadText(source);
146
+ const mentions = new Map();
147
+ if (!text) return mentions;
148
+ const lines = text.split('\n');
149
+ lines.forEach((line, index) => {
150
+ const tierMatch = line.match(/\b(M[123])\b/);
151
+ if (!tierMatch) return;
152
+ const tier = tierMatch[1];
153
+ for (const name of extractBacktickNames(line)) {
154
+ if (!skillNames.has(name)) continue;
155
+ if (!mentions.has(name)) mentions.set(name, []);
156
+ mentions.get(name).push({ tier, source: rel(root, source), line: index + 1 });
157
+ }
158
+ });
159
+ return mentions;
160
+ }
161
+
162
+ const PRIMITIVES = [
163
+ {
164
+ id: 'agent-dispatch',
165
+ severity: 'adapter',
166
+ regexes: [/Agent\s*\(/, /\bsubagent_type\b/, /\bAgent View\b/, /\bAgent\b tool\b/, /\bAgent invocation instruction\b/, /\bparallel-Agent dispatch\b/],
167
+ },
168
+ {
169
+ id: 'slash-command',
170
+ severity: 'adapter',
171
+ regexes: [/(^|[\s`])\/[a-z][a-z0-9-]+(?=($|[\s`),.;:}]))/m],
172
+ },
173
+ {
174
+ id: 'hook',
175
+ severity: 'claude-native',
176
+ regexes: [/\bStop hook\b/i, /\bPostToolUse\b/, /\bSessionStart\b/],
177
+ },
178
+ {
179
+ id: 'model-command',
180
+ severity: 'claude-native',
181
+ regexes: [/(^|[\s`])\/model\b/m],
182
+ },
183
+ ];
184
+
185
+ function scanPrimitives(text) {
186
+ const hits = [];
187
+ for (const primitive of PRIMITIVES) {
188
+ for (const regex of primitive.regexes) {
189
+ const m = regex.exec(text);
190
+ if (!m) continue;
191
+ hits.push({
192
+ id: primitive.id,
193
+ severity: primitive.severity,
194
+ line: text.slice(0, m.index).split('\n').length,
195
+ match: m[0].trim(),
196
+ });
197
+ break;
198
+ }
199
+ }
200
+ return hits;
201
+ }
202
+
203
+ function classify(docTier, primitives) {
204
+ const hasClaudeNative = primitives.some((p) => p.severity === 'claude-native');
205
+ const hasAgentDispatch = primitives.some((p) => p.id === 'agent-dispatch');
206
+ const hasAdapter = primitives.some((p) => p.severity === 'adapter');
207
+ if (docTier === 'M1' && (hasClaudeNative || hasAgentDispatch)) return 'tier-drift';
208
+ if (docTier === 'M1') return 'codex-native';
209
+ if (docTier === 'M2') return 'adapter-required';
210
+ if (docTier === 'M3') return 'claude-native';
211
+ if (hasClaudeNative) return 'claude-native-unclassified';
212
+ if (hasAdapter) return 'adapter-required-unclassified';
213
+ return 'codex-native-candidate';
214
+ }
215
+
216
+ function collectSkills(root) {
217
+ const skillsRoot = path.join(root, 'plugins');
218
+ const files = walk(skillsRoot, (p) => path.basename(p) === 'SKILL.md');
219
+ return files.sort().map((file) => {
220
+ const text = readText(file);
221
+ const fm = parseFrontmatter(text);
222
+ const skillName = fm.name || path.basename(path.dirname(file));
223
+ return {
224
+ type: 'skill',
225
+ name: skillName,
226
+ path: rel(root, file),
227
+ frontmatter: fm,
228
+ primitives: scanPrimitives(text),
229
+ };
230
+ });
231
+ }
232
+
233
+ function collectAgents(root) {
234
+ const agents = [];
235
+ for (const plugin of ['fh-meta', 'fh-commons']) {
236
+ const dir = path.join(root, 'plugins', plugin, 'agents');
237
+ for (const file of walk(dir, (p) => p.endsWith('.md')).sort()) {
238
+ const text = readText(file);
239
+ agents.push({
240
+ type: 'agent',
241
+ name: path.basename(file, '.md'),
242
+ path: rel(root, file),
243
+ plugin,
244
+ primitives: scanPrimitives(text),
245
+ });
246
+ }
247
+ }
248
+ return agents;
249
+ }
250
+
251
+ function loadAgentCards(root) {
252
+ const file = path.join(root, '.claude', 'registry', 'agent_cards.json');
253
+ if (!exists(file)) return { present: false, count: 0 };
254
+ try {
255
+ const parsed = JSON.parse(readText(file));
256
+ const count = Array.isArray(parsed)
257
+ ? parsed.length
258
+ : (Array.isArray(parsed.agents) ? parsed.agents.length : (parsed.agent_count || Object.keys(parsed).length));
259
+ return { present: true, path: rel(root, file), count };
260
+ } catch (err) {
261
+ return { present: true, path: rel(root, file), error: err.message, count: 0 };
262
+ }
263
+ }
264
+
265
+ function buildReport(root) {
266
+ const docs = documentedTiers(root);
267
+ const skills = collectSkills(root).map((skill) => {
268
+ const doc = docs.tiers.get(skill.name);
269
+ return {
270
+ ...skill,
271
+ documentedTier: doc ? doc.tier : null,
272
+ tierEvidence: doc || null,
273
+ codexMode: classify(doc && doc.tier, skill.primitives),
274
+ };
275
+ });
276
+ const agents = collectAgents(root);
277
+ const skillNames = new Set(skills.map((s) => s.name));
278
+ const compatMentions = compatDocTierMentions(root, skillNames);
279
+ const findings = [];
280
+
281
+ for (const item of skills) {
282
+ if (item.documentedTier === 'M1') {
283
+ const bad = item.primitives.filter((p) => p.severity === 'claude-native' || p.id === 'agent-dispatch');
284
+ if (bad.length > 0) {
285
+ findings.push({
286
+ severity: 'HIGH',
287
+ code: 'M1_HAS_CLAUDE_NATIVE_PRIMITIVE',
288
+ path: item.path,
289
+ message: `${item.name} is documented M1 but contains ${bad.map((p) => p.id).join(', ')}`,
290
+ });
291
+ }
292
+ }
293
+ }
294
+
295
+ for (const entry of docs.evidence) {
296
+ if (!skillNames.has(entry.name)) {
297
+ findings.push({
298
+ severity: 'WARN',
299
+ code: 'DOC_TIER_REFERENCES_MISSING_SKILL',
300
+ path: `${entry.source}:${entry.line}`,
301
+ message: `${entry.name} is listed as ${entry.tier} but no matching SKILL.md was found`,
302
+ });
303
+ }
304
+ }
305
+
306
+ for (const item of skills) {
307
+ const mentions = compatMentions.get(item.name) || [];
308
+ for (const mention of mentions) {
309
+ if (item.documentedTier && mention.tier !== item.documentedTier) {
310
+ findings.push({
311
+ severity: 'HIGH',
312
+ code: 'CODEX_COMPAT_TIER_DISAGREES_WITH_AGENTS',
313
+ path: `${mention.source}:${mention.line}`,
314
+ message: `${item.name} is ${item.documentedTier} in AGENTS.md but ${mention.tier} in docs/codex-compat.md`,
315
+ });
316
+ }
317
+ }
318
+ }
319
+
320
+ const counts = {
321
+ skills: skills.length,
322
+ agents: agents.length,
323
+ modes: {},
324
+ tiers: { M1: 0, M2: 0, M3: 0, unclassified: 0 },
325
+ findings: {
326
+ HIGH: findings.filter((f) => f.severity === 'HIGH').length,
327
+ WARN: findings.filter((f) => f.severity === 'WARN').length,
328
+ },
329
+ };
330
+ for (const skill of skills) {
331
+ counts.modes[skill.codexMode] = (counts.modes[skill.codexMode] || 0) + 1;
332
+ counts.tiers[skill.documentedTier || 'unclassified'] += 1;
333
+ }
334
+
335
+ return {
336
+ status: findings.some((f) => f.severity === 'HIGH') ? 'DRIFT' : 'OK',
337
+ root,
338
+ counts,
339
+ agentCards: loadAgentCards(root),
340
+ findings,
341
+ skills,
342
+ agents: agents.map((agent) => ({
343
+ name: agent.name,
344
+ path: agent.path,
345
+ plugin: agent.plugin,
346
+ codexMode: 'adapter-runnable',
347
+ note: 'Agents are runnable via fh-run --agent or a runtime-specific dispatch adapter; auto-dispatch is host-specific.',
348
+ })),
349
+ };
350
+ }
351
+
352
+ function printText(report) {
353
+ const lines = [];
354
+ lines.push('FH Codex Doctor');
355
+ lines.push(`Status: ${report.status}`);
356
+ lines.push(`Root: ${report.root}`);
357
+ lines.push('');
358
+ lines.push('Summary');
359
+ lines.push(`- Skills scanned: ${report.counts.skills}`);
360
+ lines.push(`- Agents scanned: ${report.counts.agents}`);
361
+ lines.push(`- Documented tiers: M1=${report.counts.tiers.M1} M2=${report.counts.tiers.M2} M3=${report.counts.tiers.M3} unclassified=${report.counts.tiers.unclassified}`);
362
+ lines.push(`- Codex modes: ${Object.keys(report.counts.modes).sort().map((k) => `${k}=${report.counts.modes[k]}`).join(' ')}`);
363
+ if (report.agentCards.present) {
364
+ lines.push(`- Agent cards: ${report.agentCards.count}${report.agentCards.error ? ` (parse error: ${report.agentCards.error})` : ''}`);
365
+ } else {
366
+ lines.push('- Agent cards: missing');
367
+ }
368
+ lines.push('');
369
+ lines.push('Findings');
370
+ if (report.findings.length === 0) {
371
+ lines.push('- none');
372
+ } else {
373
+ for (const finding of report.findings) {
374
+ lines.push(`- ${finding.severity} ${finding.code} ${finding.path} :: ${finding.message}`);
375
+ }
376
+ }
377
+ lines.push('');
378
+ lines.push('Codex Runtime Contract');
379
+ lines.push('- codex-native: run directly with fh-run/codex exec.');
380
+ lines.push('- adapter-required: core method can run, but Agent/slash steps need adapter substitution.');
381
+ lines.push('- claude-native: do not auto-pass; require Claude Code host or explicit dedicated adapter.');
382
+ lines.push('- unclassified modes are drift signals for future manifest backfill.');
383
+ process.stdout.write(`${lines.join('\n')}\n`);
384
+ }
385
+
386
+ function main() {
387
+ let args;
388
+ try {
389
+ args = parseArgs(process.argv.slice(2));
390
+ } catch (err) {
391
+ process.stderr.write(`ERROR: ${err.message}\n`);
392
+ usage();
393
+ process.exit(11);
394
+ }
395
+ if (args.help) {
396
+ usage();
397
+ return;
398
+ }
399
+ const root = args.root;
400
+ if (!exists(path.join(root, 'package.json'))) {
401
+ process.stderr.write(`ERROR: root does not look like a package/repo root: ${root}\n`);
402
+ process.exit(11);
403
+ }
404
+ if (!looksLikeFHRoot(root)) {
405
+ process.stderr.write(`ERROR: root is missing required FH surfaces (AGENTS.md and plugins/): ${root}\n`);
406
+ process.exit(11);
407
+ }
408
+ const report = buildReport(root);
409
+ if (args.json) {
410
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
411
+ } else {
412
+ printText(report);
413
+ }
414
+ if (args.strict && report.counts.findings.HIGH > 0) {
415
+ process.exit(1);
416
+ }
417
+ }
418
+
419
+ main();
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Status: **beta**. This document is beta-removal condition #2 (see `AGENTS.md` → Codex Compatibility → Beta removal conditions). It lists what works, what breaks, and what to expect when applying forge-harness (FH) methodology through OpenAI Codex (`codex exec`) instead of Claude Code.
4
4
 
5
- FH is a 2-layer system: a **methodology layer** (`tracks/`, `knowledge/`, `SKILL.md` docs) that is model-agnostic, and an **automation layer** (Claude Code hooks, `.claude/agents/`, `/model`, settings.json) that is Claude-native. Codex users run the methodology layer by reading `SKILL.md` files directly; automation steps either run through runtime adapters (`fh-gate`, `fh-run`) or require manual substitution.
5
+ FH is a 2-layer system: a **methodology layer** (`tracks/`, `knowledge/`, `SKILL.md` docs) that is model-agnostic, and an **automation layer** (Claude Code hooks, plugin-channel agents under `plugins/*/agents/`, field-project overrides under `.claude/agents/`, `/model`, settings.json) that is Claude-native. Codex users run the methodology layer by reading `SKILL.md` files directly; automation steps either run through runtime adapters (`fh-gate`, `fh-run`) or require manual substitution.
6
6
 
7
7
  ## Validated invocation pattern
8
8
 
@@ -65,6 +65,20 @@ Resolution order:
65
65
  | `--agent plugin:name` | `plugins/plugin/agents/name.md` first |
66
66
  | `--unit path` | explicit file path |
67
67
 
68
+ ### `fh-codex-doctor`
69
+
70
+ `fh-codex-doctor` is the drift detector for the thin Codex adapter boundary. It reads the canonical
71
+ FH skill/agent files plus the documented M1/M2/M3 tier table, then reports which surfaces are
72
+ Codex-native, adapter-required, Claude-native, or unclassified:
73
+
74
+ ```bash
75
+ npx --package @chrono-meta/fh-gate fh-codex-doctor --strict
76
+ ```
77
+
78
+ Use it before promoting a new Codex automation path. Unknown Claude-native primitives fail closed as
79
+ "manual adaptation required" instead of being treated as compatible by default. When run from an FH
80
+ checkout it scans the current working tree; outside a checkout it scans the installed package.
81
+
68
82
  ### `fh-goal`
69
83
 
70
84
  Codex has native goal/session features. Use those directly when they fit. `fh-goal` is not a replacement for Codex goal; it is a non-interactive wrapper for "run backend task, then run FH governance on changed files":
@@ -92,7 +106,7 @@ Both ran end-to-end with no Claude-native dependency. The M1 tier claim holds fo
92
106
  When `codex exec` runs **inside this repo**, FH's Claude-native git/Stop/PostToolUse hooks attempt to fire and emit `hook: Stop Failed` / `hook: PostToolUse Failed` lines interleaved with output. These are **harmless to the skill result** — the skill's verdict is produced correctly — but they are visible noise. Running from a directory **without** FH's `.claude/settings.json` (the normal Codex-user case) avoids them entirely. Filter with `grep -vE "^hook:"` if needed.
93
107
 
94
108
  ### 2. M2 skills need manual agent substitution
95
- M2 skills (`steel-quench`, `harness-doctor`, `context-doctor`, `sim-conductor`, `harvest-loop`) have a core workflow that runs under Codex, but any step that dispatches `Agent(subagent_type=...)` or a slash command must be replaced by `fh-run` or a direct `codex exec` call reading the sub-agent's `SKILL.md`/agent `.md` — same workflow, different runtime (the "M2 adaptation pattern" in `AGENTS.md`). Example: `steel-quench` Waves 1–3 run; the `quench-challenger` agent step becomes `fh-run --agent fh-commons:quench-challenger`.
109
+ M2 skills (`deliberation`, `steel-quench`, `harness-doctor`, `context-doctor`, `sim-conductor`, `harvest-loop`) have a core workflow that runs under Codex, but any step that dispatches `Agent(subagent_type=...)` or a slash command must be replaced by `fh-run` or a direct `codex exec` call reading the sub-agent's `SKILL.md`/agent `.md` — same workflow, different runtime (the "M2 adaptation pattern" in `AGENTS.md`). Example: `steel-quench` Waves 1–3 run; the `quench-challenger` agent step becomes `fh-run --agent fh-commons:quench-challenger`.
96
110
 
97
111
  ### 3. M3 skills do not run automatically under Codex
98
112
  M3 skills (`goal-quench` Phase-3 Stop hook, `hub-cc-pr-reviewer` CC session context, `install-wizard` settings.json write) require Claude-Code-native runtime and are **methodology reference only** under Codex unless a dedicated adapter exists. Use Codex's native goal/session features for goal control, and use `fh-gate` after completion for FH quality gating.
@@ -107,8 +121,8 @@ The sibling pattern for Gemini is `gemini -p "$(cat <skill+artifact>)"`. Outside
107
121
 
108
122
  | Tier | Under Codex | Action |
109
123
  |---|---|---|
110
- | **M1** | Runs fully | `cat SKILL.md artifact \| codex exec -m gpt-5.5 -` |
111
- | **M2** | Core runs; agent/slash steps via adapter | Substitute each dispatch with `fh-run` or a direct `codex exec` on the sub-agent's `.md` |
124
+ | **M1** | Runs fully (`token-budget-gate`, `asset-placement-gate`, `phantom-quench`, `deep-clarify`, `convergence-loop`) | `cat SKILL.md artifact \| codex exec -m gpt-5.5 -` |
125
+ | **M2** | Core runs; agent/slash steps via adapter (`deliberation`, `steel-quench`, `harness-doctor`, `context-doctor`, `sim-conductor`, `harvest-loop`) | Substitute each dispatch with `fh-run` or a direct `codex exec` on the sub-agent's `.md` |
112
126
  | **M3** | Does not run automatically | Use native Codex session features where available; otherwise read as methodology reference or use a dedicated adapter |
113
127
 
114
128
  ## Beta removal — remaining (external-blocked)
@@ -0,0 +1,139 @@
1
+ # Harness Incubator Doctrine — intent machinization, the nursery, and compose ∪ disrupt
2
+
3
+ > Crystallized 2026-07-12 from an operator insight session ("the day FH stepped forward").
4
+ > This is the *why* underneath the four pillars in `README.md §What makes it a harness, not a toolbox`.
5
+ > Always-loaded summary: `CLAUDE.md §Identity`. Operating unit: `harness_6axis_framework.md`.
6
+
7
+ ## 1. What a harness is — intent machinization
8
+
9
+ A harness is a platform that **reads a human's intent and forges it into a machined form**: either
10
+ *AI-salience* (rules and prompts an AI reliably follows) or *deterministic code* (hooks, scripts, gates
11
+ that need no model at all). Building a project IS machinizing human intent; a harness **accelerates and
12
+ amplifies** that machinization.
13
+
14
+ The trajectory is always the same four steps:
15
+
16
+ ```
17
+ intent (human) → forge into an executable form (AI) → agreement (HITL) → machinery
18
+ ```
19
+
20
+ **Agreement is a load-bearing gate, not a courtesy** — machinizing an unagreed intent hardens the wrong
21
+ thing. The HITL step sits immediately *before* machinization for exactly this reason.
22
+
23
+ ### Trial-and-error relocates; it does not disappear
24
+
25
+ The harness's payoff is **less trial-and-error on the human side** — the request → feedback → regenerate
26
+ loop is skipped. But the loop is not deleted; it **relocates into the harness**, where agents and
27
+ sidecars run it in parallel. Two gains, not one:
28
+
29
+ 1. Trial-and-error the human **does not perform** → human time drops.
30
+ 2. Trial-and-error the harness runs **in parallel** → wall-clock drops versus sequential human retries.
31
+
32
+ What is freed is not only time but **attention** — and the quality gate (the responsibility-router
33
+ pillar) re-spends that freed attention only where a change is *irreversible*. "Time down + attention
34
+ routed to what matters" is the complete form of harness acceleration, and it is what "quality is the
35
+ lever; speed is the result" cashes out to.
36
+
37
+ ## 2. The scale ladder — tool < star < galaxy
38
+
39
+ | Unit | What it is |
40
+ |---|---|
41
+ | skill / agent / plugin | a tool |
42
+ | **harness** (field harness) | a *star* — one project's tools, rules, gates, and memory bound into a single working body, purpose-built (e.g. a coding harness specialized for one product domain) |
43
+ | **meta-harness** (FH) | the *galaxy* the stars live in — and a **nursery**, not just a container |
44
+
45
+ A meta-harness is "a harness for building harnesses." Under a given theme it can machinize anything —
46
+ which is why its unit of work is the harness, not the skill.
47
+
48
+ ## 3. The nursery — FH as field-harness incubator and simulator
49
+
50
+ FH's dual role:
51
+
52
+ - **Primary — build and emit**: forge a field harness and release it as an independent, specialized
53
+ unit. What ships today is the **scaffold + approval machinery** (Full-Harness Mode in
54
+ `auto_project_mapping.md §6`, gate-compliant field scaffolds); the full simulate-then-emit chamber
55
+ flow is the *named target*, practiced to date as dogfooding a capability inside FH and then landing
56
+ it in the field repo.
57
+ - **Contingency — act as the field harness itself**: run the whole of FH (harness-unit, not
58
+ skill-unit) as a sandbox simulator for a project. Expensive per run — that is the price of a
59
+ general-purpose chamber.
60
+
61
+ **Completeness requirement**: a nursery that can birth any star must hold every element. "Everything a
62
+ field-harness simulator needs must be possible inside FH" — multi-model dispatch, tooling, live-surface
63
+ operation, gates. This is an *aspiration that directs capability assembly* (what `goal-quench`'s
64
+ assembly ladder points at), not a claim of current completeness.
65
+
66
+ **The economics (why expensive-per-run is cheap-in-total):**
67
+
68
+ ```
69
+ Option A: build N field harnesses separately, each doing its own trial-and-error
70
+ → the same errors are repeated N times; learning is never shared
71
+ Option B: incubate each field project inside the FH chamber
72
+ → trial-and-error pools in ONE place and compounds (the self-evolving loop)
73
+ → each next project inherits the previous learning → total trial-and-error shrinks
74
+ ```
75
+
76
+ FH's sandbox unit cost is higher (general-purpose overhead), but total portfolio cost is *expected* to
77
+ be lower — when reuse amortizes the chamber overhead. Honest trade-off: it is expensive *until
78
+ emission*; the emitted harness is specialized and cheap, and the learning stays in FH. **Evidence grade
79
+ (stated honestly)**: this economics is a *design argument plus n=1*, not a measured comparison — the
80
+ counterfactual (building the same capability standalone) was never run, so "cheaper in total" is a
81
+ **named bet**, the same treatment the disrupt path gets in §4(c); residual risk: one-off projects that
82
+ never recur may not amortize. Empirical grounding for the *capability* (not the cost comparison): a
83
+ field QA harness's acts 2–3 arc (2026-07, private) — its live-run capability was forged inside the FH
84
+ chamber, then landed in the field repo.
85
+
86
+ **Minimal execution skeleton (when the operator accepts simulate-first)**: the procedure is currently
87
+ *judged/ad-hoc*, standardization deferred to a second real occurrence (measured-trigger, per the
88
+ evidence-threshold build discipline): ① open a chamber workspace (a worktree or `tracks/{project}-sim/`
89
+ — never a real project repo); ② scope the run through `goal-quench`'s budget gate (chamber runs are the
90
+ expensive path — cap them); ③ drive the simulation with existing FH assets (dispatch, gates, live
91
+ surfaces as needed); ④ the emit judgment — "the simulation holds" — is a *judged* call paired with the
92
+ run's own mechanical evidence (tests passing, gate verdicts, reproduced flows), decided **with the
93
+ operator (HITL)**; ⑤ on emit, route through Full-Harness Mode / field scaffolds as usual.
94
+
95
+ **Incubation unit — projects AND features**: incubation applies not only to new projects but to **new
96
+ capabilities of an existing harness**. A field harness's self-development is itself run inside the
97
+ meta-harness chamber first, then transplanted — the nursery forges new layers for existing stars, not
98
+ only new stars. Same economics.
99
+
100
+ **Simulate-first entry**: when a new project is uncertain, exploratory, or failure-expensive, the
101
+ recommended path is *simulate inside the chamber first, then emit the initial model* — not
102
+ build-immediately. (Wired as a recommendation branch in `CLAUDE.md §Onboarding / Acceleration
103
+ Autopilot`; build-immediately remains correct for clear, small, low-failure-cost projects.)
104
+
105
+ ## 4. Compose ∪ disrupt — two operating modes over other harnesses
106
+
107
+ | Mode | What | FH mechanism |
108
+ |---|---|---|
109
+ | **Compose** (additive) | cluster leading harnesses, gather their strengths at optimized token cost | sidecar / multi-harness orchestration |
110
+ | **Disrupt** (transformative) | dismantle them into parts, overcome-and-adopt their weak points into FH or a target field harness; self-destruct and reassemble to go where others cannot | **crucible mode** (`crucible_mode.md`) — total-ingest → melt via steel/phantom-quench → identity-bond → reforge; **core invariants never melt** |
111
+
112
+ Theory anchor (an operator-supplied analogy drawing on Clayton Christensen's disruptive-innovation
113
+ thesis): disruptive technology tends to emerge from re-purposing existing parts
114
+ for unintended uses — crude and inefficient at first, then growing fast along a dimension incumbents
115
+ overlooked. Mapped here: "re-purposed parts" = other harnesses dismantled into components;
116
+ "the overlooked dimension" = the direction others cannot go. Companion criterion,
117
+ **fitness-for-purpose**: equipment that is well-made but would not survive *this* dragon is better
118
+ re-forged from scratch than patched.
119
+
120
+ Honest boundaries: (a) core invariants (floors, gates, identity) are never melted; (b)
121
+ overcome-and-adopt is curation with license/provenance respect, never wholesale copying; (c) the
122
+ disruptive path *looks inferior early* — running it is a deliberate bet, named as such.
123
+
124
+ ## 5. Sidecar corollary — ride the evolution, don't patch the weak spots
125
+
126
+ Mechanically patching each frontier model's current weaknesses produces scaffolding that dies as models
127
+ improve (the weakness itself disappears). FH's sidecar layer is therefore built to **co-evolve**: shed
128
+ what the substrate now does natively (`feedback: frontier substrate self-adaptation`), absorb what it
129
+ ships next, and use cross-family decorrelation as *today's* trust lever (composition beats a single
130
+ model's ceiling — see `multi_model_sidecar_strategy.md`). Capability is the model's; assembly, trust,
131
+ and evolution are the harness's.
132
+
133
+ ## Done When (doctrine doc — reference asset)
134
+
135
+ - The four-pillar README section, `CLAUDE.md §Identity`, and this doc tell one consistent story
136
+ (no contradicting claims). *Check class: judged; pair: contradiction scan on ingest
137
+ (`sync_push_protocols.md` step 3).*
138
+ - Every mechanism named here points at a real, existing asset (Full-Harness Mode, crucible_mode,
139
+ goal-quench, multi_model_sidecar_strategy). *Check class: mandatory-pass (phantom scan).*
@@ -33,6 +33,13 @@ markers) — never silently drop a capability.
33
33
  → SessionStart hook), the tone salience gap (2026-07-08 → recorded, prompt-layer), the
34
34
  card-reconcile blind spot (2026-07-10 → mtime-independent STATUS map in the hook). The doctrine
35
35
  is a name for what the fix pattern already was.
36
+ - **The value does not vanish at the top tier — the harness complements the strongest model too**
37
+ (operator observation, 2026-07-10, same-day measured): a Sonnet blind probe caught a SIGPIPE
38
+ silent-death defect in a script the top-tier session had just written (5/5 repro), and the new
39
+ close-chain checker blocked that same top-tier session's card-last violation twice on its first
40
+ day. Structure-enforcing checks are tier-blind by construction — so the harness raises the floor
41
+ for weak models *and* guards the ceiling for strong ones. Weak-model benefit is the larger term
42
+ (H1), top-tier benefit is the existence proof that the harness is not scaffolding-only.
36
43
 
37
44
  ## The defect class: tier-gated capability
38
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrono-meta/fh-gate",
3
- "version": "1.4.52",
3
+ "version": "1.4.54",
4
4
  "description": "FH runtime adapters — run FH governance, skills, and agents via Claude or Codex with machine-parseable gates.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -20,10 +20,11 @@
20
20
  "bin": {
21
21
  "fh-gate": "bin/fh-gate.js",
22
22
  "fh-run": "bin/fh-run.js",
23
- "fh-goal": "bin/fh-goal.js"
23
+ "fh-goal": "bin/fh-goal.js",
24
+ "fh-codex-doctor": "bin/fh-codex-doctor.js"
24
25
  },
25
26
  "scripts": {
26
- "prepare": "chmod +x bin/fh-gate.js bin/fh-run.js bin/fh-goal.js scripts/fh-gate.sh scripts/fh-run.sh scripts/fh-goal.sh",
27
+ "prepare": "chmod +x bin/fh-gate.js bin/fh-run.js bin/fh-goal.js bin/fh-codex-doctor.js scripts/fh-gate.sh scripts/fh-run.sh scripts/fh-goal.sh",
27
28
  "test": "bash scripts/selfcheck.sh",
28
29
  "prepublishOnly": "bash scripts/selfcheck.sh && bash scripts/public_surface_scan_files.sh",
29
30
  "release": "bash scripts/public_surface_scan_files.sh && npm publish"
@@ -45,10 +46,13 @@
45
46
  },
46
47
  "files": [
47
48
  "AGENTS.md",
49
+ ".claude-plugin/marketplace.json",
48
50
  "CATALOG.md",
49
51
  "CHEATSHEET.md",
50
52
  "CLAUDE.md",
53
+ ".claude/registry/agent_cards.json",
51
54
  "docs/CONTRIBUTING.md",
55
+ "bin/fh-codex-doctor.js",
52
56
  "bin/fh-gate.js",
53
57
  "bin/fh-run.js",
54
58
  "bin/fh-goal.js",
@@ -57,13 +61,20 @@
57
61
  "scripts/fh-gate.sh",
58
62
  "scripts/fh-run.sh",
59
63
  "scripts/fh-goal.sh",
64
+ "scripts/count_check.sh",
60
65
  "scripts/selfcheck.sh",
66
+ "templates/local_fh_context.md",
67
+ "plugins/fh-meta/.claude-plugin/plugin.json",
61
68
  "plugins/fh-meta/skills",
62
69
  "plugins/fh-meta/agents",
70
+ "plugins/fh-commons/.claude-plugin/plugin.json",
63
71
  "plugins/fh-commons/skills",
64
72
  "plugins/fh-commons/agents",
65
73
  "knowledge/shared/harness-core",
66
74
  "knowledge/shared/dialogue",
67
- "README.md"
75
+ "README.md",
76
+ "README.ko.md",
77
+ "README.zh.md",
78
+ "README.ja.md"
68
79
  ]
69
80
  }