@chrono-meta/fh-gate 1.4.51 → 1.4.53

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.
Files changed (36) hide show
  1. package/.claude/registry/README.md +26 -0
  2. package/.claude/registry/agent_cards.json +73 -0
  3. package/.claude-plugin/marketplace.json +25 -0
  4. package/AGENTS.md +2 -2
  5. package/CATALOG.md +6 -0
  6. package/CHEATSHEET.md +1 -0
  7. package/CLAUDE.md +21 -11
  8. package/README.md +12 -1
  9. package/bin/fh-codex-doctor.js +419 -0
  10. package/docs/codex-compat.md +18 -4
  11. package/knowledge/shared/harness-core/capability_escalation_consent.md +7 -0
  12. package/knowledge/shared/harness-core/claude_md_gate_details.md +15 -8
  13. package/knowledge/shared/harness-core/deep_research_capability_ladder.md +1 -1
  14. package/knowledge/shared/harness-core/fh_detail_protocols.md +4 -0
  15. package/knowledge/shared/harness-core/loop_engineering.md +80 -0
  16. package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +5 -1
  17. package/knowledge/shared/harness-core/self_evolution_routine.md +17 -12
  18. package/knowledge/shared/harness-core/sonnet_floor_doctrine.md +132 -0
  19. package/package.json +11 -3
  20. package/plugins/fh-commons/.claude-plugin/plugin.json +23 -0
  21. package/plugins/fh-commons/skills/deliberation/SKILL.md +1 -1
  22. package/plugins/fh-meta/.claude-plugin/plugin.json +36 -0
  23. package/plugins/fh-meta/skills/agent-composer/SKILL.md +1 -1
  24. package/plugins/fh-meta/skills/apex-review/SKILL.md +1 -1
  25. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +1 -1
  26. package/plugins/fh-meta/skills/context-doctor/SKILL.md +1 -1
  27. package/plugins/fh-meta/skills/harvest-loop/SKILL.md +1 -1
  28. package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
  29. package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +1 -1
  30. package/plugins/fh-meta/skills/sim-conductor/SKILL.md +1 -1
  31. package/plugins/fh-meta/skills/steel-quench/SKILL.md +1 -1
  32. package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +1 -1
  33. package/scripts/count_check.sh +95 -0
  34. package/scripts/selfcheck.sh +27 -20
  35. package/templates/CLAUDE.md +87 -0
  36. 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();
@@ -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)
@@ -123,3 +123,10 @@ to offload exec cost off the paid API entirely. The protocol turns hard-won econ
123
123
  protocol never flips the model itself.
124
124
  - **Sonnet floor is a floor, not a ceiling** — declined-floor-up still escalates *within* Sonnet's
125
125
  harness depth (sub-agents, good structure); it does not cap capability, only cost/trust surface.
126
+
127
+ ---
128
+
129
+ > **Canonical axiom cross-ref (2026-07-10)**: the "Sonnet floor is first-class, not degraded" stance
130
+ > this protocol operationalizes is now named — `sonnet_floor_doctrine.md` (base ops 100% Sonnet;
131
+ > tier-gated capability = defect; escalation = dispatch, consent-gated **here**). This file remains
132
+ > the consent mechanics home; the doctrine node does not restate them.
@@ -69,10 +69,13 @@ demand a strict YES/NO + one-line reason, judge whether the rule fired (mechanis
69
69
  directions — a claim checkable against that skill — re-validating that day's salience-binding fix at a
70
70
  sub-Sonnet tier).
71
71
 
72
- **FAIL-triage**: a FAIL never blocks alone — the opus orchestrator triages it as a *real salience gap* (fix
73
- the rule) vs a *floor-model quirk* (small-model loop/hallucination, per the public "Local AI is not Opus"
74
- finding + the cheap-oracle ceiling a small model adds nothing where one grep already settles the check).
75
- The terminal verdict stays with the frontier (Sonnet sim + opus judge) no judge-only path, no
72
+ **FAIL-triage**: a FAIL never blocks alone — the orchestrator (whatever tier is driving; the triage
73
+ judgment is *trusted* at opus+ and run-or-ask below, per §Floor governance) triages it as a *real
74
+ salience gap* (fix the rule) vs a *floor-model quirk* (small-model loop/hallucination, per the public
75
+ "Local AI is not Opus" finding + the cheap-oracle ceiling a small model adds nothing where one grep
76
+ already settles the check). The terminal verdict stays with the **Sonnet-or-higher governor bound to a
77
+ mechanical anchor** (Sonnet sim verdict + the anchor evidence; an opus judge is the *dispatch-recommended*
78
+ strengthener, not a requirement — Sonnet-Floor Doctrine 2026-07-10) — no judge-only path, no
76
79
  weak-local-judge regression of the judge-robustness principle (mechanical anchor over judge-only verdict).
77
80
  The cross-family-panel upgrade spec lives in the private companion store's `handoff/` design note.
78
81
 
@@ -190,11 +193,15 @@ to be modified), check the **session model** (self-identity; if the runtime with
190
193
  unknown) and surface **one line** — then proceed, never block:
191
194
 
192
195
  - Model known and opus-tier or above → no notice (already optimal).
193
- - Model known and below opus-tier → *"이 작업은 FH 자체개발(Mode D)입니다 가용 최강 모델 핀을
194
- 권장합니다 (`/model opus` 이상; 측정 근거: README §Model setup). 그대로 진행해도 floored
195
- 디스패치가 깊이 턴을 커버하지만, 세션-레벨 설계 깊이는 핀이 좌우합니다."*
196
+ - Model known and below opus-tier → **dispatch-first** (Sonnet-Floor Doctrine 2026-07-10the
197
+ primary recommendation keeps the Sonnet substrate and routes depth to dispatch; a session pin is
198
+ the *secondary* option): *"이 작업은 FH 자체개발(Mode D)입니다 — Sonnet 그대로 진행하면서 깊이
199
+ 턴(적대검증·설계리뷰)은 사이드카/opus 디스패치로 커버하는 걸 권장합니다(동의 게이트:
200
+ capability_escalation_consent). 세션 전체가 설계-깊이 중심이면 차선으로 `/model opus` 핀도
201
+ 가능합니다."*
196
202
  - Model unknown (runtime withholds identity) → static fallback: *"FH 자체개발 작업입니다 — 세션
197
- 모델이 opus 이상이 아니라면 전환을 권장합니다 (`/model opus`+)."*
203
+ 모델이 opus 미만이면 깊이 턴을 디스패치로 커버하세요(권장); 설계-깊이 세션이면 `/model opus`
204
+ 핀이 차선입니다."*
198
205
 
199
206
  **Guards**: once per session · advisory only — **never switch the session model** (human override is
200
207
  inviolable; a pin is not a cap — tier-floor resolution §Floor governance) · field-project operation
@@ -23,7 +23,7 @@ research-heavy task can pull it.
23
23
  | Rung | Capability | When it applies | Tier note |
24
24
  |---|---|---|---|
25
25
  | **1. Agentic research skill** | An autonomous multi-step researcher **present in the live session skill list** — in Claude Code that is `octo:research` (Claude Octopus, multi-AI synthesis) when installed. A native `/deep-research` was **not registered in CC in this install** (measured 2026-06-14: Skill `deep-research` → "Unknown skill"; the Claude **app** surfaces it highlighted, this CC build does not). Treat it as **app-side / operator-invoked unless a future CC build surfaces it** — re-detect from the live skill list, don't assume CC *cannot* have it (capability is install- and version-dependent — `[[feedback_verify_before_downgrade]]`) | Best when agent-fireable: runs its own search→read→synthesize loop | Self-contained; an external multi-AI path (Octopus → Gemini/Codex) bills **outside** CC's budget |
26
- | **2. Claude multi-source synthesis** | `WebSearch` + `WebFetch` tools, synthesized in-context | The always-available floor for any Claude session — no extra install | **Tier-sensitive**: synthesis depth tracks the session model. Routine survey = Sonnet default; deep analysis / contested findings = pin Opus (tier-floor, `multi_model_sidecar_strategy.md §Tier-floor resolution`) |
26
+ | **2. Claude multi-source synthesis** | `WebSearch` + `WebFetch` tools, synthesized in-context | The always-available floor for any Claude session — no extra install | **Tier-sensitive**: synthesis depth tracks the session model. Routine survey = Sonnet default; deep analysis / contested findings = dispatch-first (route the deep-read to an opus/sidecar agent, consent-gated; session pin secondary — sonnet_floor_doctrine.md; tier-floor mechanics: `multi_model_sidecar_strategy.md §Tier-floor resolution`) |
27
27
  | **3. `frontier-digest`** | The narrow specialization — HN + arxiv trend scan with FH-context synthesis | Use **only** when the research *is* AI/harness trend-scanning, not general topic research | FH-native; has its own WebSearch fallback |
28
28
 
29
29
  **Resolution rule**: detect research-heavy intent → check the live skill list → take rung 1 if a
@@ -175,6 +175,10 @@ priority: high|medium|low
175
175
 
176
176
  **forge-harness is not meant to use more tokens** — standard tier delivers meaningful improvements while minimizing token usage.
177
177
 
178
+ > Terminology guard: the S/M/L/XL **execution tier is a token-depth budget, NOT a model tier** — it is
179
+ > orthogonal to the Sonnet-floor / model-floor axis (`sonnet_floor_doctrine.md`); an XL run on Sonnet and
180
+ > an S run on Opus are both legal combinations.
181
+
178
182
  ```yaml
179
183
  EXECUTION_TIER: standard # light / standard / full / max
180
184
  ```
@@ -0,0 +1,80 @@
1
+ # Loop Engineering — the 5-question discipline and FH's loop inventory
2
+
3
+ > Companion node to the CLAUDE.md §Field-Harness Diagnostic **Loop-readiness lens** (its detail
4
+ > home) and to `sonnet_floor_doctrine.md` (PROSE loop legs are exactly where Sonnet-tier misses
5
+ > live — one spine, two lenses). Origin: 황민호 loop-eng 5-question review (2026-07-10, C-tier
6
+ > sister ledger — dedup hit on coverage, the *lens* absorbed) + Loop Engineering sister
7
+ > (`[[project_loop_engineering_sister]]`, Prompt→Context→Harness→Loop layering).
8
+
9
+ ## The 5 questions — from diagnostic to design-time discipline
10
+
11
+ A path that *runs* is not a path that *loops*. Before authoring any autonomous path (skill step
12
+ chain, routine, close sequence), answer all five **at design time** — the diagnostic lens then only
13
+ re-checks what authoring already declared:
14
+
15
+ | # | Question | FH's mechanical form |
16
+ |---|---|---|
17
+ | 1 | **Initiate** — what starts it, mechanically or by utterance? | trigger phrases (≥3, gate-checked) · hooks (SessionStart/Stop/pre-commit) · cadence rules |
18
+ | 2 | **Complete** — is there a Done-When? | Done-When with declared check class (mandatory-pass / measured / judged) — already a skill-gate item |
19
+ | 3 | **Validate** — is the check anchored, not judge-only? | mechanical anchor over judge verdict (`[[feedback_judge_robustness_mechanical_anchor]]`); judged conditions name their adversarial pairing |
20
+ | 4 | **Halt** — budget guard, convergence detection, runaway stop? | token-budget-gate · convergence-loop N-round cap · goal-quench thresholds · the 1000-agent backstop |
21
+ | 5 | **Persist** — does state reach the next run? | session card · handoff STATUS stamps · edit-manifest predict-verify · memory |
22
+
23
+ **Design-time rule**: a new autonomous path whose author cannot answer one of the five has found a
24
+ defect *before shipping it* — cheaper than the diagnostic finding it later. The field-asset
25
+ scaffold (auto_project_mapping §6) carries halt + persist stubs so field skills answer #4/#5 by
26
+ construction, the same by-construction pattern as the gate-compliant skeleton.
27
+
28
+ ## FH loop inventory — legs, enforcement class, measured gaps
29
+
30
+ Census 2026-07-10, all rows cross-family source-verified (codex gpt-5.5 — xhigh for micro/session/
31
+ weekly, high for quarterly/substrate; the substrate row's original governor self-assessment was
32
+ 4/5 REFUTED by the external pass — the census discipline earning its keep). MECH = hook /
33
+ script / exit-code (tier-independent); PROSE = salience-dependent (Sonnet-floor risk surface).
34
+
35
+ | Loop | initiate | complete | validate | halt | persist |
36
+ |---|---|---|---|---|---|
37
+ | **Micro** — goal-quench | MECH (`.active` state) | mixed (Done-When explicit; `/goal` invocation manual) | MECH (`.pending` + pipeline-conductor gate) | **PROSE** (mid-run thresholds instructional) | MECH (calibration record) |
38
+ | **Session** — close chain ①–⑥ | PROSE (closing-phrase trigger) | **mixed** (card-last + step coverage now verified by `scripts/session_close_check.sh` — exit 1 on violation; the *performing* stays with the session, the *catching* is MECH, 2026-07-10) | mixed (git/PR inputs mech, synthesis remembered) | PROSE (no budget/stop guard) | **mixed** (companion-store sync script + SessionStart STATUS map + close-check ⑤ invariant, 2026-07-10) |
39
+ | **Weekly** — harvest-loop / audit cycle | PROSE (proposal at session start) | PROSE (self-reported Done-When) | mixed (`below_floor_scan.sh` exit code; rest hand-gathered) | mixed (critic retry cap 1; no global budget) | PROSE (audit file by hand) |
40
+ | **Quarterly** — maturity roadmap | PROSE (~90d cadence, no auto-detection) | PROSE (phase gates, checked manually) | PROSE (basis-path obligations, no anchor bundle) | **PROSE** (transition deferral / Phase-regression guards exist — hub_maturity_roadmap §6.1 — prose, NOT n/a) | PROSE (roadmap doc, no canonical state file) |
41
+ | **Substrate** — self-adaptation mission | **mixed→MECH improving** (routine schedules + context-entry proposals; the "substrate-version jump" detector now EXISTS — `scripts/substrate_jump_detector.sh`, SessionStart-wired, silent-unless-jump; was a phantom until 2026-07-10) | PROSE (routine terminal states are prompt-following) | mixed (weekly change runs the 4-axis gate — marker form MECH at commit; removal approval prose/HITL) | PROSE (one-proposal-per-week · deferred-draft-PR fallback · stop-after-PR — guards exist, all prose, NOT n/a) | mixed (GitHub issue comments + draft PRs are the durable spine, plus gate markers/edit-manifest/fh_signals — not "memory") |
42
+
43
+ **Reading the map**: the 4-axis auto-gate is FH's only all-MECH loop (initiate=hook detect,
44
+ complete=all-axes-or-block, halt=missing-marker-fails, persist=marker+manifest) — and it is also
45
+ FH's most trusted loop. That correlation is the doctrine: **trust tracks mechanization, not model
46
+ tier.** The PROSE-densest loops (session close, weekly) are where the measured misses actually
47
+ occurred (card staleness 2026-07-10; audit cadence slips).
48
+
49
+ ## Case study — persist-leg mechanization (2026-07-10)
50
+
51
+ Company sessions push results to the companion store but never run the local close chain, so the
52
+ card's ⑤ update was the only reconcile point — prose, and mtime-blind: a status stamp landing
53
+ *before* a card rewrite became permanently invisible to the "newer than card" list. Fix: the
54
+ SessionStart hook now emits an **mtime-independent STATUS map** (all DONE/SUPERSEDED/RESOLVED
55
+ stamps, every session) with an explicit cross-check imperative. One measured miss → one mechanized
56
+ leg — the standing pattern (`sonnet_floor_doctrine.md §Why`).
57
+
58
+ ## Hardening backlog — evidence-threshold, NOT built speculatively
59
+
60
+ The census surfaces candidate hardenings (close-chain checklist script, weekly-audit scaffold
61
+ script, harvest-loop evidence bundle, goal-quench checkpoint files). Per the build discipline
62
+ (`[[feedback_evidence_threshold_build_discipline]]`), each is built **only when its miss is
63
+ measured** (a real slip attributable to that PROSE leg), mirroring how the SessionStart hook and
64
+ STATUS map each shipped on a production miss, not a guess. Recording the map here *is* the
65
+ instrument: the next slip finds its leg pre-diagnosed.
66
+
67
+ | Backlog item | Fires when (measured trigger) |
68
+ |---|---|
69
+ | ~~Close-chain ordered-checklist script~~ | **BUILT 2026-07-10** (`scripts/session_close_check.sh`) — operator strengthen-instruction; the miss class (card staleness) was already measured, only the build trigger was overridden (recorded, not silent) |
70
+ | Weekly-audit scaffold + data-gather script | a weekly audit missed or hand-gathered wrong window data |
71
+ | harvest-loop Step 0-b/0-c evidence check | a harvest run misses completed items despite `fh_completed_*` existing |
72
+ | goal-quench mid-run checkpoint files (70/85/95%) | a /goal run blows through a threshold unnoticed |
73
+ | ~~Substrate-jump detector~~ | **BUILT 2026-07-10** (`scripts/substrate_jump_detector.sh`, SessionStart-wired) — same operator instruction; structure-enforcing class (out-of-context drift), permanent per the durable-mechanization criterion |
74
+ | Quarterly maturity checker (`quarterly_maturity_check.sh` — criterion status + §6.1 BLOCKED emit) | a quarterly re-diagnosis is missed >90d or a phase transition skips the simplification checklist |
75
+
76
+ ## Done When (for a new/changed autonomous path)
77
+
78
+ - All 5 questions answered at design time, each leg labeled MECH or PROSE *(mandatory-pass)*.
79
+ - Any judged validate-leg names its adversarial pairing *(mandatory-pass — inherits the skill gate)*.
80
+ - PROSE legs on load-bearing paths carry a Sonnet blind-sim verdict *(measured — doctrine §ladder step 2)*.
@@ -396,7 +396,11 @@ because it has opus?". A floor is satisfied by the chosen engine's **strongest f
396
396
  **Human override is inviolable — and a pin is not a cap**: if the operator pins a session default
397
397
  (stronger or weaker), FH follows it for **session turns**; floors govern FH's **own sub-agent
398
398
  dispatches** and a session pin does not lower them — that separation *is* the Sonnet-main +
399
- Opus-dispatch doctrine (pinned-sonnet sessions still dispatch floored agents at opus).
399
+ Opus-dispatch doctrine (pinned-sonnet sessions still dispatch floored agents at opus). Canonical
400
+ axiom + defect-class + prescription ladder: `sonnet_floor_doctrine.md` (2026-07-10) — this section
401
+ remains the operating mechanics (F1/F2, floor governance) under that axiom; SKILL.md hard `model:`
402
+ pins were retired the same day (session-inherit + dispatch recommendation), agent-side dispatch
403
+ floors unchanged.
400
404
 
401
405
  **Field depth-escalation (the upward complement)**: floors push *dispatches* up automatically, but
402
406
  main-thread depth on a field session has no floor — so the templates bundle carries a Field