@chrono-meta/fh-gate 1.4.74 → 1.4.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +18 -0
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +14 -1
- package/knowledge/shared/harness-core/harness_frontier_diagnosis_2026-06-02.md +1 -1
- package/knowledge/shared/harness-core/meta_harness_engineering_definition.md +1 -1
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +6 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +29 -1
- package/package.json +7 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +38 -3
- package/plugins/fh-meta/skills/sim-conductor/SKILL_detail.md +6 -0
- package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +6 -0
- package/scripts/degrade_direction_scan.sh +17 -2
- package/scripts/memory_link_check.py +237 -0
- package/scripts/memory_nearcheck.py +131 -0
- package/scripts/package_coverage_check.sh +25 -4
- package/scripts/selfcheck.sh +35 -0
- package/scripts/session_close_check.sh +31 -1
- package/scripts/sidecar_wait.sh +76 -0
- package/scripts/test_card_drift_probe.sh +77 -0
- package/scripts/test_degrade_scan_shell_probes.sh +26 -0
- package/scripts/test_memory_link_check.sh +134 -0
- package/scripts/test_session_close_lanes.sh +99 -0
- package/templates/degrade_direction_scan.sh +17 -2
- package/plugins/fh-meta/skills/context-bridge-dispatch/SKILL.md +0 -32
- package/plugins/fh-meta/skills/self-marketing-lint/SKILL.md +0 -30
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "fh-meta",
|
|
14
|
-
"version": "1.4.
|
|
14
|
+
"version": "1.4.76",
|
|
15
15
|
"description": "Hub meta-operations toolkit — 35 skills + 7 agents. New in 1.4.53: `fh-codex-doctor` (npm bin) — Codex adapter drift scanner; reads the documented M1/M2/M3 skill tier map + skill/agent source and reports codex-native/adapter-required/claude-native/unclassified per unit, wired into `npm test`/`prepublishOnly` (fail-closed on unclassified Claude-native primitives). New in 1.4.49: steel-quench gains Step 0.6 Verdict-Invariance Probe (groundedness axis — a load-bearing judged gate's verdict must track behavior, not rubric phrasing; measured flip-count over cross-family paraphrases; arXiv:2605.06161 Policy Invariance anchor); multi_model_sidecar_strategy §Vendor-native harness (a model is strongest in its own vendor CLI — Claude/CC, GPT/codex, Gemini/Antigravity; a universal router degrades all of them, so it stays an autocomplete/QA sidecar, never orchestration); predelete_check.sh fail-closed rewrite; memory-hygiene A-TMA anchor. New in 1.4.48: phantom-quench + steel-quench gain external frontier anchors (arXiv:2607.02052 package-hallucination; arXiv:2607.02057 prompt-coverage-adequacy); README model-flat claim reframed from a per-release point-curve to structural invariants (operation flattens across tiers; depth tier-order fixed within a generation). New in 1.4.47: onboarding step ① surfaces the Mode D companion-store session-start load in the auto-read salience anchor (previously only in the local binding + rules, so a greeting could skip the load). New in 1.4.46: context-doctor command-output axis (route to rtk/proxy for verbose CLI stdout, complementing .claudeignore; risk-gated to token-scarce envs). New in 1.4.41: context-doctor 2026 trigger vocab (context engineering/rot/collapse) + phantom-citation hardening; hub measurement-integrity-checklist (cross-model measurement pre-flight: display-name pin/reps≥3/discriminating probe). New in 1.4.40: install-wizard queryable-wiki scaffold (INDEX + session-start read + R/W/C ingest). New in 1.4.39: auto-decorrelation (cross-family verifier sidecar recruitment) + video-ingest (capability-routed video ingestion). New in 1.4.x: verify-axis check-class taxonomy (mandatory-pass/measured/judged), no-reinvention Tier-0 inventory, 7-class failure taxonomy, Destructive-Op Gate, Wave-T (Temper), tier-floor governance, Mode D Model Notice, FC consent lane, default-Sonnet guidance. New in 1.3.0: public-surface-audit, field-harvest Mode B auto-trigger, 4-axis gate scope ext. Validated cross-CLI: Claude Code, Codex, Gemini.",
|
|
16
16
|
"source": "./plugins/fh-meta"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "fh-commons",
|
|
20
|
-
"version": "1.4.
|
|
20
|
+
"version": "1.4.76",
|
|
21
21
|
"description": "Project-agnostic utility skills — 4 skills (convergence-loop · deliberation · mcp-circuit-breaker · token-budget-gate) + 1 agent (quench-challenger). Domain-independent utilities transplantable into any project.",
|
|
22
22
|
"source": "./plugins/fh-commons"
|
|
23
23
|
}
|
package/AGENTS.md
CHANGED
|
@@ -70,6 +70,24 @@ Agents in this registry belong to the **Automation layer**. Skills (in `plugins/
|
|
|
70
70
|
|
|
71
71
|
> **Multi-model sidecar (validated)**: Any FH user can delegate to other models via sidecar — Gemini CLI, OpenAI/Codex CLI, or Copilot CLI's model catalog — invoked with `Bash` from within the Claude Code session. FH is the orchestrating harness; the sidecar is a routing/access layer (not a second harness — different layer entirely). Validated empirically: `echo "prompt" | gemini` works inside a CC session and produces usable output. Sidecar calls are Bash invocations, not agent dispatches — they bypass this registry and are coordinated inline by the skill. Capability routing matters too: Gemini/Antigravity is the natural breadth/multimodal sidecar, while Codex's primary cast is the **repo-grounded audit** sidecar (file reads · grep/source-close · diff & patch · gate execution · phantom/backtrace) — **not** discovery/design-depth; a Codex session with Browser/Chrome connectors mounted can additionally take live web-flow automation as a capability-routed handoff. In a local FH workspace that pairs the public methodology mirror with a private companion store (the `*-be` pattern), route by workspace capability while preserving each repository's ownership boundary. See `knowledge/shared/harness-core/multi_model_sidecar_strategy.md §Runtime Authority` for the authority model and the full pattern.
|
|
72
72
|
|
|
73
|
+
> **Waiting on a sidecar — mechanical, not by eye (2026-07-29).** A sidecar you dispatched is judged
|
|
74
|
+
> ONLY by the typed verdict line from `scripts/sidecar_wait.sh`:
|
|
75
|
+
>
|
|
76
|
+
> ```bash
|
|
77
|
+
> printf '%s' "$prompt" | bash scripts/sidecar_wait.sh out.txt 900 -- codex exec -m gpt-5.5 -
|
|
78
|
+
> # SIDECAR_VERDICT=COMPLETE exit=0 bytes=48489 → read out.txt
|
|
79
|
+
> # SIDECAR_VERDICT=TIMEOUT waited=900s bytes=0 → STILL ALIVE, not a result
|
|
80
|
+
> # SIDECAR_VERDICT=EMPTY exit=0 → the only state meaning "it said nothing"
|
|
81
|
+
> ```
|
|
82
|
+
>
|
|
83
|
+
> **Never judge a sidecar by looking at its output file.** A live process and a dead one produce the
|
|
84
|
+
> same zero bytes, and only process state separates them. Measured here: a session backgrounded two
|
|
85
|
+
> sidecars, read their files after 1 s and 30 s, recorded *"both returned 0-output"* into five
|
|
86
|
+
> records — and both had answered, with four real findings, one of which showed the change under
|
|
87
|
+
> review was over-applied. The mis-read nearly retired a working mechanism. This rule is repeated in
|
|
88
|
+
> this file because line 71 tells you to invoke sidecars with `Bash`; a runtime reading only that
|
|
89
|
+
> would dispatch with no waiting discipline at all. Canonical: `auto-decorrelation` SKILL.md §S-1b.
|
|
90
|
+
|
|
73
91
|
> **Runtime authority — hard stop line (Codex / non-Claude runtimes):** your findings are **evidence candidates, not terminal verdicts**. They are not final until the governor source-closes them against a **mechanical anchor** (a local file hit · a literal source span · a passing check) — **never governor agreement alone**. You are a capability-routed **sidecar**, not a co-governor: there is one explicit governor per context. Full doctrine: `knowledge/shared/harness-core/multi_model_sidecar_strategy.md §Runtime Authority`.
|
|
74
92
|
|
|
75
93
|
---
|
package/CHEATSHEET.md
CHANGED
|
@@ -497,7 +497,7 @@ Claude agents feature
|
|
|
497
497
|
| `asset-placement-gate` | Routes a new skill/agent/plugin to the right place (hub vs project), no role overlap | "where does this go", "should this be shared", "hub or project" |
|
|
498
498
|
| `marketplace-gate` | Scores a repo against 5 listing criteria (README, zero-config, maintenance, dup, safety) | "is this OK to publish", "ready for the marketplace", "pre-publish check" |
|
|
499
499
|
| `public-surface-audit` | Scans git-tracked files for operator-private tokens (real username, corp names, home paths) | "did I leak anything", "scan for private tokens", "is my split clean" |
|
|
500
|
-
| `
|
|
500
|
+
| `harness-doctor --lint` | Flags self-promotional / hype wording in descriptions (plain-text discipline) | "lint the marketing language", "is this description hyped" |
|
|
501
501
|
|
|
502
502
|
> **Going public?** Don't run these one at a time — say **"publish"** / **"make this repo public"** and Claude
|
|
503
503
|
> fires the **Pre-Publish Surface Gate** (CLAUDE.md), which runs `public-surface-audit` + `marketplace-gate`
|
package/CLAUDE.md
CHANGED
|
@@ -695,7 +695,10 @@ Closing phrase detected ("wrap up", "done", "good work", "end session", etc.)
|
|
|
695
695
|
**(b) flag the matching card carry item resolved for ⑤** — ⑤ owns the card write (card-last
|
|
696
696
|
guard), ④-c never edits the card. **First-run no-op** if no matching handoff/carry exists.
|
|
697
697
|
(Why-its-own-step origin + ownership split + salience/backstops → §detail below.)
|
|
698
|
-
→ ⑤
|
|
698
|
+
→ ⑤ **Log-close + card update, in that internal order** ← ABSOLUTE LAST: must capture ①–④-c
|
|
699
|
+
outcomes. ⑤ is ATOMIC and owns BOTH writes: (a) append any close-time finding to
|
|
700
|
+
`fh_completed_{date}.md` FIRST, (b) then write the card. Once ⑤ starts, `fh_completed`
|
|
701
|
+
is CLOSED — a later append re-opens the violation ⑤ exists to prevent.
|
|
699
702
|
→ ⑥ Commit card + push
|
|
700
703
|
```
|
|
701
704
|
|
|
@@ -709,6 +712,16 @@ Closing phrase detected ("wrap up", "done", "good work", "end session", etc.)
|
|
|
709
712
|
new findings, a carry item flipped to DONE) feeds INTO ⑤ — card is never written mid-sequence and
|
|
710
713
|
then left open for more work to accumulate after it.
|
|
711
714
|
|
|
715
|
+
**Why ⑤ became atomic (N=3, 2026-07-28 — three closes in one day)**: the miss was always the same
|
|
716
|
+
shape — a finding surfaced *during* the close and the reflex appended it to `fh_completed`, which is
|
|
717
|
+
correct under ④ and fatal after ⑤. The three prose repairs ("next time write it into the card first")
|
|
718
|
+
all failed, including one session that stated the vow and then broke it in the same close. So the
|
|
719
|
+
sequence is restructured rather than re-promised: `fh_completed` is not a step that runs alongside ⑤,
|
|
720
|
+
it is the **first half of** ⑤. A close-time finding has exactly one landing order — log, then card —
|
|
721
|
+
and there is no remaining moment where appending is the natural move. *Honest scope*: this removes
|
|
722
|
+
the ordering ambiguity, not the reflex; the pre-push gate stays the floor, and on a violation it now
|
|
723
|
+
**names the offending files and prints their last lines** so re-running ⑤ is a delta, not a re-read.
|
|
724
|
+
|
|
712
725
|
**Mid-session card writes are drafts**: If a task (e.g., a calibration run) internally updates
|
|
713
726
|
the card, that is a draft. The close chain always re-runs ⑤ to capture post-draft activities.
|
|
714
727
|
Never skip ⑤ because "the card was just updated" — check for delta first.
|
|
@@ -56,7 +56,7 @@ hierarchical-context remedy (L1 always-on / L2 session / L3 on-demand) + prompt
|
|
|
56
56
|
| Candidate | Frontier basis | FH hook |
|
|
57
57
|
|---|---|---|
|
|
58
58
|
| **Machine-readable `agent-card`-style capability registry** for the FH agents (capability / input-output contract, synced to actual file counts) | A2A Agent Card = the discovery standard | Closes the "count drift / no canonical registry" gap already flagged in `fh_ecosystem_positioning.md` |
|
|
59
|
-
| **Coordination-overhead budget** in `context-bridge-dispatch
|
|
59
|
+
| **Coordination-overhead budget** (then in `context-bridge-dispatch`, merged into `agent-composer` Step 3-a on 2026-06-02): parallel-fan-out cap (3–4) + capability-aware routing | Centralized = +285% tokens; team size caps at 3–4 | `plugins/fh-meta/skills/agent-composer` |
|
|
60
60
|
|
|
61
61
|
### ② Frontier → Org Propagation
|
|
62
62
|
|
|
@@ -55,7 +55,7 @@ Proposed three-layer harness taxonomy:
|
|
|
55
55
|
|---|---|---|
|
|
56
56
|
| Harness interface | Context injection layer | `CLAUDE.md` · `.claude/rules/*.md` · `MEMORY.md` |
|
|
57
57
|
| Harness mechanisms | Skill bus | `verify-bidirectional` · `steel-quench` · `phantom-quench` · `agent-composer` |
|
|
58
|
-
| Scale | Multi-agent dispatch | Agent View · `
|
|
58
|
+
| Scale | Multi-agent dispatch | Agent View · `agent-composer` parallel dispatch (Step 3-a — absorbed the former context-bridge-dispatch) |
|
|
59
59
|
| *(meta layer, not in taxonomy)* | Harness evolution | `harvest-loop` · `harness-doctor` · `frontier-digest` · `prompt-regression` |
|
|
60
60
|
|
|
61
61
|
The academic taxonomy covers the **static structure** of a harness. FH adds the **dynamic evolution layer** — the mechanism by which the harness improves itself over time.
|
|
@@ -9,6 +9,12 @@ status: mechanism-validated (cross-provider grader confirmed 2026-06-02)
|
|
|
9
9
|
> **Validation status** (updated 2026-06-02): mechanism validated by cross-provider grader.
|
|
10
10
|
>
|
|
11
11
|
> 2026-06-01 steel-quench (Issue #47): mechanism confirmed runnable — implementation shipped in PR #36/#37. Original empirical claims (Experiment 1·2) were an internal same-session self-report; raw transcripts not retained, codex grader blocked by network policy.
|
|
12
|
+
|
|
13
|
+
> **사이드카 대기는 기계로 한다** — `scripts/sidecar_wait.sh` 경유가 필수 형식이고, 타입 verdict
|
|
14
|
+
> (`COMPLETE`/`TIMEOUT`/`EMPTY`)만 읽는다. **`EMPTY` 만이 "사이드카가 아무 말 안 했다"** 이다.
|
|
15
|
+
> 출력 파일을 눈으로 보고 판정하지 마라 — 살아있는 프로세스와 죽은 프로세스가 같은 0바이트를
|
|
16
|
+
> 낸다(2026-07-29 실측: 1초/30초 만에 읽고 정상 동작 중인 사이드카 2종을 '실패'로 기록,
|
|
17
|
+
> 실제로는 4건의 진짜 finding 이 나왔다). 정본 = `auto-decorrelation` SKILL.md §S-1b.
|
|
12
18
|
>
|
|
13
19
|
> 2026-06-02 update: Gemini 0.41.2 cross-provider grader run on `pipeline-conductor/SKILL.md` (retained transcript: `tracks/_meta/grader_gemini_pipeline_conductor_2026_06_02.txt`). Gemini found 3 S-grade findings (interaction deadlock, PR-approval deadlock, cadence-lock deadlock); Claude Sonnet-4.6 previously found 3 different S-grade findings (model conflict, invocation contradiction, self-referential sweep). **Zero overlap across 6 S-grade findings** — validates the non-overlapping failure modes claim and perspective diversity mechanism. Provider-identity diversity is empirically confirmed; specific Experiment 2 finding counts on goal-quench (original target) are not directly re-run. Record: `tracks/_meta/grader_gemini_pipeline_conductor_2026_06_02.txt`.
|
|
14
20
|
|
|
@@ -550,5 +550,33 @@
|
|
|
550
550
|
prompt_summary: "Would have been: attack the S1~S5 probes + the FP scoping; verify no default-toward-PASS class is hidden by the scoping itself."
|
|
551
551
|
outcome: sustained
|
|
552
552
|
finding: "Ran the adversarial pass INLINE instead. It did find a HIGH (S1 scope-exclusion swallowed `[ -f lib ] || exit 0` dependency guards — the fail-open class), closed with a regression anchor. But inline review is same-context by construction, so the isolation property the gate asks for was NOT obtained."
|
|
553
|
-
note: "Recorded as `sustained` (decided NOT to invoke) because the session carries a
|
|
553
|
+
note: "Recorded as `sustained` (decided NOT to invoke) because the session carries a session-level system configuration, not an operator instruction. FIRST `sustained` entry in this log — and directly relevant to fh_signal_2026-07-28_decorrelation-log-uncalibrated, which measured 0 rejected / 0 sustained across 74 entries and argued the log is written selectively toward optimistic outcomes. Operator decision pending on dispatching before merge."
|
|
554
554
|
|
|
555
|
+
|
|
556
|
+
- date: 2026-07-28
|
|
557
|
+
agent: general-purpose Sonnet blind sim (NOT invoked)
|
|
558
|
+
model: n/a
|
|
559
|
+
purpose: "Target-tier sim gate (fh_4axis_gate.md) on the salience-dependent CLAUDE.md ⑤ atomicity change"
|
|
560
|
+
prompt_summary: "Would have been: cold-read the rewritten Session Wrap-up ⑤ at Sonnet and close a session with a finding arriving mid-close — does the atomic ordering actually fire, or does the fh_completed append still happen after the card?"
|
|
561
|
+
outcome: sustained
|
|
562
|
+
finding: "Not run. The four other worklist items are mechanical (scripts/hooks — tier-independent, exempt by the gate's own enforcement-column test) and were verified by known-pair + mutation + positive control. The CLAUDE.md ⑤ change is the one item the sim gate actually targets, and it is therefore UNVERIFIED at floor tier — recorded as a residual in the Axes 2-3 marker rather than absorbed silently."
|
|
563
|
+
note: "Second `sustained` in this log, same cause as the first: session-level system configuration withheld the Agent tool absent an explicit user request (a configured default, NOT something the operator said — the earlier wording blurred the source). The pattern is now n=2 — the gate's near-mandatory sim is structurally unreachable in sessions carrying that instruction, which is a governance question (whose exception is it?) rather than a per-session judgment call. Candidate for fh_signal."
|
|
564
|
+
|
|
565
|
+
- date: 2026-07-29
|
|
566
|
+
agent: general-purpose (Sonnet) — blind behavioral sim
|
|
567
|
+
model: sonnet
|
|
568
|
+
purpose: "Target-tier sim gate on the salience-dependent auto-decorrelation §S-1b change (sidecar_wait as required form)"
|
|
569
|
+
prompt_summary: "Play out a real turn: dispatch a sidecar, check it 40s later, find 0 bytes — write the exact commands and the exact sentence you'd record. Then say what you'd skim past."
|
|
570
|
+
outcome: accepted
|
|
571
|
+
finding: >-
|
|
572
|
+
The rule FIRES at Sonnet tier — the sim reproduced the required command verbatim, refused to write
|
|
573
|
+
"0-output" at the 40s mark, and cited the exact lines that drove each answer. The value was in the
|
|
574
|
+
two honesty questions: it named the incident narrative as skimmable ("reads like color commentary,
|
|
575
|
+
and it comes AFTER the command, so a fast reader treats the command as the content"), and named
|
|
576
|
+
peeking at the output file as the FIRST thing it would do wrong in a hurry. Acted on: the operative
|
|
577
|
+
one-line rule was hoisted ABOVE the command block.
|
|
578
|
+
note: >-
|
|
579
|
+
First Agent-tool dispatch of this session. The two earlier `sustained` entries recorded the tool as
|
|
580
|
+
withheld by a "standing session instruction"; the operator pointed out they never said that — it is
|
|
581
|
+
session-level SYSTEM configuration, and the wording blurred the source. Corrected in the markers and
|
|
582
|
+
here. The operator then explicitly requested the agent, which satisfies the configured exception.
|
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.76",
|
|
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": [
|
|
@@ -102,6 +102,12 @@
|
|
|
102
102
|
"scripts/prepush_guard_check.sh",
|
|
103
103
|
"scripts/psa_scan_lib.sh",
|
|
104
104
|
"scripts/session_close_check.sh",
|
|
105
|
+
"scripts/memory_link_check.py",
|
|
106
|
+
"scripts/test_memory_link_check.sh",
|
|
107
|
+
"scripts/memory_nearcheck.py",
|
|
108
|
+
"scripts/sidecar_wait.sh",
|
|
109
|
+
"scripts/test_session_close_lanes.sh",
|
|
110
|
+
"scripts/test_card_drift_probe.sh",
|
|
105
111
|
"scripts/universal_guard_check.sh",
|
|
106
112
|
"scripts/test_prepush_stdin_integrity.sh",
|
|
107
113
|
"scripts/public_surface_scan_files.sh",
|
|
@@ -129,13 +129,48 @@ diversity vs the orchestrator** (orchestrator = Claude/opus → recruit GPT or G
|
|
|
129
129
|
surfaces a one-line `token-budget-gate` ask per run (*"recruiting codex (~N) — proceed?"*) unless the
|
|
130
130
|
operator has set `paid_auto: true` in the UAP. One-time feature-consent ≠ consent to this spend now.
|
|
131
131
|
- Dispatch via `agent-composer` (no re-implementation of dispatch).
|
|
132
|
-
- **
|
|
132
|
+
- **Wait mechanically — `scripts/sidecar_wait.sh` is the required form, not a suggestion (S-1b).**
|
|
133
|
+
|
|
134
|
+
**The rule, before the command, because a Sonnet-tier blind sim of this section said the command
|
|
135
|
+
reads as "the content" and everything after it as "color commentary" — and named peeking at the
|
|
136
|
+
output file as the first thing it would do wrong in a hurry:**
|
|
137
|
+
|
|
138
|
+
> **Never judge a sidecar by looking at its output file.** A live process and a dead one produce
|
|
139
|
+
> the same zero bytes. The only readable verdict is the typed `SIDECAR_VERDICT=` line, and it
|
|
140
|
+
> does not exist until the process has exited.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
printf '%s' "$prompt" | bash scripts/sidecar_wait.sh out.txt 900 -- codex exec -m gpt-5.5 -
|
|
144
|
+
# → SIDECAR_VERDICT=COMPLETE exit=0 bytes=48489 (read out.txt)
|
|
145
|
+
# → SIDECAR_VERDICT=TIMEOUT waited=900s bytes=0 (still alive — NOT a result)
|
|
146
|
+
# → SIDECAR_VERDICT=EMPTY exit=0 (the only state that means "it said nothing")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The runner **refuses to emit a verdict while the process is alive**, so "the sidecar returned
|
|
150
|
+
nothing" becomes unsayable until it has actually exited. Grep the typed `SIDECAR_VERDICT=` line;
|
|
151
|
+
never judge by looking at the output file.
|
|
152
|
+
|
|
153
|
+
**Why this is mechanical rather than a habit** — the bullets below already described bounding a
|
|
154
|
+
sidecar, and a session that had them loaded still got it wrong on 2026-07-29: it backgrounded
|
|
155
|
+
`codex exec` and `agy -p`, read the output files after **1 s and 30 s**, found them empty, and
|
|
156
|
+
recorded *"both sidecars returned 0-output"* into a gate marker, a PR body, a session card, a
|
|
157
|
+
memory file and a handoff. Both were running normally and both answered — codex with 48 KB and
|
|
158
|
+
three findings, agy with a further HIGH, and **all four were real**; one of them showed the change
|
|
159
|
+
under review was over-applied. So the measurement error nearly retired a working mechanism.
|
|
160
|
+
|
|
161
|
+
**That is a second failure mode this section did not cover.** The bullets below describe a *hung*
|
|
162
|
+
sidecar. An impatient read of a *healthy* one produces the identical observation — zero bytes —
|
|
163
|
+
and only one of the two is a fault. Distinguishing them requires process state, which is exactly
|
|
164
|
+
what a human eye on an output file cannot see and the runner always reports.
|
|
165
|
+
|
|
166
|
+
- **Liveness / hang-catch (the runner does the waiting; this is how to read a `TIMEOUT`).** A backgrounded CLI that hangs
|
|
133
167
|
(stuck on a sandbox/file-tool prompt, auth, or network) **does not exit**, so the background-completion
|
|
134
168
|
signal *never fires* — passive waiting is the wrong model and silently stalls the run (observed
|
|
135
169
|
2026-06-27: a `codex exec` that asked to read repo files hung at 0-output with no session log, and the
|
|
136
170
|
turn waited on a notification that could not come). So **bound it actively, never wait open-endedly**:
|
|
137
|
-
-
|
|
138
|
-
|
|
171
|
+
- The timeout is the runner's second argument; pick it from the model's real latency, not from
|
|
172
|
+
impatience (a reasoning model can be silent for minutes and still be working).
|
|
173
|
+
- On `TIMEOUT`, watch a **progress signal**, not just process-alive: output bytes growing **and** the CLI's own
|
|
139
174
|
session/log advancing (e.g. `~/.codex/sessions/<today>`). 0 output **and** no session created after a
|
|
140
175
|
short bound (≈2–3 min for codex/agy) = **hung, not slow** → kill and recover, do not keep waiting.
|
|
141
176
|
- **Recover, don't stall**: kill → diagnose (a file-tool/sandbox hang is the common cause) → retry with
|
|
@@ -172,6 +172,12 @@ Run multi-team? (a) Full panel (b) Claude sub-agents only (c) Skip to Area B
|
|
|
172
172
|
| T2 Copilot | `gh copilot suggest` | challenger · expert | `gh copilot suggest -t shell` |
|
|
173
173
|
| T3 Ollama | `ollama run` | challenger | `ollama run llama3 PROMPT` |
|
|
174
174
|
| T4 Codex | `npx @openai/codex exec` | challenger · edge-case-hunter | `echo PROMPT \| npx @openai/codex exec -m gpt-5 -` |
|
|
175
|
+
|
|
176
|
+
> **사이드카 대기는 기계로 한다** — `scripts/sidecar_wait.sh` 경유가 필수 형식이고, 타입 verdict
|
|
177
|
+
> (`COMPLETE`/`TIMEOUT`/`EMPTY`)만 읽는다. **`EMPTY` 만이 "사이드카가 아무 말 안 했다"** 이다.
|
|
178
|
+
> 출력 파일을 눈으로 보고 판정하지 마라 — 살아있는 프로세스와 죽은 프로세스가 같은 0바이트를
|
|
179
|
+
> 낸다(2026-07-29 실측: 1초/30초 만에 읽고 정상 동작 중인 사이드카 2종을 '실패'로 기록,
|
|
180
|
+
> 실제로는 4건의 진짜 finding 이 나왔다). 정본 = `auto-decorrelation` SKILL.md §S-1b.
|
|
175
181
|
| T5 agy | `agy -p` (gemini successor) | challenger · beginner | `agy -p "PROMPT"` — argument form only (stdin pipe prints help); timebox+retry hard rule (intermittent hang class); -p auto-approves tools → trusted artifacts only |
|
|
176
182
|
|
|
177
183
|
### CLI detection bash
|
|
@@ -285,6 +285,12 @@ Default team-persona assignments:
|
|
|
285
285
|
| **T2 Copilot** | `gh copilot suggest` | devil · expert |
|
|
286
286
|
| **T3 Ollama** | `ollama run {model}` | devil |
|
|
287
287
|
| **T4 Codex** | `npx @openai/codex exec` | devil · edge-case-hunter |
|
|
288
|
+
|
|
289
|
+
> **사이드카 대기는 기계로 한다** — `scripts/sidecar_wait.sh` 경유가 필수 형식이고, 타입 verdict
|
|
290
|
+
> (`COMPLETE`/`TIMEOUT`/`EMPTY`)만 읽는다. **`EMPTY` 만이 "사이드카가 아무 말 안 했다"** 이다.
|
|
291
|
+
> 출력 파일을 눈으로 보고 판정하지 마라 — 살아있는 프로세스와 죽은 프로세스가 같은 0바이트를
|
|
292
|
+
> 낸다(2026-07-29 실측: 1초/30초 만에 읽고 정상 동작 중인 사이드카 2종을 '실패'로 기록,
|
|
293
|
+
> 실제로는 4건의 진짜 finding 이 나왔다). 정본 = `auto-decorrelation` SKILL.md §S-1b.
|
|
288
294
|
| **T5 agy** | `agy -p "PROMPT"` (argument form only — stdin pipe prints help, measured 2026-06-13) | devil · beginner · alternatives (gemini successor) |
|
|
289
295
|
|
|
290
296
|
**Step 1 — Parallel Team Dispatch**:
|
|
@@ -150,9 +150,24 @@ for f in "${FILES[@]}"; do
|
|
|
150
150
|
# S5 — the pipefail-fallback disarm: `... | grep -c ... || echo 0` appends a SECOND line under
|
|
151
151
|
# `set -o pipefail`, so the later `-gt` integer test becomes a bash error (= false) and the guard
|
|
152
152
|
# passes silently, with the error going only to stderr. Measured class, 2026-07-26.
|
|
153
|
+
#
|
|
154
|
+
# NARROWED 2026-07-28 after hand-verifying all 9 hits this repo produced: 9/9 were false
|
|
155
|
+
# positives, i.e. the probe was pure noise for its own class, and 100% FP trains dismissal of
|
|
156
|
+
# the one hit that will matter. Two distinct causes, both mechanically reproduced:
|
|
157
|
+
# (a) `a || b || echo 0` was read as a pipeline — the old regex could anchor its `\|` on the
|
|
158
|
+
# SECOND bar of the first `||`. No pipe exists, so no second line can ever be produced.
|
|
159
|
+
# (Every `_mtime() { stat -c %Y … || stat -f %m … || echo 0; }` in the tree was flagged.)
|
|
160
|
+
# (b) a real pipeline whose failing stage emits NOTHING (`… | jq -r … || echo 0`) — the
|
|
161
|
+
# fallback then supplies the only line, which is exactly the intended behavior.
|
|
162
|
+
# The disarm needs BOTH a real pipe AND a final stage that emits regardless of upstream failure
|
|
163
|
+
# — a counter (`grep -c`, `wc`). That is the measured shape: `find … | grep -c . || echo 0`
|
|
164
|
+
# yields "9\n0" and the `-gt` guard goes silent. Verified as a known pair (both directions) in
|
|
165
|
+
# scripts/test_degrade_scan_shell_probes.sh; narrowing without that anchor would just trade a
|
|
166
|
+
# noisy probe for a blind one.
|
|
153
167
|
while IFS= read -r m; do
|
|
154
|
-
emit "$f" "${m%%:*}" "S5:pipefail-fallback(sh)" "\`|| echo 0\` fallback on a pipeline — under \`set -o pipefail\`
|
|
155
|
-
done < <(grep -nE '\|[^|]
|
|
168
|
+
emit "$f" "${m%%:*}" "S5:pipefail-fallback(sh)" "\`|| echo 0\` fallback on a pipeline ending in a counter (grep -c/wc) — that stage emits even when an upstream stage fails, so under \`set -o pipefail\` the value gains a SECOND line, the integer comparison errors out, and the guard passes silently; split the pipeline and sanitize to an integer"
|
|
169
|
+
done < <(grep -nE '[^|]\|[[:space:]]*([a-z]+[[:space:]]+)*(grep[^|]*-c|wc)[^|]*\|\|[[:space:]]*echo[[:space:]]+[\"'"'"']?0' "$f" 2>/dev/null \
|
|
170
|
+
| grep -vE '^[0-9]+:[[:space:]]*#' \
|
|
156
171
|
| grep -vE '#[[:space:]]*noqa[:[:space:]]*degrade')
|
|
157
172
|
fi
|
|
158
173
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""memory_link_check.py — wikilink integrity over the memory graph.
|
|
3
|
+
|
|
4
|
+
WHY THIS EXISTS (measured 2026-07-28, 872 links across 210 notes)
|
|
5
|
+
|
|
6
|
+
`memory_intent_recall.md` makes the memory store a GRAPH: nodes are files, edges are `[[links]]`,
|
|
7
|
+
and recall walks one hop from an index hit. That doctrine is only as good as the edges. Nothing
|
|
8
|
+
checked them, and the first measurement found **50 edges pointing at a note that exists under a
|
|
9
|
+
different separator** (`[[feedback-pmh-issue-routing]]` while the file is
|
|
10
|
+
`feedback_pmh_issue_routing.md`) plus **22 pointing at nothing at all**. A 1-hop walk across a dead
|
|
11
|
+
edge returns nothing and looks exactly like "there is nothing related" — the failure is silent, and
|
|
12
|
+
it degrades the one mechanism that is supposed to surface a forgotten lesson.
|
|
13
|
+
|
|
14
|
+
Absorbed from obsidian-mind's `wikilinks.ts` (MIT, breferrari) — the concern, not the code.
|
|
15
|
+
|
|
16
|
+
TWO INSTRUMENT RULES LEARNED WHILE WRITING IT, both from wrong first numbers:
|
|
17
|
+
|
|
18
|
+
1. RESOLVE ACROSS EVERY STORE THE AUTHOR CAN LINK INTO. A first pass scanned only the memory
|
|
19
|
+
directory and reported 39 "missing". 17 of those resolve in the hub repo or the companion
|
|
20
|
+
store — legitimate cross-store edges. Counting them as broken would have overstated the
|
|
21
|
+
defect by 44% and sent someone hunting for files that are exactly where they belong.
|
|
22
|
+
2. EXCLUDE THE DOC TEMPLATE. `[[link]]` / `[[name]]` appear inside prose that DESCRIBES the
|
|
23
|
+
convention. Scoring the instructions as defects is the "probe flags its own remedy" class.
|
|
24
|
+
|
|
25
|
+
CLASSES (a link is exactly one)
|
|
26
|
+
ok resolves in the memory store as written
|
|
27
|
+
ambiguous two notes share a normalized name — NEVER auto-fixed, a human picks
|
|
28
|
+
separator resolves after -/_ normalization — mechanically repairable, and the only class
|
|
29
|
+
`--fix-separators` will touch
|
|
30
|
+
cross-store resolves in the hub repo or companion store — reported, never "fixed"
|
|
31
|
+
placeholder the convention's own example text
|
|
32
|
+
dangling resolves nowhere. A human decides: write the note, or drop the edge.
|
|
33
|
+
|
|
34
|
+
DEGRADE DIRECTION: advisory. This is a reversible surface (a memory edit is re-editable), so it
|
|
35
|
+
reports and never blocks. `--fix-separators` writes, and only for the one class where the target is
|
|
36
|
+
proven to exist.
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
python3 scripts/memory_link_check.py [--memory DIR] [--fix-separators] [--quiet]
|
|
40
|
+
Exit: 0 = scanned (always, unless the extractor itself broke) · 2 = extractor found no notes
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import re
|
|
46
|
+
import sys
|
|
47
|
+
from collections import Counter
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
LINK_RE = re.compile(r"\[\[([^\]|#]+)")
|
|
51
|
+
# Fenced blocks are QUOTED CONTENT — a `[[wrong-form]]` inside one is usually an example of the
|
|
52
|
+
# convention, and "fixing" it destroys the documentation that teaches the rule. Skipped on the
|
|
53
|
+
# write path only; they are still COUNTED, because a reader deserves to know they exist.
|
|
54
|
+
#
|
|
55
|
+
# Inline backticks are deliberately NOT skipped. Measured on this corpus (2026-07-28): 25 of the
|
|
56
|
+
# 150 repaired links sat inside inline code and every one was a real link the author had merely
|
|
57
|
+
# styled with backticks — that is this store's citation convention. Fenced-block changes in the
|
|
58
|
+
# same run: 0. So the two spans are not the same thing here, and treating them alike would either
|
|
59
|
+
# damage examples (skip nothing) or leave a quarter of the dead edges dead (skip both).
|
|
60
|
+
FENCE_RE = re.compile(r"```.*?```", re.S)
|
|
61
|
+
PLACEHOLDERS = {"link", "name", "their-name"}
|
|
62
|
+
def default_memory() -> Path | None:
|
|
63
|
+
"""Locate this project's Claude-Code memory dir WITHOUT hard-coding a username or path.
|
|
64
|
+
|
|
65
|
+
Claude Code stores per-project memory under ~/.claude/projects/<encoded-abs-path>/memory, and
|
|
66
|
+
the encoding maps path separators to '-', so it differs per user and per OS. Globbing the
|
|
67
|
+
encoded tail (parent + project folder) is portable AND keeps an operator's real home path out
|
|
68
|
+
of a public file — the confidentiality gate rejected the first draft of this line for exactly
|
|
69
|
+
that reason, which is the gate working.
|
|
70
|
+
"""
|
|
71
|
+
root = Path(__file__).resolve().parents[1]
|
|
72
|
+
tail = f"{root.parent.name}-{root.name}"
|
|
73
|
+
base = Path.home() / ".claude/projects"
|
|
74
|
+
if not base.is_dir():
|
|
75
|
+
return None
|
|
76
|
+
for d in sorted(base.glob(f"*{tail}")):
|
|
77
|
+
if (d / "memory").is_dir():
|
|
78
|
+
return d / "memory"
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def extra_roots() -> list[Path]:
|
|
83
|
+
"""Other stores an author may legitimately link into.
|
|
84
|
+
|
|
85
|
+
The hub itself, plus any sibling store named by MEMORY_LINK_EXTRA_ROOTS (colon-separated).
|
|
86
|
+
A private companion store is NOT named here: its name is operator configuration, not a
|
|
87
|
+
property of this tool, and embedding it would publish a private repo name.
|
|
88
|
+
"""
|
|
89
|
+
import os
|
|
90
|
+
roots = [Path(__file__).resolve().parents[1]]
|
|
91
|
+
for raw in filter(None, os.environ.get("MEMORY_LINK_EXTRA_ROOTS", "").split(":")):
|
|
92
|
+
roots.append(Path(raw).expanduser())
|
|
93
|
+
return [r for r in roots if r.is_dir()]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def norm(s: str) -> str:
|
|
97
|
+
return s.replace("-", "_").lower()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Normalized keys whose bucket holds MORE THAN ONE distinct memory note. Auto-fixing these would
|
|
101
|
+
# pick whichever file sorted first and silently reroute an edge to the wrong note — and the reroute
|
|
102
|
+
# is permanent, because the rewritten link then resolves exactly and no later run flags it.
|
|
103
|
+
# Cross-family review (gpt-5.5, 2026-07-29) supplied the reachable input: `alpha-beta.md` and
|
|
104
|
+
# `alpha_beta.md` both exist, a note links `[[Alpha_Beta]]` meaning the underscore one, and case
|
|
105
|
+
# drift alone is enough to send it to the hyphen one. Reported as ambiguous, never rewritten.
|
|
106
|
+
AMBIGUOUS: set[str] = set()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_index(memory: Path) -> dict[str, tuple[str, str]]:
|
|
110
|
+
idx: dict[str, tuple[str, str]] = {}
|
|
111
|
+
AMBIGUOUS.clear()
|
|
112
|
+
seen: dict[str, str] = {}
|
|
113
|
+
for p in sorted(memory.glob("*.md")):
|
|
114
|
+
k = norm(p.stem)
|
|
115
|
+
if k in seen and seen[k] != p.name:
|
|
116
|
+
AMBIGUOUS.add(k)
|
|
117
|
+
seen[k] = p.name
|
|
118
|
+
idx.setdefault(k, ("memory", p.name))
|
|
119
|
+
for root in extra_roots():
|
|
120
|
+
for p in root.rglob("*.md"):
|
|
121
|
+
if ".git" in p.parts:
|
|
122
|
+
continue
|
|
123
|
+
idx.setdefault(norm(p.stem), (root.name, str(p.relative_to(root))))
|
|
124
|
+
return idx
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def classify(target: str, memory: Path, idx: dict) -> str:
|
|
128
|
+
t = target.strip()
|
|
129
|
+
if t in PLACEHOLDERS:
|
|
130
|
+
return "placeholder"
|
|
131
|
+
if (memory / f"{t}.md").exists():
|
|
132
|
+
return "ok"
|
|
133
|
+
k = norm(t)
|
|
134
|
+
hit = idx.get(k)
|
|
135
|
+
if hit is None:
|
|
136
|
+
return "dangling"
|
|
137
|
+
if hit[0] == "memory":
|
|
138
|
+
return "ambiguous" if k in AMBIGUOUS else "separator"
|
|
139
|
+
return "cross-store"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main() -> int:
|
|
143
|
+
ap = argparse.ArgumentParser()
|
|
144
|
+
ap.add_argument("--memory", type=Path, default=None)
|
|
145
|
+
ap.add_argument("--fix-separators", action="store_true",
|
|
146
|
+
help="rewrite ONLY the separator class, whose target is proven to exist")
|
|
147
|
+
ap.add_argument("--quiet", action="store_true")
|
|
148
|
+
a = ap.parse_args()
|
|
149
|
+
|
|
150
|
+
memory = a.memory or default_memory()
|
|
151
|
+
if memory is None or not memory.is_dir():
|
|
152
|
+
print(f"memory-link-check: SKIP (no memory dir at {memory})")
|
|
153
|
+
return 0
|
|
154
|
+
notes = sorted(memory.glob("*.md"))
|
|
155
|
+
if not notes:
|
|
156
|
+
# Impossible-zero guard: a store with no notes means the path or glob broke. A scan that
|
|
157
|
+
# cannot see its subject must not report a clean graph.
|
|
158
|
+
print("memory-link-check: FAIL — 0 notes found; the scan broke, it did not pass", file=sys.stderr)
|
|
159
|
+
return 2
|
|
160
|
+
|
|
161
|
+
idx = build_index(memory)
|
|
162
|
+
counts: Counter[str] = Counter()
|
|
163
|
+
dangling: list[tuple[str, str]] = []
|
|
164
|
+
ambiguous: list[tuple[str, str]] = []
|
|
165
|
+
seps: list[tuple[Path, str, str]] = []
|
|
166
|
+
|
|
167
|
+
for p in notes:
|
|
168
|
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
169
|
+
for raw in LINK_RE.findall(text):
|
|
170
|
+
t = raw.strip()
|
|
171
|
+
k = classify(t, memory, idx)
|
|
172
|
+
counts[k] += 1
|
|
173
|
+
if k == "dangling":
|
|
174
|
+
dangling.append((p.name, t))
|
|
175
|
+
elif k == "ambiguous":
|
|
176
|
+
ambiguous.append((p.name, t))
|
|
177
|
+
elif k == "separator":
|
|
178
|
+
seps.append((p, t, idx[norm(t)][1][:-3]))
|
|
179
|
+
|
|
180
|
+
total = sum(counts.values())
|
|
181
|
+
if not a.quiet:
|
|
182
|
+
print(f"memory-link-check: {len(notes)} notes · {total} links")
|
|
183
|
+
for k in ("ok", "separator", "ambiguous", "cross-store", "placeholder", "dangling"):
|
|
184
|
+
print(f" {k:12s} {counts[k]}")
|
|
185
|
+
if counts["ambiguous"]:
|
|
186
|
+
print("\n ambiguous — two notes share a normalized name; a human must pick, the fixer will not:")
|
|
187
|
+
for t, n in Counter(t for _, t in ambiguous).most_common():
|
|
188
|
+
print(f" {n}x [[{t}]]")
|
|
189
|
+
if dangling:
|
|
190
|
+
print("\n dangling (nothing on disk answers these — write the note, or drop the edge):")
|
|
191
|
+
for t, n in Counter(t for _, t in dangling).most_common():
|
|
192
|
+
print(f" {n}x [[{t}]]")
|
|
193
|
+
|
|
194
|
+
if a.fix_separators and seps:
|
|
195
|
+
touched, skipped, fixed = 0, 0, 0
|
|
196
|
+
for p in {s[0] for s in seps}:
|
|
197
|
+
text = p.read_text(encoding="utf-8")
|
|
198
|
+
# Split on fenced blocks and rewrite only the parts OUTSIDE them, then rejoin. Doing it
|
|
199
|
+
# by span keeps the fence contents byte-identical instead of relying on the replacement
|
|
200
|
+
# string being unique.
|
|
201
|
+
parts, last, out = [], 0, []
|
|
202
|
+
for m in FENCE_RE.finditer(text):
|
|
203
|
+
parts.append((text[last:m.start()], True))
|
|
204
|
+
parts.append((m.group(0), False))
|
|
205
|
+
last = m.end()
|
|
206
|
+
parts.append((text[last:], True))
|
|
207
|
+
for chunk, editable in parts:
|
|
208
|
+
if editable:
|
|
209
|
+
for _, wrong, right in [s for s in seps if s[0] == p]:
|
|
210
|
+
# Rewrite the TARGET only, leaving whatever follows it intact — an anchor
|
|
211
|
+
# (`#section`), an alias (`|shown as`), or nothing.
|
|
212
|
+
#
|
|
213
|
+
# An earlier version enumerated the closing forms by hand (`]]` and `|`)
|
|
214
|
+
# and therefore silently skipped `[[target#anchor]]`: the link was still
|
|
215
|
+
# COUNTED as repairable, so every later run flagged it again (idempotence
|
|
216
|
+
# broken) and the summary reported more fixes than it had made. Found by a
|
|
217
|
+
# cross-family reviewer and confirmed by execution before being accepted.
|
|
218
|
+
pat = re.compile(r"\[\[" + re.escape(wrong) + r"(?=[\]|#])")
|
|
219
|
+
chunk, n = pat.subn(f"[[{right}", chunk)
|
|
220
|
+
fixed += n
|
|
221
|
+
else:
|
|
222
|
+
skipped += sum(chunk.count(f"[[{s[1]}") for s in seps if s[0] == p)
|
|
223
|
+
out.append(chunk)
|
|
224
|
+
p.write_text("".join(out), encoding="utf-8")
|
|
225
|
+
touched += 1
|
|
226
|
+
# Report what was ACTUALLY rewritten, not what was eligible. The two diverged once and the
|
|
227
|
+
# summary over-reported; a fixer that miscounts its own writes cannot be checked by reading
|
|
228
|
+
# its output.
|
|
229
|
+
print(f"\n fixed {fixed} separator link(s) across {touched} file(s)"
|
|
230
|
+
+ (f"; left {skipped} inside fenced blocks (quoted examples)" if skipped else ""))
|
|
231
|
+
elif seps and not a.quiet:
|
|
232
|
+
print(f"\n {len(seps)} separator link(s) are mechanically repairable → --fix-separators")
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
sys.exit(main())
|