> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 05-1 OpenClaw 解剖：一套開源 agent 外殼怎麼運轉

> OpenClaw 把 agent 的人格、記憶、心跳、工具慣例用一組 Markdown 檔組裝起來，讓 agent 在每次 session 啟動時「讀自己的靈魂」才開始工作。本單元從倉庫實際檔案出發，解剖它的 Gateway 執行迴圈、heartbeat 排程、記憶系統與各自訂 md 檔的確切職責。

export const AgentLoop = ({lang = "zh", title, phases = []}) => {
  const L = lang === "en" ? {
    play: "Play",
    pause: "Pause",
    step: "Step",
    reset: "Reset",
    replay: "Replay",
    loop: "Agent Loop",
    hint: "Click a phase or press Play",
    phase: "Phase",
    substeps: "Steps"
  } : {
    play: "播放",
    pause: "暫停",
    step: "下一步",
    reset: "重設",
    replay: "重新播放",
    loop: "執行迴圈",
    hint: "點階段或按播放逐步走",
    phase: "階段",
    substeps: "步驟"
  };
  const PALETTE = {
    blue: {
      base: "#5b6c8f",
      bg: "#5b6c8f18",
      border: "#5b6c8f55",
      label: "#46587a",
      dot: "#5b6c8f"
    },
    purple: {
      base: "#8a6f8e",
      bg: "#8a6f8e18",
      border: "#8a6f8e55",
      label: "#6f5673",
      dot: "#8a6f8e"
    },
    yellow: {
      base: "#c9a35b",
      bg: "#c9a35b18",
      border: "#c9a35b55",
      label: "#9a7a3c",
      dot: "#c9a35b"
    },
    green: {
      base: "#8a9a7b",
      bg: "#8a9a7b18",
      border: "#8a9a7b55",
      label: "#6a7a5c",
      dot: "#8a9a7b"
    },
    teal: {
      base: "#6f9290",
      bg: "#6f929018",
      border: "#6f929055",
      label: "#527472",
      dot: "#6f9290"
    },
    rose: {
      base: "#ab7269",
      bg: "#ab726918",
      border: "#ab726955",
      label: "#8e564e",
      dot: "#ab7269"
    },
    amber: {
      base: "#b9835c",
      bg: "#b9835c18",
      border: "#b9835c55",
      label: "#96683f",
      dot: "#b9835c"
    }
  };
  const ROTATION = ["blue", "purple", "yellow", "green", "teal", "rose"];
  const getColor = (phase, index) => {
    const key = phase.color && PALETTE[phase.color] ? phase.color : ROTATION[index % ROTATION.length];
    return PALETTE[key];
  };
  const safe = Array.isArray(phases) && phases.length > 0 ? phases : [{
    id: "perceive",
    label: lang === "en" ? "Perceive" : "感知",
    icon: "eye",
    color: "blue",
    detail: lang === "en" ? "Collect inbound signals: user messages, heartbeat ticks, cron events, tool callbacks. Assemble workspace context (persona, memory, instructions)." : "收集入站訊號：使用者訊息、heartbeat tick、cron 事件、工具回傳。組裝工作區上下文（人格、記憶、指令）。"
  }, {
    id: "plan",
    label: lang === "en" ? "Plan" : "規劃",
    icon: "brain",
    color: "purple",
    detail: lang === "en" ? "Model reasons over the context. Decides what action to take, which tools to call, and in what order. May produce a structured tool-call sequence." : "模型對上下文進行推理。決定要採取什麼行動、呼叫哪些工具、以何種順序執行，可能產出結構化的 tool-call 序列。"
  }, {
    id: "act",
    label: lang === "en" ? "Act" : "執行",
    icon: "zap",
    color: "yellow",
    detail: lang === "en" ? "Execute the chosen tools within permission boundaries. May dispatch sub-agents for parallel work. Results flow back as observations." : "在權限邊界內執行選定的工具。可能派出子代理進行並行工作。執行結果作為觀察回流。"
  }, {
    id: "observe",
    label: lang === "en" ? "Observe" : "觀察",
    icon: "check",
    color: "green",
    detail: lang === "en" ? "Evaluate tool results and agent output. Distill meaningful facts into memory. Decide whether to loop again or terminate this turn." : "評估工具執行結果與代理輸出。將有價值的事實精煉進記憶層。決定是否再次循環或結束本輪。"
  }];
  const n = safe.length;
  const [current, setCurrent] = useState(0);
  const [running, setRunning] = useState(false);
  const [done, setDone] = useState(false);
  useEffect(() => {
    if (!running || done) return;
    const id = setTimeout(() => {
      const next = current + 1;
      if (next >= n) {
        setRunning(false);
        setDone(true);
      } else {
        setCurrent(next);
      }
    }, 1400);
    return () => clearTimeout(id);
  }, [running, done, current, n]);
  const reset = () => {
    setRunning(false);
    setDone(false);
    setCurrent(0);
  };
  const playPause = () => {
    if (done) {
      reset();
      setRunning(true);
      return;
    }
    setRunning(!running);
  };
  const step = () => {
    setRunning(false);
    if (done) return;
    const next = current + 1;
    if (next >= n) {
      setDone(true);
    } else {
      setCurrent(next);
    }
  };
  const curPhase = safe[current] || safe[0];
  const curColor = getColor(curPhase, current);
  const iconPaths = {
    eye: <>
      <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z" />
      <circle cx="12" cy="12" r="3" />
    </>,
    brain: <>
      <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 4.44-1.06z" />
      <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-4.44-1.06z" />
    </>,
    zap: <>
      <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
    </>,
    check: <>
      <path d="M20 6 9 17l-5-5" />
    </>,
    layers: <>
      <path d="m12 2 10 6.5v7L12 22 2 15.5v-7L12 2z" />
      <path d="M12 22v-6.5" />
      <path d="M22 8.5 12 15.5 2 8.5" />
      <path d="M2 15.5 12 9l10 6.5" />
    </>,
    repeat: <>
      <path d="m17 2 4 4-4 4" />
      <path d="M3 11V9a4 4 0 0 1 4-4h14" />
      <path d="m7 22-4-4 4-4" />
      <path d="M21 13v2a4 4 0 0 1-4 4H3" />
    </>
  };
  const PhaseIcon = ({id, size = 18, strokeColor}) => {
    const path = iconPaths[id] || iconPaths.repeat;
    return <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={strokeColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{
      transition: "stroke 0.2s"
    }}>
        {path}
      </svg>;
  };
  const PlayIcon = () => <svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" stroke="none">
      <path d="M7 5.5v13l11-6.5z" />
    </svg>;
  const PauseIcon = () => <svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" stroke="none">
      <rect x="6.5" y="5" width="4" height="14" rx="1" />
      <rect x="13.5" y="5" width="4" height="14" rx="1" />
    </svg>;
  const StepFwdIcon = () => <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M5 4l10 8-10 8z" />
      <line x1="19" y1="5" x2="19" y2="19" />
    </svg>;
  const ResetIcon = () => <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
      <path d="M3 3v5h5" />
    </svg>;
  const css = `
.al-root{--al-bg:#FAF8F3;--al-surface2:rgba(0,0,0,0.022);
  --al-border:rgba(0,0,0,0.08);--al-text:#2b2722;--al-dim:#6f6a62;--al-faint:#9a9490;
  --al-accent:#bf7551;--al-accent-l:#cf8a68;--al-bar-bg:linear-gradient(#FAF8F3,#f5f1ea);
  border:1px solid var(--al-border);border-radius:14px;overflow:hidden;background:var(--al-bg);
  color:var(--al-text);font-family:inherit;}
.dark .al-root{--al-bg:#1b1a18;--al-surface2:rgba(255,255,255,0.025);
  --al-border:rgba(255,255,255,0.07);--al-text:#e7e3da;--al-dim:#a8a299;--al-faint:#6a6560;
  --al-accent:#cf8a68;--al-accent-l:#e0a080;--al-bar-bg:#1b1a18;}
.al-bar{display:flex;align-items:center;gap:8px;padding:10px 16px;
  background:var(--al-bar-bg);border-bottom:1px solid var(--al-border);flex-wrap:wrap;}
.al-bar-title{font-size:12px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  color:var(--al-accent);flex:1;min-width:80px;}
.al-ctrl{display:flex;align-items:center;gap:5px;}
.al-btn{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--al-border);
  background:transparent;color:var(--al-dim);border-radius:8px;padding:4px 10px;
  font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;line-height:1;font-family:inherit;}
.al-btn:hover{border-color:var(--al-accent);color:var(--al-accent);
  background:rgba(191,117,81,0.08);}
.al-btn:disabled{opacity:.35;cursor:default;}
.al-btn--primary{background:var(--al-accent);border-color:var(--al-accent);color:#fff;}
.al-btn--primary:hover{background:var(--al-accent-l);color:#fff;}
.dark .al-btn--primary{background:var(--al-accent-l);border-color:var(--al-accent-l);}
.al-progress{display:flex;align-items:center;gap:4px;margin-left:4px;}
.al-dot{width:6px;height:6px;border-radius:50%;background:var(--al-border);
  transition:background .25s,transform .25s;}
.al-dot.al-dot-done{opacity:0.55;}
.al-dot.al-dot-cur{transform:scale(1.4);}
.al-phases{display:flex;align-items:stretch;padding:14px 14px 10px;gap:8px;overflow-x:auto;}
.al-phase-btn{flex:1 1 0;min-width:80px;display:flex;flex-direction:column;align-items:center;
  gap:7px;padding:10px 8px;border-radius:10px;border:1px solid transparent;
  background:transparent;cursor:pointer;transition:background .15s,border-color .15s;
  font-family:inherit;color:inherit;}
.al-phase-icon{width:38px;height:38px;border-radius:50%;display:flex;align-items:center;
  justify-content:center;border:2px solid var(--al-border);background:var(--al-bg);
  transition:border-color .2s,background .2s;}
.al-phase-label{font-size:12px;font-weight:600;color:var(--al-dim);text-align:center;
  transition:color .15s;}
.al-connector{display:flex;align-items:center;padding-top:19px;color:var(--al-faint);}
.al-connector-last{display:flex;align-items:center;padding-top:19px;opacity:.5;}
.al-detail{border-top:1px solid var(--al-border);padding:14px 18px;background:var(--al-surface2);}
.al-detail-tag{font-size:10.5px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  margin-bottom:5px;}
.al-detail-label{font-size:16px;font-weight:700;margin-bottom:8px;}
.al-detail-text{font-size:13.5px;line-height:1.65;color:var(--al-dim);}
.al-substeps{margin-top:10px;display:flex;flex-direction:column;gap:4px;}
.al-substep{display:flex;align-items:flex-start;gap:8px;font-size:13px;color:var(--al-dim);}
.al-substep-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;margin-top:6px;}
.al-hint{padding:6px 16px;font-size:11.5px;color:var(--al-faint);
  border-top:1px solid var(--al-border);background:var(--al-bg);}
@media (max-width:540px){
  .al-phases{flex-direction:column;overflow-x:visible;}
  .al-connector{display:none;}
  .al-connector-last{display:none;}
  .al-phase-btn{flex-direction:row;align-items:center;gap:10px;padding:8px 12px;text-align:left;}
  .al-phase-label{text-align:left;}
}
`;
  const ArrowRight = ({color}) => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color || "currentColor"} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{
    opacity: 0.45
  }}>
      <path d="M5 12h14M12 5l7 7-7 7" />
    </svg>;
  const LoopArrow = ({color}) => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color || "currentColor"} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <path d="m17 2 4 4-4 4" />
      <path d="M3 11V9a4 4 0 0 1 4-4h14" />
      <path d="m7 22-4-4 4-4" />
      <path d="M21 13v2a4 4 0 0 1-4 4H3" />
    </svg>;
  return <div className="al-root">
      <style>{css}</style>

      <div className="al-bar">
        <span className="al-bar-title">{title || L.loop}</span>
        <div className="al-ctrl">
          <button className="al-btn al-btn--primary" onClick={playPause}>
            {running ? <PauseIcon /> : <PlayIcon />}
            {done ? L.replay : running ? L.pause : L.play}
          </button>
          <button className="al-btn" onClick={step} disabled={done}>
            <StepFwdIcon />{L.step}
          </button>
          <button className="al-btn" onClick={reset}>
            <ResetIcon />{L.reset}
          </button>
        </div>
        <div className="al-progress">
          {safe.map((ph, i) => {
    const pc = getColor(ph, i);
    const isDone = i < current || done;
    const isCur = i === current && !done;
    return <div key={ph.id || i} className={"al-dot" + (isDone ? " al-dot-done" : "") + (isCur ? " al-dot-cur" : "")} style={{
      background: isDone || isCur ? pc.base : ""
    }} />;
  })}
        </div>
      </div>

      <div className="al-phases">
        {}
        {safe.flatMap((ph, i) => {
    const isCur = i === current && !done;
    const pc = getColor(ph, i);
    const key = ph.id || String(i);
    const nextPc = i < safe.length - 1 ? getColor(safe[i + 1], i + 1) : pc;
    return [<button key={key + "-btn"} type="button" className="al-phase-btn" style={isCur ? {
      background: pc.bg,
      borderColor: pc.base
    } : {}} onMouseEnter={e => {
      if (!isCur) {
        e.currentTarget.style.background = pc.bg;
        e.currentTarget.style.borderColor = pc.border;
      }
    }} onMouseLeave={e => {
      if (!isCur) {
        e.currentTarget.style.background = "";
        e.currentTarget.style.borderColor = "";
      }
    }} onClick={() => {
      setRunning(false);
      setDone(false);
      setCurrent(i);
    }}>
              <div className="al-phase-icon" style={isCur ? {
      borderColor: pc.base,
      background: pc.bg
    } : {}}>
                <PhaseIcon id={ph.icon || "repeat"} size={18} strokeColor={isCur ? pc.label : "var(--al-dim)"} />
              </div>
              <div className="al-phase-label" style={isCur ? {
      color: pc.label
    } : {}}>{ph.label}</div>
            </button>, i < safe.length - 1 ? <div key={key + "-conn"} className="al-connector"><ArrowRight color={nextPc.base} /></div> : <div key={key + "-conn"} className="al-connector-last" style={{
      color: pc.base
    }}><LoopArrow color={pc.base} /></div>];
  })}
      </div>

      <div className="al-detail">
        <div className="al-detail-tag" style={{
    color: curColor.label
  }}>{L.phase} {current + 1} / {n}{done ? " — " + (lang === "en" ? "cycle complete" : "一輪完成") : ""}</div>
        <div className="al-detail-label">{curPhase.label}</div>
        <div className="al-detail-text">{curPhase.detail}</div>
        {curPhase.substeps && curPhase.substeps.length > 0 && <div className="al-substeps">
            {curPhase.substeps.map((s, i) => <div className="al-substep" key={i}>
                <div className="al-substep-dot" style={{
    background: curColor.base
  }} />
                <span>{s}</span>
              </div>)}
          </div>}
      </div>

      <div className="al-hint">{L.hint}</div>
    </div>;
};

export const FrameworkMap = ({lang = "zh", title, layers = []}) => {
  const t = lang === "en" ? {
    hint: "Click a module to see details",
    layer: "Layer",
    module: "Module",
    responsibilities: "Responsibilities",
    path: "Key path",
    relations: "Connected to",
    noSelect: "Select a module above"
  } : {
    hint: "點模組卡片查看職責與關係",
    layer: "層",
    module: "模組",
    responsibilities: "職責",
    path: "關鍵路徑",
    relations: "連結到",
    noSelect: "點上方模組查看詳細說明"
  };
  const [selected, setSelected] = useState(null);
  const safe = Array.isArray(layers) ? layers : [];
  const PALETTE = {
    blue: {
      base: "#5b6c8f",
      bg: "#5b6c8f18",
      border: "#5b6c8f55",
      label: "#46587a"
    },
    purple: {
      base: "#8a6f8e",
      bg: "#8a6f8e18",
      border: "#8a6f8e55",
      label: "#6f5673"
    },
    yellow: {
      base: "#c9a35b",
      bg: "#c9a35b18",
      border: "#c9a35b55",
      label: "#9a7a3c"
    },
    green: {
      base: "#8a9a7b",
      bg: "#8a9a7b18",
      border: "#8a9a7b55",
      label: "#6a7a5c"
    },
    teal: {
      base: "#6f9290",
      bg: "#6f929018",
      border: "#6f929055",
      label: "#527472"
    },
    rose: {
      base: "#ab7269",
      bg: "#ab726918",
      border: "#ab726955",
      label: "#8e564e"
    },
    amber: {
      base: "#b9835c",
      bg: "#b9835c18",
      border: "#b9835c55",
      label: "#96683f"
    }
  };
  const ROTATION = ["blue", "purple", "yellow", "green", "teal", "rose"];
  const getColor = (layer, index) => {
    const key = layer.color && PALETTE[layer.color] ? layer.color : ROTATION[index % ROTATION.length];
    return PALETTE[key];
  };
  const sel = (() => {
    if (!selected) return null;
    for (let li = 0; li < safe.length; li++) {
      const layer = safe[li];
      const m = layer.modules.find(m => m.id === selected);
      if (m) return {
        module: m,
        layerLabel: layer.label,
        color: getColor(layer, li)
      };
    }
    return null;
  })();
  const css = `
.fm-root{--fm-bg:#FAF8F3;--fm-surface2:rgba(0,0,0,0.022);
  --fm-border:rgba(0,0,0,0.08);--fm-text:#2b2722;--fm-dim:#6f6a62;
  --fm-faint:#9a9490;--fm-accent:#bf7551;--fm-card-bg:#fff;
  border:1px solid var(--fm-border);border-radius:14px;background:var(--fm-bg);
  color:var(--fm-text);overflow:hidden;font-family:inherit;}
.dark .fm-root{--fm-bg:#1b1a18;--fm-surface2:rgba(255,255,255,0.025);
  --fm-border:rgba(255,255,255,0.07);--fm-text:#e7e3da;
  --fm-dim:#a8a299;--fm-faint:#6a6560;--fm-card-bg:rgba(255,255,255,0.04);}
.fm-head{padding:13px 18px 11px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--fm-border);flex-wrap:wrap;}
.fm-head-ic{color:var(--fm-accent);flex-shrink:0;}
.fm-head-title{font-size:14px;font-weight:700;letter-spacing:0.01em;flex:1;}
.fm-head-hint{font-size:11.5px;color:var(--fm-faint);}
.fm-layers{padding:14px 16px 10px;display:flex;flex-direction:column;gap:12px;}
.fm-layer{}
.fm-layer-label{font-size:10.5px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  margin-bottom:7px;display:flex;align-items:center;gap:6px;}
.fm-layer-label::after{content:"";flex:1;height:1px;opacity:0.4;}
.fm-modules{display:flex;flex-wrap:wrap;gap:8px;}
.fm-module{display:flex;flex-direction:column;gap:3px;padding:9px 13px;border-radius:9px;
  border:1px solid var(--fm-border);background:var(--fm-card-bg);cursor:pointer;
  transition:border-color .15s,background .15s,box-shadow .15s;text-align:left;
  min-width:120px;max-width:220px;flex:1 1 120px;}
.fm-mod-name{font-size:13px;font-weight:600;line-height:1.3;color:var(--fm-text);}
.fm-mod-summary{font-size:11.5px;line-height:1.5;color:var(--fm-dim);}
.fm-mod-path{font-size:10.5px;font-family:ui-monospace,"Cascadia Code","Consolas",monospace;
  color:var(--fm-faint);margin-top:2px;word-break:break-all;}
.fm-arrow{display:flex;justify-content:center;padding:2px 0;}
.fm-panel{border-top:1px solid var(--fm-border);padding:14px 18px;background:var(--fm-surface2);min-height:80px;}
.fm-panel-empty{display:flex;align-items:center;justify-content:center;height:60px;
  font-size:12.5px;color:var(--fm-faint);}
.fm-panel-layer{font-size:10.5px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  margin-bottom:5px;}
.fm-panel-name{font-size:16px;font-weight:700;margin-bottom:6px;line-height:1.3;}
.fm-panel-path{font-size:11px;font-family:ui-monospace,"Cascadia Code","Consolas",monospace;
  color:var(--fm-faint);margin-bottom:10px;word-break:break-all;}
.fm-panel-sec{font-size:10.5px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;
  color:var(--fm-dim);margin-bottom:5px;margin-top:10px;}
.fm-panel-detail{font-size:13.5px;line-height:1.65;color:var(--fm-dim);}
.fm-rels{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;}
.fm-rel{font-size:11.5px;padding:3px 9px;border-radius:20px;}
@media (max-width:600px){
  .fm-modules{flex-direction:column;}
  .fm-module{max-width:100%;}
  .fm-head-hint{display:none;}
}
`;
  return <div className="fm-root">
      <style>{css}</style>
      <div className="fm-head">
        <svg className="fm-head-ic" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="2" y="3" width="20" height="5" rx="2" />
          <rect x="2" y="10" width="20" height="5" rx="2" />
          <rect x="2" y="17" width="20" height="5" rx="2" />
        </svg>
        {title && <span className="fm-head-title">{title}</span>}
        <span className="fm-head-hint">{t.hint}</span>
      </div>

      <div className="fm-layers">
        {safe.map((layer, li) => {
    const c = getColor(layer, li);
    return <div className="fm-layer" key={layer.id || li}>
              <div className="fm-layer-label" style={{
      color: c.label
    }}>
                <span>{layer.label}</span>
                <span style={{
      flex: 1,
      height: "1px",
      background: c.border,
      display: "block"
    }} />
              </div>
              <div className="fm-modules">
                {(layer.modules || []).map(mod => {
      const isActive = selected === mod.id;
      return <button key={mod.id} type="button" className="fm-module" style={isActive ? {
        borderColor: c.base,
        background: c.bg,
        boxShadow: "0 0 0 3px " + c.base + "22"
      } : {}} onMouseEnter={e => {
        if (!isActive) {
          e.currentTarget.style.borderColor = c.base;
          e.currentTarget.style.background = c.bg;
        }
      }} onMouseLeave={e => {
        if (!isActive) {
          e.currentTarget.style.borderColor = "";
          e.currentTarget.style.background = "";
        }
      }} onClick={() => setSelected(selected === mod.id ? null : mod.id)}>
                      <div className="fm-mod-name" style={isActive ? {
        color: c.label
      } : {}}>{mod.name}</div>
                      {mod.summary && <div className="fm-mod-summary">{mod.summary}</div>}
                      {mod.path && <div className="fm-mod-path">{mod.path}</div>}
                    </button>;
    })}
              </div>
              {li < safe.length - 1 && <div className="fm-arrow">
                  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={getColor(safe[li + 1] || layer, li + 1).base} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{
      opacity: 0.5
    }}>
                    <path d="M12 5v14M5 15l7 7 7-7" />
                  </svg>
                </div>}
            </div>;
  })}
      </div>

      <div className="fm-panel">
        {sel ? <div>
            <div className="fm-panel-layer" style={{
    color: sel.color.label
  }}>{t.layer}: {sel.layerLabel}</div>
            <div className="fm-panel-name">{sel.module.name}</div>
            {sel.module.path && <div className="fm-panel-path">{sel.module.path}</div>}
            {sel.module.detail && <div>
                <div className="fm-panel-sec">{t.responsibilities}</div>
                <div className="fm-panel-detail">{sel.module.detail}</div>
              </div>}
            {sel.module.relations && sel.module.relations.length > 0 && <div>
                <div className="fm-panel-sec">{t.relations}</div>
                <div className="fm-rels">
                  {sel.module.relations.map((r, i) => <span className="fm-rel" key={i} style={{
    background: sel.color.bg,
    color: sel.color.label,
    border: "1px solid " + sel.color.border
  }}>{r.to}{r.label ? " — " + r.label : ""}</span>)}
                </div>
              </div>}
          </div> : <div className="fm-panel-empty">{t.noSelect}</div>}
      </div>
    </div>;
};

export const LearnerPrimer = ({items = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    title: "Before this unit, be honest with yourself",
    sub: "If you can't answer these, that gap is exactly what this unit closes."
  } : {
    title: "讀這個單元前，先誠實面對",
    sub: "這幾題答不出來，正是這個單元要替你補的洞。"
  };
  const css = `
  .lp-root{--lp-bg:#FAF7F1;--lp-surface:rgba(191,117,81,0.05);--lp-border:rgba(0,0,0,0.09);--lp-edge:rgba(191,117,81,0.42);--lp-text:#2b2722;--lp-dim:#6f6a62;--lp-accent:#bf7551;border:1px solid var(--lp-border);border-left:3px solid var(--lp-edge);border-radius:13px;background:var(--lp-bg);color:var(--lp-text);overflow:hidden;margin:1.25rem 0;}
  .dark .lp-root{--lp-bg:#1b1a18;--lp-surface:rgba(207,138,104,0.07);--lp-border:rgba(255,255,255,0.08);--lp-edge:rgba(207,138,104,0.5);--lp-text:#e7e3da;--lp-dim:#a8a299;--lp-accent:#cf8a68;}
  .lp-head{display:flex;align-items:center;gap:9px;padding:13px 18px 11px;border-bottom:1px solid var(--lp-border);background:var(--lp-surface);}
  .lp-ic{color:var(--lp-accent);flex-shrink:0;}
  .lp-htx{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .lp-title{font-size:14px;font-weight:650;line-height:1.3;letter-spacing:.01em;}
  .lp-sub{font-size:12px;color:var(--lp-dim);line-height:1.4;}
  .lp-list{list-style:none;margin:0;padding:10px 18px 14px;display:flex;flex-direction:column;gap:0;}
  .lp-item{display:flex;align-items:baseline;gap:11px;padding:7px 0;font-size:14px;line-height:1.6;border-top:1px solid var(--lp-border);}
  .lp-item:first-child{border-top:none;}
  .lp-mark{flex-shrink:0;color:var(--lp-accent);font-size:13px;font-weight:700;line-height:1.55;font-variant-numeric:tabular-nums;opacity:.85;}
  .lp-q{flex:1 1 0;min-width:0;color:var(--lp-text);}
  @media (max-width:620px){.lp-head{padding:12px 14px 10px;}.lp-list{padding:8px 14px 12px;}}
  `;
  return <div className="lp-root">
      <style>{css}</style>
      <div className="lp-head">
        <svg className="lp-ic" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></svg>
        <span className="lp-htx">
          <span className="lp-title">{t.title}</span>
          <span className="lp-sub">{t.sub}</span>
        </span>
      </div>
      <ul className="lp-list">
        {items.map((q, i) => <li className="lp-item" key={i}>
            <span className="lp-mark">{String(i + 1).padStart(2, "0")}</span>
            <span className="lp-q">{q}</span>
          </li>)}
      </ul>
    </div>;
};

<LearnerPrimer
  lang="zh"
  items={[
"OpenClaw 是個人 daemon，不是對話框 UI。",
"SOUL.md 是「我是誰」，USER.md 是「你是誰」。",
"共享群組用 main session 預設，陌生人就拿到全主機權限。",
"BOOTSTRAP.md 不刪除，每次啟動都重走首次設定。",
"MEMORY.md 是精煉層不是日誌，詳細記錄寫進 memory/YYYY-MM-DD.md。",
"heartbeat 留空就跳過模型呼叫。",
"SOUL.md 寫成企業手冊，token 上升遵從度下降。",
]}
/>

<Info>
  **這個單元解決什麼問題**

  OpenClaw 把 agent 的人格、記憶、心跳、工具慣例用一組存在工作區的 Markdown 檔組裝起來，讓 agent 在每次 session 啟動時「讀自己的靈魂」才開始工作。本單元從倉庫實際檔案出發，解剖它的由來與定位、Gateway 執行迴圈、心跳（heartbeat）排程、記憶系統，以及各自訂 md 檔的確切職責；無法從倉庫直接確認的細節以官方文件為準。讀完這個單元，你能把 OpenClaw 的架構說清楚，並判斷它的設計思路是否適合你自己的 harness 建設。
</Info>

## 學習目標

* [ ] 能說清 OpenClaw 的由來（Warelay、Clawdbot、Moltbot、OpenClaw）與它想解決的問題：讓 agent 在使用者已在用的通訊管道上「真的做事」
* [ ] 能描述 Gateway 控制平面的角色，以及 heartbeat 排程如何觸發定期 agent turn
* [ ] 能逐一說出工作區標準 md 檔（AGENTS.md、BOOTSTRAP.md、HEARTBEAT.md、IDENTITY.md、MEMORY.md、SOUL.md、TOOLS.md、USER.md）各自的職責與載入時機
* [ ] 能用一句生活比喻概括這個系統的本質
* [ ] 能評估 OpenClaw 的「harness 可視化」設計是否能套到你自己的 agent 設定

***

## 1. 由來與定位

OpenClaw 不是橫空出世。它有一段可追溯的演化路徑，名字本身就是這個故事：

```mermaid theme={null}
graph LR
    A["Warelay<br/>2025-11 首個公開名"] --> B["Clawdbot<br/>廣為人知的名稱"]
    B --> C["Moltbot<br/>2026-01-27 因商標改名"]
    C --> D["OpenClaw<br/>2026-01-30 起為主線"]
```

這條命名軸記錄的是一段快速更名史，而非漫長演化：作者 Peter Steinberger 以 Warelay 之名首次發布（2025-11），以 Clawdbot 廣為人知，2026-01-27 因 Anthropic 商標投訴改名 Moltbot，三天後（2026-01-30）再改為 OpenClaw 並在 GitHub 開源（[VISION.md](https://github.com/openclaw/openclaw/blob/main/VISION.md) 截至 2026-05）。

它要解決的問題很直白：絕大多數 AI 助理都要求使用者「切到新介面」，而 OpenClaw 反過來，要在使用者既有的通訊管道（WhatsApp、Telegram、Slack、Discord 等 20+ 個平台）上直接執行真實任務。換句話說，它不是另一個對話框 UI，而是一個**貼在使用者既有對話流的個人助理 daemon**。

定位上屬於「local-first 個人 agent harness」：以 TypeScript 寫成，理由是「廣為人知、迭代快、易讀易改」這三個工程圈的共識（[VISION.md](https://github.com/openclaw/openclaw/blob/main/VISION.md)）。在 agent 外殼的分類上，它屬於「單機自託管、多管道路由、plugin 可擴展」這一型：核心保持精簡，可選能力以 plugin 形式交付，避免核心膨脹。

<Note>
  **「local-first」的工程意涵**

  預設在你的本機跑、預設存取你的本機資源、預設使用你已有的工具。代價是：沒有企業級 SLA、沒有團隊共用基礎設施、沒有統一審計。如果你需要這些，要看 [05-3 NemoClaw](/code-agent/case-studies/nemoclaw) 的硬化外殼模式；OpenClaw 本體不解決這個問題。
</Note>

## 2. 核心技術：Gateway 控制平面與執行迴圈

OpenClaw 整個系統的心臟是一個常駐在本機的 **Gateway 控制平面**。它不是 agent 本身，而是「管理所有 agent、session、channel、tool、event 的總線」。把這層從 agent 邏輯中抽離出來，是 OpenClaw 設計上最關鍵的決定：整合層可以變，agent 邏輯不必跟著重寫。

<AgentLoop
  lang="zh"
  title="OpenClaw 執行迴圈"
  phases={[
{
  id: "perceive",
  label: "Gateway 收訊",
  icon: "eye",
  color: "blue",
  detail: "任何管道（WhatsApp、Telegram、Slack、Discord 等）的入站訊息進入 Gateway 事件匯流排。Gateway 是系統總線，不是 agent 本身。每條管道路由到一個隔離的 agent 實例，上下文互不污染。",
},
{
  id: "plan",
  label: "工作區 md 載入",
  icon: "layers",
  color: "purple",
  detail: "agent 讀取 SOUL.md、AGENTS.md、USER.md、IDENTITY.md、TOOLS.md、MEMORY.md，這是它每次 session 開頭「重新長出自己的人格」的方式。若 HEARTBEAT.md 留空，模型呼叫直接跳過。",
},
{
  id: "act",
  label: "模型呼叫與工具執行",
  icon: "zap",
  color: "yellow",
  detail: "工作區上下文組裝完成後，模型處理請求、決定呼叫哪些工具並生成回應。工具在 openclaw.json 設定的權限邊界內執行。main session 預設有全主機存取權限，其他 session 可設為沙箱模式。",
},
{
  id: "observe",
  label: "記憶寫回",
  icon: "check",
  color: "green",
  detail: "session 結束後，有價值的事實精煉進 MEMORY.md（長期層）；詳細記錄寫入 memory/YYYY-MM-DD.md（日誌層）。下一個 heartbeat tick 或入站訊息將重新觸發此迴圈。",
},
]}
/>

Gateway 的核心職責可以拆成四塊：

1. **多管道路由（multi-channel routing）**：每條入站管道進來的訊息，會被路由到隔離的 agent 實例，各自擁有獨立的工作區與 session，互不污染
2. **多 agent 隔離**：每個 agent 是一個獨立的「人」，擁有自己的工作區、自己的 session、自己的記憶，彼此不共享上下文
3. **工具與權限管理**：哪個 agent 可以呼叫哪個 tool、由誰授權、是否走沙箱，在這層統一決定
4. **事件匯流排**：所有事件（使用者訊息、heartbeat tick、cron 觸發、tool 回傳）都進入同一條匯流排，agent 透過它知道「現在該做什麼」

在這之上，OpenClaw 提供了\*\*心跳機制（heartbeat）\*\*讓 agent 可以在沒有使用者訊息的情況下週期性被喚醒。預設間隔 30 分鐘，使用 Anthropic OAuth/token 驗證時預設為 1 小時（[heartbeat 文件](https://github.com/openclaw/openclaw/blob/main/docs/gateway/heartbeat.md) 截至 2026-05）。

心跳 turn 的預設 prompt 是這句：

```
Read HEARTBEAT.md if it exists (workspace context). Follow it strictly.
Do not infer or repeat old tasks from prior chats.
If nothing needs attention, reply HEARTBEAT_OK.
```

這句 prompt 本身就是設計說明：心跳不是「讓 agent 自己去發想任務」，而是「讀 HEARTBEAT.md 上明確寫的任務清單，沒事就回 HEARTBEAT\_OK 結束這輪」。HEARTBEAT.md 留空時，OpenClaw 甚至會跳過模型呼叫，連一次 LLM 推論都不發起（[heartbeat 文件](https://github.com/openclaw/openclaw/blob/main/docs/gateway/heartbeat.md)）。

<Tip>
  **heartbeat 與 cron 的分工**

  heartbeat 是 main session 的週期性 turn，**不建立背景任務記錄**；cron 是用來建立獨立背景任務記錄（ACP runs、subagents）的機制。混淆這兩個會讓你以為「心跳也能跑長任務」而失望：心跳的本質是 session 內的週期性喚醒，跑長任務要靠 cron 派出去的獨立子代理。
</Tip>

## 3. 系統結構：各組件如何串起來

<FrameworkMap
  lang="zh"
  title="OpenClaw 架構分層"
  layers={[
{
  id: "gateway",
  color: "blue",
  label: "入口層 — Gateway 控制平面",
  modules: [
    {
      id: "gateway-core",
      name: "Gateway",
      path: "src/gateway/",
      summary: "多管道路由、事件匯流排、session 隔離",
      detail: "常駐在本機的控制平面總線。接收所有入站訊息（20+ 管道）並路由到隔離的 agent 實例，各 session 上下文互不污染。所有事件（訊息、heartbeat tick、cron、tool 回傳）都走同一條匯流排。",
      relations: [{ to: "Heartbeat", label: "排程喚醒" }, { to: "Session Router", label: "路由" }],
    },
    {
      id: "heartbeat",
      name: "Heartbeat",
      path: "docs/gateway/heartbeat.md",
      summary: "週期性 turn，預設 30 分鐘",
      detail: "讓 agent 在無使用者訊息時被週期性喚醒。讀 HEARTBEAT.md 執行任務清單；HEARTBEAT.md 留空時跳過模型呼叫，零推論成本。與 cron（背景任務記錄）不同：heartbeat 是 main session 的週期性 turn。",
      relations: [{ to: "Gateway", label: "觸發 turn" }],
    },
  ],
},
{
  id: "workspace",
  color: "purple",
  label: "工作區層 — md 檔組裝人格與記憶",
  modules: [
    {
      id: "soul",
      name: "SOUL.md",
      path: "workspace/SOUL.md",
      summary: "人格、語氣、邊界",
      detail: "每次 session 啟動時注入為高優先指令層，決定 agent 的說話感覺。建議不超過一頁 A4；過長會讓 token 成本上升且遵從度下降。",
      relations: [{ to: "IDENTITY.md", label: "配合" }, { to: "AGENTS.md", label: "人格 vs 政策分工" }],
    },
    {
      id: "agents",
      name: "AGENTS.md",
      path: "workspace/AGENTS.md",
      summary: "操作政策與記憶使用規則",
      detail: "管「做什麼」的操作指令層。同時是根層級政策與路由指南；倉庫根目錄的 AGENTS.md 是整個專案的 policy 檔。每次 session 啟動載入。",
      relations: [{ to: "SOUL.md", label: "政策 vs 人格" }, { to: "TOOLS.md", label: "工具慣例互補" }],
    },
    {
      id: "memory",
      name: "MEMORY.md",
      path: "workspace/MEMORY.md",
      summary: "長期記憶精煉層",
      detail: "存放持久事實、偏好、決策摘要。僅在 DM session 啟動時載入。超出 bootstrap 預算時自動截斷，詳細日誌另存 memory/YYYY-MM-DD.md。",
      relations: [{ to: "memory/YYYY-MM-DD.md", label: "精煉來源" }],
    },
    {
      id: "user",
      name: "USER.md",
      path: "workspace/USER.md",
      summary: "關於使用者的資料",
      detail: "記錄使用者的稱呼、時區、備注、喜好；由 agent 在互動中逐步補充。每次 session 啟動載入。",
      relations: [{ to: "SOUL.md", label: "使用者 vs agent 分工" }],
    },
    {
      id: "identity",
      name: "IDENTITY.md",
      path: "workspace/IDENTITY.md",
      summary: "名字、creature、emoji、avatar",
      detail: "bootstrap 儀式後建立，記錄 agent 的身份卡：名字、屬性、風格、emoji。與 SOUL.md 互補：IDENTITY 是「是什麼」，SOUL 是「怎麼說話」。",
      relations: [{ to: "BOOTSTRAP.md", label: "儀式後建立" }],
    },
    {
      id: "tools",
      name: "TOOLS.md",
      path: "workspace/TOOLS.md",
      summary: "本地工具慣例備注",
      detail: "環境專屬備注（攝影機名稱、SSH 別名、TTS 偏好），不控制工具可用性，僅作指引。每次 session 啟動載入。",
      relations: [{ to: "skills/", label: "慣例 vs 可分享程序" }],
    },
    {
      id: "heartbeat-md",
      name: "HEARTBEAT.md",
      path: "workspace/HEARTBEAT.md",
      summary: "心跳任務清單",
      detail: "心跳 turn 的任務清單；保持短小以避免 token 消耗。留空時 OpenClaw 跳過模型呼叫，連一次 LLM 推論都不發起。每個 heartbeat tick 載入。",
      relations: [{ to: "Heartbeat", label: "被讀取" }],
    },
    {
      id: "bootstrap",
      name: "BOOTSTRAP.md",
      path: "workspace/BOOTSTRAP.md",
      summary: "首次啟動儀式（完成後刪除）",
      detail: "全新工作區的一次性啟動儀式；agent 依序問你稱呼、身份、語氣邊界，填入 USER.md / IDENTITY.md / SOUL.md 後刪除此檔。不刪除則每次啟動重走流程。",
      relations: [{ to: "IDENTITY.md", label: "建立" }, { to: "SOUL.md", label: "建立" }],
    },
  ],
},
{
  id: "execution",
  color: "green",
  label: "執行層 — 工具與沙箱",
  modules: [
    {
      id: "openclaw-json",
      name: "openclaw.json",
      path: "~/.openclaw/openclaw.json",
      summary: "工作區設定、模型、sandbox 選項",
      detail: "控制工作區路徑、模型選擇、sandbox 模式（Docker/SSH/OpenShell）。main session 預設全主機權限；非 main session 應設沙箱，否則 prompt injection 可拿全主機權限。",
      relations: [{ to: "skills/", label: "工具集邊界" }, { to: "Gateway", label: "全域設定" }],
    },
    {
      id: "skills",
      name: "skills/",
      path: "workspace/skills/<skill-name>/SKILL.md",
      summary: "可重用技能（plugin）",
      detail: "以 SKILL.md 格式存放的可分享程序，集中市集為 ClawHub（clawhub.ai）。與 TOOLS.md 的分工：skills 是可版控、可分享的能力，TOOLS.md 是環境專屬備注。",
      relations: [{ to: "TOOLS.md", label: "能力 vs 環境備注" }],
    },
  ],
},
]}
/>

OpenClaw 預設把 agent 的「生活範圍」放在 `~/.openclaw/workspace` 這個目錄，這是 file tool 的相對路徑根。可以在 `openclaw.json` 裡覆寫。

<Tree>
  <Tree.Folder name="~/.openclaw/" defaultOpen>
    <Tree.File name="openclaw.json · 工作區設定（路徑、模型、sandbox 選項）" />

    <Tree.Folder name="workspace/ · agent 的工作目錄" defaultOpen>
      <Tree.File name="AGENTS.md · 操作指令與記憶使用規則" />

      <Tree.File name="SOUL.md · 人格、語氣、邊界" />

      <Tree.File name="IDENTITY.md · 名字、屬性、emoji、avatar" />

      <Tree.File name="USER.md · 關於使用者的資料" />

      <Tree.File name="TOOLS.md · 本地工具慣例備注" />

      <Tree.File name="HEARTBEAT.md · 心跳任務清單" />

      <Tree.File name="MEMORY.md · 長期記憶精煉層" />

      <Tree.Folder name="memory/ · 詳細日誌（每日一檔）">
        <Tree.File name="2026-06-01.md" />
      </Tree.Folder>

      <Tree.File name="BOOTSTRAP.md · 一次性首次啟動儀式（完成後刪除）" />

      <Tree.Folder name="skills/ · 可重用技能（plugin）">
        <Tree.Folder name="<skill-name>/">
          <Tree.File name="SKILL.md" />
        </Tree.Folder>
      </Tree.Folder>
    </Tree.Folder>

    <Tree.Folder name="logs/ · Gateway 與各 agent 的日誌" />
  </Tree.Folder>
</Tree>

幾個關鍵設計點要先釐清：

* **工作區不是沙箱**。工作區只是 agent 的工作目錄；沒有啟用 `agents.defaults.sandbox` 之前，agent 用絕對路徑仍可觸及主機檔案系統。把它當作「agent 預設的『家』」，不是「agent 的牢籠」。
* **bootstrap 是儀式**。全新工作區會被 Gateway 植入標準 md 檔與一份 `BOOTSTRAP.md`；agent 會在首次啟動時用一問一答的方式與使用者共同填寫 `IDENTITY.md` 與 `SOUL.md`，完成後刪除 `BOOTSTRAP.md`（[bootstrapping 文件](https://github.com/openclaw/openclaw/blob/main/docs/start/bootstrapping.md)）。這個「自己決定自己是誰」的儀式不是裝飾，是 OpenClaw 設計哲學的核心：agent 的身份由使用者與 agent 共同協商，不寫死在程式碼裡。
* **plugin 透過 `skills/` 目錄載入**。技能以 `~/.openclaw/workspace/skills/<skill>/SKILL.md` 形式存放；集中市集為 ClawHub（[clawhub.ai](https://clawhub.ai)，截至 2026-05 依 README 所述，市集現況與營運狀態請以官方頁面為準）。
* **安全模型預設是寬鬆的**。main session 代表你本人，預設有全主機存取權限；非 main session（群組、管道進來的）可透過 `openclaw.json` 設為 Docker / SSH / OpenShell 沙箱，限制工具集。

<Warning>
  **main session 的全主機權限是設計選擇，不是 bug**

  OpenClaw 預設信任「你本人透過 main session 下達的指令」。這個假設在你的工作流裡成立，在「共享群組裡的別人透過管道叫你的 agent」場景就不成立。後者的場景請務必在 `openclaw.json` 把對應 session 設為沙箱模式，否則 prompt injection 進來時 agent 會拿全主機權限執行任意指令。
</Warning>

## 4. 自訂 md 檔逐一解剖

OpenClaw 工作區的所有 md 檔都不是「寫好玩的」，每個都有明確的職責與載入時機。以下職責均來自倉庫 [`docs/concepts/agent-workspace.md`](https://github.com/openclaw/openclaw/blob/main/docs/concepts/agent-workspace.md) 與 [`docs/reference/templates/`](https://github.com/openclaw/openclaw/tree/main/docs/reference/templates)（截至 2026-05）：

| 檔案             | 職責                                                                   | 載入時機              |
| -------------- | -------------------------------------------------------------------- | ----------------- |
| `AGENTS.md`    | agent 的操作指令與記憶使用規則；同時是根層級政策與路由指南（倉庫根目錄的 `AGENTS.md` 是整個專案的 policy 檔） | 每次 session 啟動     |
| `SOUL.md`      | 人格、語氣、邊界；OpenClaw 將其注入為高優先指令層，決定 agent「說話感覺」                         | 每次 session 啟動     |
| `USER.md`      | 關於使用者的資料（稱呼、時區、備注、喜好）；由 agent 在互動中逐步補充                               | 每次 session 啟動     |
| `IDENTITY.md`  | agent 的名字、屬性（creature）、風格（vibe）、emoji、avatar                         | bootstrap 儀式後建立   |
| `TOOLS.md`     | 本地工具慣例備注（攝影機名稱、SSH 別名、TTS 偏好設定等）；不控制工具可用性，僅作指引                       | 每次 session 啟動     |
| `HEARTBEAT.md` | 心跳 turn 的任務清單；保持短小以避免 token 消耗；留空時 OpenClaw 跳過模型呼叫                   | 每個 heartbeat tick |
| `MEMORY.md`    | 長期記憶的精煉層；存放持久事實、偏好、決策摘要；詳細日誌在 `memory/YYYY-MM-DD.md`                 | 每次 DM session 啟動  |
| `BOOTSTRAP.md` | 一次性首次啟動儀式；引導 agent 完成身份建立                                            | 全新工作區首次啟動後刪除      |

幾個常被混淆的對照：

* **`SOUL.md` vs `USER.md`**：前者是「我是誰、我怎麼說話」，後者是「你是誰、你喜歡什麼」。前者是 agent 的，後者是你的。
* **`AGENTS.md` vs `SOUL.md`**：`AGENTS.md` 是操作政策（「先做 A 再做 B、遇到 X 就呼叫 Y 工具」），`SOUL.md` 是人格層（「語氣直白、結論先行、不用 emoji」）。一個管「做什麼」，一個管「是怎樣的人」。
* **`MEMORY.md` vs `memory/YYYY-MM-DD.md`**：`MEMORY.md` 是精煉後的長期事實（「使用者住在台中、研究領域是精準畜牧 AI」），日誌檔是當天的詳細記錄。`MEMORY.md` 超出 bootstrap 預算時會被自動截斷，日誌檔則完整保留。
* **`TOOLS.md` vs `skills/`**：`TOOLS.md` 是環境專屬的備注（「我的攝影機叫 `front_door`，SSH 別名 `lab`」），`skills/` 是可分享的可重用程序。先把這兩層分開，之後升級 skill 或換機器時才不會把個人基礎設施洩漏出去。

<Note>
  **一個工作區的 bootstrap 過程**

  全新安裝後，第一次啟動 Gateway：

  <Steps>
    <Step title="植入範本">
      自動植入 AGENTS.md、SOUL.md、USER.md、IDENTITY.md、TOOLS.md、HEARTBEAT.md、MEMORY.md 範本與 BOOTSTRAP.md
    </Step>

    <Step title="一問一答建立身份">
      agent 讀到 BOOTSTRAP.md，依序問你：「你希望我叫你什麼？」（寫入 USER.md）「你希望我自稱什麼、什麼 creature、用什麼 emoji？」（寫入 IDENTITY.md）「我的語氣與邊界？」（寫入 SOUL.md）
    </Step>

    <Step title="儀式結束">
      全部確認後，刪除 BOOTSTRAP.md，session 正常開始
    </Step>
  </Steps>

  這個過程為什麼重要：因為接下來每次 session 啟動，agent 都會從這幾份檔案「重新長出」自己的人格與對你的理解。如果這幾份檔案是空的、矛盾的或過時的，每次 session 開頭的行為就會漂移。
</Note>

## 5. 該學什麼：OpenClaw 把哪些 harness 概念外顯成可讀檔案

OpenClaw 最大的設計貢獻不是它的模型支援或工具集，而是**把通常埋在 system prompt 或程式碼裡的 harness 概念全部外顯成可讀、可 diff 的 md 檔**。這件事在工程上的意義跟在產品設計上的意義是同樣的：把隱性設定變成顯性資產。

與 [01-6 Harness Engineering](/code-agent/foundations/harness-engineering) 單元對位：

| OpenClaw 檔案                          | 對應的 harness 概念                      |
| ------------------------------------ | ----------------------------------- |
| `SOUL.md`                            | 系統提示的人格層（persona layer）             |
| `HEARTBEAT.md`                       | 排程喚醒機制（scheduled wake-up）           |
| `MEMORY.md` + `memory/YYYY-MM-DD.md` | 記憶分層策略（精煉層 vs 日誌層）                  |
| `AGENTS.md`                          | 操作政策與工具邊界（operational policy）       |
| `TOOLS.md`                           | 環境專屬設定（environment-specific config） |
| `IDENTITY.md`                        | 身份卡（identity card）                  |
| `USER.md`                            | 使用者側設定（user profile）                |
| `BOOTSTRAP.md`                       | 首次設定儀式（onboarding ritual）           |

幾個值得內化的設計教訓：

* **lean core + plugin**：OpenClaw 的核心只保留「必須的」東西，技能、管道、政策都透過 plugin / md 檔擴展。核心膨脹是軟體腐敗的開始，harness 也一樣。
* **環境專屬與共享行為切開**：`TOOLS.md`（你的攝影機叫什麼）與 `SOUL.md`（這個 agent 的人格）分開管理，你把 `SOUL.md` 與 `skills/` 分享給別人時，不會洩漏你的基礎設施。
* **儀式不是裝飾**：`BOOTSTRAP.md` 強制 agent 與使用者坐下來談「我們是誰、我們要怎麼合作」，這比寫在文件裡的「default persona」更能產生實際的約束效果。
* **心跳的「沒事就回 OK」是省錢設計**：留空 `HEARTBEAT.md` 時連模型呼叫都跳過，這是給「裝了但暫時不用」的場景留的零成本開關。

## 6. 一句生活比喻

OpenClaw 像一個有自己「生活手冊」的合租室友：它在冰箱上貼了一張便條告訴自己「我是誰、我怎麼說話」（`SOUL.md`），冰箱裡留著昨天的剩菜標籤（`MEMORY.md`），廚房計時器每隔半小時響一次提醒「該看一下爐子沒」（`HEARTBEAT.md`），門口有個本子專門記「這台攝影機叫什麼、那台 SSH 怎麼連」（`TOOLS.md`），而它每晚睡著前都會翻一下本日筆記決定哪些要寫進長期記憶（每天的 `memory/YYYY-MM-DD.md` 精煉成 `MEMORY.md`）。**人不是它，它是人 + 環境的共同組裝，而這本人手冊就是那個環境。**

***

## 動手做

<Steps>
  <Step title="本機安裝並觀察 bootstrap">
    在本機 clone OpenClaw 後跑 `openclaw onboard --install-daemon`，觀察工作區被植入哪些 md 檔，以及 bootstrap 儀式如何一問一答地建立 `IDENTITY.md` 與 `SOUL.md`。完成後檢查 `BOOTSTRAP.md` 是否真的被刪除。
  </Step>

  <Step title="改 HEARTBEAT.md 並觀察心跳 turn">
    在 `~/.openclaw/workspace/HEARTBEAT.md` 加入一條「每半小時檢查我的 GitHub 有沒有新的 issue 標記為我」的任務，觀察 30 分鐘後的心跳 turn 是否正確執行並回覆 `HEARTBEAT_OK` 或實際動作。
  </Step>

  <Step title="閱讀倉庫的範本目錄">
    直接讀 [`docs/reference/templates/`](https://github.com/openclaw/openclaw/tree/main/docs/reference/templates) 下的 `SOUL.md`、`AGENTS.md`、`MEMORY.md` 範本，比對你本機 bootstrap 產生的版本差異；這個 diff 就是「預設 vs 你的客製化」邊界。
  </Step>
</Steps>

## 常見誤區

* **把 `SOUL.md` 寫成企業手冊**：冗長的規則牆讓 token 成本上升，且 agent 遵從度下降（高優先層太長會被模型忽略或稀釋）。短而有行為效果的指令才有用，建議不超過一頁 A4。
* **把大量詳細日誌塞進 `MEMORY.md`**：`MEMORY.md` 是精煉層，不是日誌；詳細記錄應寫入 `memory/YYYY-MM-DD.md`，否則觸發 bootstrap 截斷後會遺失重要資訊。
* **忘記 `BOOTSTRAP.md` 需要手動刪除**：儀式完成後若不刪除，每次啟動都會重走一遍首次設定流程（agent 看到 `BOOTSTRAP.md` 就當作「首次啟動」處理）。
* **main session 預設全主機權限**：見第 3 節的 Warning；把「群組 / 陌生人管道」與「main session」混淆，是 OpenClaw 最常見的暴露點，prompt injection 進來時 agent 會拿全主機權限執行任意指令。
* **把 plugin 當成「裝越多越好」**：OpenClaw 的 skill 越多，每個 session 注入的上下文越多、token 成本越高；只裝你會實際用到的，並定期審查。

## 自我檢核

<Check>
  **通過本單元的標準**

  1. 不看文件，能否說出 `SOUL.md`、`IDENTITY.md`、`MEMORY.md` 三個檔案的差異，以及它們各自在哪個時機載入？
  2. 能否解釋 heartbeat 與 cron job 的分工，以及為何心跳 turn 不建立背景任務記錄？
  3. 能否用一句話向同事說明「OpenClaw 把什麼東西藏在工作區的 md 檔裡」？
  4. 你現在的 agent 設定（不管是 Claude Code、Codex、Cursor 還是其他），有沒有任何一段是「散落在 system prompt 字串裡、不可版控」的操作程序？這段能不能也外顯成一個 md 檔？
</Check>

## 來源與延伸閱讀

事實主張依官方文件，快變動項標註截至 2026-05。

<div className="references">
  * \[1] OpenClaw Repository, GitHub. [https://github.com/openclaw/openclaw](https://github.com/openclaw/openclaw) （截至 2026-05）

  * \[2] OpenClaw, "Agent Workspace Concepts," openclaw/openclaw, docs/concepts/agent-workspace.md. [https://github.com/openclaw/openclaw/blob/main/docs/concepts/agent-workspace.md](https://github.com/openclaw/openclaw/blob/main/docs/concepts/agent-workspace.md) （截至 2026-05）

  * \[3] OpenClaw, "SOUL.md Persona Guide," openclaw/openclaw, docs/concepts/soul.md. [https://github.com/openclaw/openclaw/blob/main/docs/concepts/soul.md](https://github.com/openclaw/openclaw/blob/main/docs/concepts/soul.md) （截至 2026-05）

  * \[4] OpenClaw, "Heartbeat," openclaw/openclaw, docs/gateway/heartbeat.md. [https://github.com/openclaw/openclaw/blob/main/docs/gateway/heartbeat.md](https://github.com/openclaw/openclaw/blob/main/docs/gateway/heartbeat.md) （截至 2026-05）

  * \[5] OpenClaw, "Memory," openclaw/openclaw, docs/concepts/memory.md. [https://github.com/openclaw/openclaw/blob/main/docs/concepts/memory.md](https://github.com/openclaw/openclaw/blob/main/docs/concepts/memory.md) （截至 2026-05）

  * \[6] OpenClaw, "Workspace Templates," openclaw/openclaw, docs/reference/templates/. [https://github.com/openclaw/openclaw/tree/main/docs/reference/templates](https://github.com/openclaw/openclaw/tree/main/docs/reference/templates) （截至 2026-05）

  * \[7] OpenClaw, "Bootstrapping," openclaw/openclaw, docs/start/bootstrapping.md. [https://github.com/openclaw/openclaw/blob/main/docs/start/bootstrapping.md](https://github.com/openclaw/openclaw/blob/main/docs/start/bootstrapping.md) （截至 2026-05）

  * \[8] P. Steinberger, "VISION.md," openclaw/openclaw. [https://github.com/openclaw/openclaw/blob/main/VISION.md](https://github.com/openclaw/openclaw/blob/main/VISION.md) （截至 2026-05）
</div>

* 極簡派對照：[05-2 Hermes-Agent](/code-agent/case-studies/hermes-agent)（Nous Research 的 Python 對位，採取三層 system prompt 組裝）
* 硬化層對照：[05-3 NemoClaw](/code-agent/case-studies/nemoclaw)（NVIDIA 2026-03 開源，把 OpenClaw 包進 OpenShell 沙箱，企業就緒）
* 比較：[05-4 三條路線差異](/code-agent/case-studies/three-approaches-comparison)
* 前置單元：[01-6 Harness Engineering](/code-agent/foundations/harness-engineering)（本案例研究的概念基礎）
