@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.
- package/.claude/registry/README.md +26 -0
- package/.claude/registry/agent_cards.json +73 -0
- package/.claude-plugin/marketplace.json +25 -0
- package/AGENTS.md +2 -2
- package/CHEATSHEET.md +1 -0
- package/CLAUDE.md +31 -1
- package/README.ja.md +443 -0
- package/README.ko.md +442 -0
- package/README.md +70 -5
- package/README.zh.md +425 -0
- package/bin/fh-codex-doctor.js +419 -0
- package/docs/codex-compat.md +18 -4
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +139 -0
- package/knowledge/shared/harness-core/sonnet_floor_doctrine.md +7 -0
- package/package.json +15 -4
- package/plugins/fh-commons/.claude-plugin/plugin.json +23 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +36 -0
- package/scripts/count_check.sh +95 -0
- package/scripts/selfcheck.sh +27 -20
- package/templates/CLAUDE.md +87 -0
- package/templates/local_fh_context.md +18 -0
|
@@ -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();
|
package/docs/codex-compat.md
CHANGED
|
@@ -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.
|
|
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
|
}
|