97f649a3e1
- Copy delegate-job-skill/skills/delegate-job/ → skills/delegate-job/ - Move requirements.txt (paho-mqtt>=2.0.0) into the new location - Refactor outdated hardcoded paths (~/PuKi/lab/, ~/.hermes/skills/) to dynamic resolution - Add MQTT connection timeout / retry hardening - Remove legacy delegate-job-skill/ directory - Update .gitignore Note: delegate-job-skill git history is squashed — preserved content, dropped commit lineage.
2196 lines
92 KiB
HTML
2196 lines
92 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>delegate-job — User Manual & Architecture Guide</title>
|
|
<meta name="description" content="MQTT 이벤트 채널 기반 자율 에이전트 비동기 위임 스킬 delegate-job 사용자 가이드 및 아키텍처 명세서.">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;600;700;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
/* ----------------------------------------------------------------------
|
|
1. Design Tokens & CSS Variables (Theme Support)
|
|
---------------------------------------------------------------------- */
|
|
:root {
|
|
/* Light Mode (Default) - Clean Slate/Vercel Aesthetic */
|
|
--bg-base: #ffffff;
|
|
--bg-sidebar: #fafafa;
|
|
--bg-surface: #ffffff;
|
|
--bg-surface-hover: #f3f4f6;
|
|
--border: #e5e7eb;
|
|
--border-focus: #111827;
|
|
|
|
--fg-base: #111827;
|
|
--fg-muted: #4b5563;
|
|
--fg-subtle: #9ca3af;
|
|
|
|
--accent: #4f46e5; /* Indigo */
|
|
--accent-glow: rgba(79, 70, 229, 0.05);
|
|
--accent-teal: #0d9488; /* Teal */
|
|
--accent-teal-glow: rgba(13, 148, 136, 0.05);
|
|
|
|
--state-pending: #d97706; /* Amber */
|
|
--state-running: #2563eb; /* Blue */
|
|
--state-completed: #059669; /* Emerald */
|
|
--state-error: #dc2626; /* Red */
|
|
--state-cancel: #4b5563;
|
|
--state-timeout: #db2777;
|
|
|
|
--code-bg: #f8fafc;
|
|
--code-fg: #1e293b;
|
|
--code-border: #e2e8f0;
|
|
--code-comment: #64748b;
|
|
--code-keyword: #4f46e5;
|
|
--code-string: #0d9488;
|
|
--code-number: #d97706;
|
|
|
|
--svg-bg: #f8fafc;
|
|
--svg-node-bg: #ffffff;
|
|
--svg-node-stroke: #e5e7eb;
|
|
--svg-text-base: #111827;
|
|
--svg-text-muted: #4b5563;
|
|
|
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
--font-display: 'Outfit', var(--font-sans);
|
|
--font-mono: 'Fira Code', 'JetBrains Mono', Courier, monospace;
|
|
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.02);
|
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.03), 0 2px 4px -2px rgba(0, 0, 0, 0.03);
|
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.04), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
/* Dark Mode - Clean Linear/Stripe Aesthetic */
|
|
--bg-base: #08090a;
|
|
--bg-sidebar: #0c0d0e;
|
|
--bg-surface: #121315;
|
|
--bg-surface-hover: #1b1d20;
|
|
--border: #1f2022;
|
|
--border-focus: #f3f4f6;
|
|
|
|
--fg-base: #f3f4f6;
|
|
--fg-muted: #9ca3af;
|
|
--fg-subtle: #4b5563;
|
|
|
|
--accent: #6366f1; /* Bright Indigo */
|
|
--accent-glow: rgba(99, 102, 241, 0.12);
|
|
--accent-teal: #14b8a6; /* Teal */
|
|
--accent-teal-glow: rgba(20, 184, 166, 0.12);
|
|
|
|
--code-bg: #0d0e10;
|
|
--code-fg: #e5e7eb;
|
|
--code-border: #1a1b1d;
|
|
--code-comment: #4b5563;
|
|
--code-keyword: #818cf8;
|
|
--code-string: #34d399;
|
|
--code-number: #fbbf24;
|
|
|
|
--svg-bg: #0b0d10;
|
|
--svg-node-bg: #121315;
|
|
--svg-node-stroke: #1f2022;
|
|
--svg-text-base: #f3f4f6;
|
|
--svg-text-muted: #9ca3af;
|
|
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
|
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.5);
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
2. Global Layout Reset
|
|
---------------------------------------------------------------------- */
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-base);
|
|
color: var(--fg-base);
|
|
font-family: var(--font-sans);
|
|
font-size: 16px;
|
|
line-height: 1.7;
|
|
min-height: 100vh;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/* Document Layout Structure */
|
|
.app-container {
|
|
display: grid;
|
|
grid-template-columns: 280px minmax(0, 1fr) 260px;
|
|
gap: 40px;
|
|
max-width: 1440px;
|
|
margin: 0 auto;
|
|
padding: 0 32px;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
3. Left Sidebar (Project Info & Navigation)
|
|
---------------------------------------------------------------------- */
|
|
.left-sidebar {
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
padding: 48px 0;
|
|
overflow-y: auto;
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 32px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.logo-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
text-decoration: none;
|
|
color: var(--fg-base);
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
font-size: 1.2rem;
|
|
}
|
|
.logo-symbol {
|
|
fill: none;
|
|
stroke: var(--accent);
|
|
stroke-width: 2.5;
|
|
}
|
|
|
|
.sidebar-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.sidebar-title {
|
|
font-family: var(--font-display);
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--fg-subtle);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.sidebar-links {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.sidebar-link {
|
|
font-size: 0.9rem;
|
|
color: var(--fg-muted);
|
|
text-decoration: none;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.sidebar-link:hover {
|
|
background-color: var(--bg-surface-hover);
|
|
color: var(--fg-base);
|
|
}
|
|
.sidebar-link.active {
|
|
background-color: var(--accent-glow);
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.sidebar-badge {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background-color: var(--border);
|
|
color: var(--fg-muted);
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
4. Right Sidebar (Active Table of Contents Outline)
|
|
---------------------------------------------------------------------- */
|
|
.right-sidebar {
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
padding: 48px 0;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.toc-link {
|
|
font-size: 0.85rem;
|
|
color: var(--fg-muted);
|
|
text-decoration: none;
|
|
display: block;
|
|
padding: 4px 8px;
|
|
border-left: 2px solid transparent;
|
|
transition: all 0.2s ease;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.toc-link:hover {
|
|
color: var(--fg-base);
|
|
}
|
|
.toc-link.active {
|
|
color: var(--accent);
|
|
border-left-color: var(--accent);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
5. Scroll Progress Bar
|
|
---------------------------------------------------------------------- */
|
|
#progress-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 2px;
|
|
z-index: 200;
|
|
pointer-events: none;
|
|
background: transparent;
|
|
}
|
|
#progress-bar {
|
|
height: 100%;
|
|
width: 100%;
|
|
background: var(--accent);
|
|
transform-origin: 0 50%;
|
|
transform: scaleX(0);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
@supports (animation-timeline: scroll()) {
|
|
#progress-bar {
|
|
animation: grow-progress auto linear;
|
|
animation-timeline: scroll(root);
|
|
}
|
|
@keyframes grow-progress {
|
|
from { transform: scaleX(0); }
|
|
to { transform: scaleX(1); }
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
6. Main Content Area & Sections
|
|
---------------------------------------------------------------------- */
|
|
.main-content {
|
|
padding: 48px 0 120px;
|
|
max-width: 820px;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 56px;
|
|
}
|
|
|
|
/* Title Headings styling */
|
|
.bread-crumb {
|
|
font-size: 0.85rem;
|
|
font-family: var(--font-mono);
|
|
color: var(--fg-subtle);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
h1 {
|
|
font-family: var(--font-display);
|
|
font-weight: 800;
|
|
font-size: 2.5rem;
|
|
letter-spacing: -0.02em;
|
|
line-height: 1.2;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 1.2rem;
|
|
color: var(--fg-muted);
|
|
font-weight: 400;
|
|
margin-bottom: 32px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
h2 {
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
font-size: 1.6rem;
|
|
letter-spacing: -0.015em;
|
|
margin-bottom: 24px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
h3 {
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
font-size: 1.15rem;
|
|
margin: 32px 0 16px;
|
|
color: var(--fg-base);
|
|
}
|
|
|
|
p {
|
|
color: var(--fg-muted);
|
|
margin-bottom: 20px;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
/* Logical Section Card */
|
|
.section-card {
|
|
scroll-margin-top: 48px;
|
|
}
|
|
|
|
/* MQTT Broker Setup Banner Card (Vercel Style) */
|
|
.setup-banner-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin: 24px 0 32px 0;
|
|
box-shadow: var(--shadow-md);
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
position: relative;
|
|
}
|
|
.setup-banner-card:hover {
|
|
border-color: var(--accent);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
.setup-banner-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 10px;
|
|
background-color: var(--accent-glow);
|
|
color: var(--accent);
|
|
flex-shrink: 0;
|
|
}
|
|
.setup-banner-content {
|
|
flex-grow: 1;
|
|
}
|
|
.setup-banner-content h4 {
|
|
font-family: var(--font-display);
|
|
font-size: 1.05rem;
|
|
font-weight: 700;
|
|
margin-bottom: 6px;
|
|
color: var(--fg-base);
|
|
}
|
|
.setup-banner-content p {
|
|
font-size: 0.88rem;
|
|
color: var(--fg-muted);
|
|
margin-bottom: 0;
|
|
line-height: 1.5;
|
|
}
|
|
.setup-banner-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background-color: var(--fg-base);
|
|
color: var(--bg-base);
|
|
border: none;
|
|
padding: 10px 18px;
|
|
border-radius: 8px;
|
|
font-family: var(--font-sans);
|
|
font-weight: 600;
|
|
font-size: 0.88rem;
|
|
text-decoration: none;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
.setup-banner-btn:hover {
|
|
opacity: 0.9;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.setup-banner-card {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 16px;
|
|
}
|
|
.setup-banner-btn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
7. Interactive Diagrams (High-Level, Sequence, Component Map)
|
|
---------------------------------------------------------------------- */
|
|
.diagram-panel {
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
box-shadow: var(--shadow-sm);
|
|
margin: 24px 0;
|
|
}
|
|
|
|
.diagram-tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid var(--border);
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.diagram-tab-btn {
|
|
background: none;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
padding: 8px 12px;
|
|
color: var(--fg-muted);
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.diagram-tab-btn:hover {
|
|
color: var(--fg-base);
|
|
}
|
|
.diagram-tab-btn.active {
|
|
color: var(--accent);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
|
|
.diagram-content {
|
|
display: none;
|
|
}
|
|
.diagram-content.active {
|
|
display: block;
|
|
animation: fadeIn 0.3s ease;
|
|
}
|
|
|
|
/* SVG diagram style config */
|
|
.svg-container {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background-color: var(--svg-bg);
|
|
padding: 16px 0;
|
|
}
|
|
|
|
.diagram-svg {
|
|
display: block;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* SVG Styling variables mapping */
|
|
.svg-node-box {
|
|
fill: var(--svg-node-bg);
|
|
stroke: var(--svg-node-stroke);
|
|
stroke-width: 1.5;
|
|
}
|
|
.svg-txt-main {
|
|
fill: var(--svg-text-base);
|
|
font-family: var(--font-sans);
|
|
font-weight: 700;
|
|
}
|
|
.svg-txt-sub {
|
|
fill: var(--svg-text-muted);
|
|
font-family: var(--font-sans);
|
|
}
|
|
.svg-line {
|
|
stroke: var(--border);
|
|
stroke-dasharray: 4 4;
|
|
stroke-width: 1;
|
|
}
|
|
.svg-arrow-line {
|
|
stroke: var(--fg-muted);
|
|
stroke-width: 1.5;
|
|
fill: none;
|
|
}
|
|
.svg-arrow-line.accent {
|
|
stroke: var(--accent);
|
|
stroke-width: 2;
|
|
}
|
|
.svg-arrow-line.accent-teal {
|
|
stroke: var(--accent-teal);
|
|
stroke-width: 2;
|
|
}
|
|
|
|
@keyframes dash-animation {
|
|
to {
|
|
stroke-dashoffset: -20;
|
|
}
|
|
}
|
|
|
|
.svg-flow-path {
|
|
stroke-dasharray: 5, 5;
|
|
animation: dash-animation 1.5s linear infinite;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
8. Quick Start Content Switcher Tabs
|
|
---------------------------------------------------------------------- */
|
|
.quickstart-tabs {
|
|
margin-top: 24px;
|
|
}
|
|
.qs-tab-headers {
|
|
display: flex;
|
|
gap: 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 24px;
|
|
}
|
|
.qs-tab-btn {
|
|
background: none;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
padding: 10px 16px;
|
|
color: var(--fg-muted);
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.qs-tab-btn:hover {
|
|
color: var(--fg-base);
|
|
}
|
|
.qs-tab-btn.active {
|
|
color: var(--accent);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
.qs-tab-panel {
|
|
display: none;
|
|
}
|
|
.qs-tab-panel.active {
|
|
display: block;
|
|
animation: fadeIn 0.3s ease;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
9. Tables & Custom Data Grids
|
|
---------------------------------------------------------------------- */
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0 32px;
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
th, td {
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 14px 16px;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
|
|
th {
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
color: var(--fg-base);
|
|
background-color: var(--bg-sidebar);
|
|
}
|
|
|
|
tr:hover td {
|
|
background-color: var(--bg-surface-hover);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
10. Callouts & Warning Panels
|
|
---------------------------------------------------------------------- */
|
|
.callout {
|
|
border-left: 3px solid var(--border);
|
|
padding: 16px 20px;
|
|
margin: 28px 0;
|
|
border-radius: 0 8px 8px 0;
|
|
background-color: var(--bg-sidebar);
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
.callout-icon {
|
|
flex-shrink: 0;
|
|
color: var(--fg-muted);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.callout-body {
|
|
flex: 1;
|
|
}
|
|
.callout-body h4 {
|
|
font-size: 0.95rem;
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
margin-bottom: 6px;
|
|
}
|
|
.callout-body p {
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0;
|
|
color: var(--fg-muted);
|
|
}
|
|
|
|
.callout.tip {
|
|
border-left-color: var(--accent-teal);
|
|
background-color: var(--accent-teal-glow);
|
|
}
|
|
.callout.tip .callout-icon {
|
|
color: var(--accent-teal);
|
|
}
|
|
.callout.important {
|
|
border-left-color: var(--accent);
|
|
background-color: var(--accent-glow);
|
|
}
|
|
.callout.important .callout-icon {
|
|
color: var(--accent);
|
|
}
|
|
.callout.warning {
|
|
border-left-color: var(--state-pending);
|
|
background-color: rgba(217, 119, 6, 0.05);
|
|
}
|
|
.callout.warning .callout-icon {
|
|
color: var(--state-pending);
|
|
}
|
|
.callout.danger {
|
|
border-left-color: var(--state-error);
|
|
background-color: rgba(220, 38, 38, 0.05);
|
|
}
|
|
.callout.danger .callout-icon {
|
|
color: var(--state-error);
|
|
}
|
|
|
|
/* Markdown Link Indicator Badge */
|
|
.markdown-link::after {
|
|
content: 'MD';
|
|
font-family: var(--font-sans);
|
|
font-size: 0.6rem;
|
|
font-weight: 700;
|
|
background-color: var(--border);
|
|
color: var(--fg-muted);
|
|
padding: 1px 4px;
|
|
border-radius: 4px;
|
|
margin-left: auto;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
/* Table Responsive Wrapper */
|
|
.table-wrapper {
|
|
overflow-x: auto;
|
|
margin: 24px 0;
|
|
width: 100%;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
.table-wrapper table {
|
|
margin: 0;
|
|
border: none;
|
|
}
|
|
|
|
/* Setup Guide Card Responsive styling */
|
|
.setup-guide-card {
|
|
margin: 20px 0;
|
|
background: var(--bg-sidebar);
|
|
border: 1px dashed var(--accent);
|
|
border-radius: 8px;
|
|
padding: 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
.setup-guide-card:hover {
|
|
border-color: var(--accent-teal);
|
|
background: var(--bg-surface-hover);
|
|
}
|
|
@media (max-width: 768px) {
|
|
.setup-guide-card {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
}
|
|
.setup-guide-card a.setup-banner-btn {
|
|
width: 100%;
|
|
text-align: center;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
11. Code Blocks & Highlight Styling
|
|
---------------------------------------------------------------------- */
|
|
.code-container {
|
|
background-color: var(--code-bg);
|
|
border: 1px solid var(--code-border);
|
|
border-radius: 8px;
|
|
margin: 24px 0;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-sm);
|
|
position: relative;
|
|
}
|
|
|
|
.code-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 16px;
|
|
background-color: rgba(255, 255, 255, 0.01);
|
|
border-bottom: 1px solid var(--code-border);
|
|
font-size: 0.78rem;
|
|
color: var(--fg-subtle);
|
|
}
|
|
.code-lang-label {
|
|
font-family: var(--font-mono);
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
}
|
|
.copy-btn {
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
color: var(--fg-muted);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-family: var(--font-sans);
|
|
font-size: 0.75rem;
|
|
transition: all 0.2s;
|
|
}
|
|
.copy-btn:hover {
|
|
background-color: var(--bg-surface-hover);
|
|
color: var(--fg-base);
|
|
}
|
|
.copy-btn.copied {
|
|
background-color: var(--state-completed);
|
|
color: white;
|
|
border-color: var(--state-completed);
|
|
}
|
|
|
|
pre {
|
|
padding: 16px;
|
|
overflow-x: auto;
|
|
margin: 0;
|
|
}
|
|
code {
|
|
font-family: var(--font-mono);
|
|
font-size: 14px;
|
|
line-height: 1.7;
|
|
color: var(--code-fg);
|
|
}
|
|
|
|
/* Code color overrides matching Linear/Vercel standard */
|
|
pre .c { color: var(--code-comment); font-style: italic; }
|
|
pre .k { color: var(--code-keyword); font-weight: 600; }
|
|
pre .s { color: var(--code-string); }
|
|
pre .n { color: var(--code-number); }
|
|
|
|
p code, li code, td code {
|
|
background-color: var(--bg-surface-hover);
|
|
border: 1px solid var(--border);
|
|
color: var(--accent);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
12. Lists and Checklists
|
|
---------------------------------------------------------------------- */
|
|
ul, ol {
|
|
padding-left: 24px;
|
|
margin-bottom: 20px;
|
|
color: var(--fg-muted);
|
|
}
|
|
li {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.checklist {
|
|
list-style: none;
|
|
padding-left: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
.checklist li {
|
|
position: relative;
|
|
padding-left: 28px;
|
|
margin: 0;
|
|
}
|
|
.checklist li::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
top: 4px;
|
|
width: 18px;
|
|
height: 18px;
|
|
background-color: var(--bg-base);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
}
|
|
.checklist li.ok::before {
|
|
background-color: var(--accent-teal-glow);
|
|
border-color: var(--accent-teal);
|
|
}
|
|
.checklist li.ok::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 6px;
|
|
top: 7px;
|
|
width: 4px;
|
|
height: 8px;
|
|
border: solid var(--accent-teal);
|
|
border-width: 0 2px 2px 0;
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
13. Command Search and Filtering Box
|
|
---------------------------------------------------------------------- */
|
|
.search-box-container {
|
|
position: relative;
|
|
margin-bottom: 16px;
|
|
}
|
|
.search-box-input {
|
|
width: 100%;
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 10px 14px 10px 38px;
|
|
color: var(--fg-base);
|
|
font-family: var(--font-sans);
|
|
font-size: 0.88rem;
|
|
outline: none;
|
|
transition: all 0.2s;
|
|
}
|
|
.search-box-input:focus {
|
|
border-color: var(--border-focus);
|
|
}
|
|
.search-box-container svg {
|
|
position: absolute;
|
|
left: 14px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--fg-subtle);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
14. Interactive State Transition Flow
|
|
---------------------------------------------------------------------- */
|
|
.lifecycle-viz {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin: 28px 0;
|
|
overflow-x: auto;
|
|
padding: 16px 0;
|
|
}
|
|
.lifecycle-state-card {
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
width: 130px;
|
|
text-align: center;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
.lifecycle-state-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background-color: var(--fg-subtle);
|
|
}
|
|
.lifecycle-state-card.pending .lifecycle-state-dot { background-color: var(--state-pending); box-shadow: 0 0 8px var(--state-pending); }
|
|
.lifecycle-state-card.running .lifecycle-state-dot { background-color: var(--state-running); box-shadow: 0 0 8px var(--state-running); }
|
|
.lifecycle-state-card.completed .lifecycle-state-dot { background-color: var(--state-completed); box-shadow: 0 0 8px var(--state-completed); }
|
|
.lifecycle-state-card.error .lifecycle-state-dot { background-color: var(--state-error); box-shadow: 0 0 8px var(--state-error); }
|
|
.lifecycle-state-card.cancel .lifecycle-state-dot { background-color: var(--state-cancel); }
|
|
.lifecycle-state-card.timeout .lifecycle-state-dot { background-color: var(--state-timeout); box-shadow: 0 0 8px var(--state-timeout); }
|
|
|
|
.lifecycle-state-name {
|
|
font-family: var(--font-mono);
|
|
font-weight: 700;
|
|
font-size: 0.85rem;
|
|
}
|
|
.lifecycle-state-desc {
|
|
font-size: 0.72rem;
|
|
color: var(--fg-muted);
|
|
}
|
|
.lifecycle-flow-arrow {
|
|
color: var(--fg-subtle);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
15. Navigation Prev / Next Cards at footer
|
|
---------------------------------------------------------------------- */
|
|
.nav-footer-cards {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
margin-top: 48px;
|
|
padding-top: 32px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.nav-card {
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
text-decoration: none;
|
|
color: var(--fg-base);
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.nav-card:hover {
|
|
border-color: var(--accent);
|
|
background-color: var(--bg-surface-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
.nav-card-label {
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--fg-subtle);
|
|
margin-bottom: 4px;
|
|
}
|
|
.nav-card-title {
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
font-size: 0.95rem;
|
|
}
|
|
.nav-card.prev {
|
|
align-items: flex-start;
|
|
}
|
|
.nav-card.next {
|
|
align-items: flex-end;
|
|
text-align: right;
|
|
}
|
|
|
|
.back-to-top {
|
|
text-align: center;
|
|
margin-top: 24px;
|
|
}
|
|
.back-to-top-link {
|
|
color: var(--fg-subtle);
|
|
text-decoration: none;
|
|
font-size: 0.88rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.back-to-top-link:hover {
|
|
color: var(--fg-base);
|
|
}
|
|
|
|
/* Footer details */
|
|
footer {
|
|
padding-top: 32px;
|
|
margin-top: 32px;
|
|
border-top: 1px solid var(--border);
|
|
color: var(--fg-subtle);
|
|
font-size: 0.82rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
16. Responsive Adaptation (Mobile / Tablet)
|
|
---------------------------------------------------------------------- */
|
|
@media (max-width: 1200px) {
|
|
.app-container {
|
|
grid-template-columns: 260px minmax(0, 1fr);
|
|
}
|
|
.right-sidebar {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.app-container {
|
|
display: block;
|
|
padding: 0 20px;
|
|
}
|
|
.left-sidebar {
|
|
display: none;
|
|
}
|
|
.main-content {
|
|
padding: 32px 0 80px;
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
17. Print Optimization Styling
|
|
---------------------------------------------------------------------- */
|
|
@media print {
|
|
body {
|
|
background-color: #ffffff !important;
|
|
color: #000000 !important;
|
|
display: block !important;
|
|
}
|
|
.left-sidebar, .right-sidebar, #progress-container, .code-header, .copy-btn, .nav-footer-cards, .back-to-top {
|
|
display: none !important;
|
|
}
|
|
.app-container {
|
|
display: block !important;
|
|
padding: 0 !important;
|
|
}
|
|
.main-content {
|
|
margin-left: 0 !important;
|
|
padding: 0 !important;
|
|
max-width: 100% !important;
|
|
}
|
|
.section-card {
|
|
page-break-inside: avoid;
|
|
margin-bottom: 3rem;
|
|
}
|
|
pre, .diagram-panel {
|
|
page-break-inside: avoid;
|
|
}
|
|
}
|
|
|
|
/* Animation helper */
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(4px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Scroll Progress tracker -->
|
|
<div id="progress-container" aria-hidden="true">
|
|
<div id="progress-bar"></div>
|
|
</div>
|
|
|
|
<!-- App Wrapper Grid -->
|
|
<div class="app-container">
|
|
|
|
<!-- 1. Left Sidebar Navigation -->
|
|
<aside class="left-sidebar">
|
|
<a href="#" class="logo-container">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" class="logo-symbol">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
<span>Hermes Skill</span>
|
|
</a>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-title">메인 가이드</div>
|
|
<ul class="sidebar-links">
|
|
<li><a href="./SKILL.md" class="sidebar-link markdown-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg> SKILL.md <span class="sidebar-badge">코드 가이드</span></a></li>
|
|
<li><a href="./README.md" class="sidebar-link markdown-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg> README.md <span class="sidebar-badge">스킬 요약</span></a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-title">참조 스펙 명세 (References)</div>
|
|
<ul class="sidebar-links">
|
|
<li><a href="./job-protocol.md" class="sidebar-link markdown-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg> job-protocol.md</a></li>
|
|
<li><a href="./registry.md" class="sidebar-link markdown-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg> registry.md</a></li>
|
|
<li><a href="./mqtt-broker-setup.html" class="sidebar-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg> MQTT 브로커 셋업</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-title">인프라 관리</div>
|
|
<a href="./mqtt-broker-setup.html" class="sidebar-action-btn" style="
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
background-color: var(--accent);
|
|
color: #ffffff !important;
|
|
text-decoration: none;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
margin-top: 4px;
|
|
transition: opacity 0.2s ease;
|
|
">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
</svg>
|
|
MQTT 브로커 셋업
|
|
</a>
|
|
</div>
|
|
|
|
<div class="sidebar-section" style="margin-top: auto;">
|
|
<div style="font-size:0.75rem; color:var(--fg-subtle);">
|
|
PoC Mode: Public Broker<br>
|
|
Host: broker.hivemq.com
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- 2. Middle Main Column Reading Pane -->
|
|
<main class="main-content">
|
|
|
|
<!-- Title Header -->
|
|
<header>
|
|
<div class="bread-crumb">Hermes / Skills / delegate-job</div>
|
|
<h1>delegate-job</h1>
|
|
<p class="subtitle">자율 에이전트 비동기 위임 및 MQTT 이벤트 관찰 아키텍처 명세서</p>
|
|
</header>
|
|
|
|
<!-- MQTT Broker Setup Banner Card -->
|
|
<div class="setup-banner-card">
|
|
<div class="setup-banner-icon">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="setup-banner-content">
|
|
<h4>MQTT 브로커 셋업 가이드 (PoC → Production)</h4>
|
|
<p>공개 테스트 브로커에서 내부 TLS 암호화 및 ACL 인가 제어를 적용한 사설 프로덕션 브로커로 안전하게 전환하기 위한 단계별 구축 가이드입니다.</p>
|
|
</div>
|
|
<a href="./mqtt-broker-setup.html" class="setup-banner-btn">가이드 열기 →</a>
|
|
</div>
|
|
|
|
<!-- Architecture Card -->
|
|
<section id="architecture" class="section-card">
|
|
<h2>시스템 아키텍처 명세</h2>
|
|
<p>자율 에이전트 위임 모델은 동시성 간섭이 배제된 단방향 이벤트 스트림 구조를 띱니다. 아래 탭을 전환하며 상호작용 가능한 다이어그램들을 확인하실 수 있습니다.</p>
|
|
|
|
<div class="diagram-panel">
|
|
<div class="diagram-tabs">
|
|
<button class="diagram-tab-btn active" onclick="switchDiagram('diag-flow')" data-tab="diag-flow">1. High-Level Flow</button>
|
|
<button class="diagram-tab-btn" onclick="switchDiagram('diag-seq')" data-tab="diag-seq">2. Sequence Diagram</button>
|
|
<button class="diagram-tab-btn" onclick="switchDiagram('diag-map')" data-tab="diag-map">3. Component Map</button>
|
|
</div>
|
|
|
|
<!-- Diagram 1: High Level Flow -->
|
|
<div id="diag-flow" class="diagram-content active">
|
|
<div class="svg-container">
|
|
<svg viewBox="0 0 900 380" width="100%" height="auto" class="diagram-svg">
|
|
<rect width="900" height="380" fill="none" />
|
|
<!-- Grid pattern background inside SVG -->
|
|
<path d="M 0,0 L 900,0 M 0,50 L 900,50 M 0,100 L 900,100 M 0,150 L 900,150 M 0,200 L 900,200 M 0,250 L 900,250 M 0,300 L 900,300 M 0,350 L 900,350 M 0,0 L 0,380 M 50,0 L 50,380 M 100,0 L 100,380 M 150,0 L 150,380 M 200,0 L 200,380 M 250,0 L 250,380 M 300,0 L 300,380 M 350,0 L 350,380 M 400,0 L 400,380 M 450,0 L 450,380 M 500,0 L 500,380 M 550,0 L 550,380 M 600,0 L 600,380 M 650,0 L 650,380 M 700,0 L 700,380 M 750,0 L 750,380 M 800,0 L 800,380 M 850,0 L 850,380" stroke="var(--border)" stroke-width="0.3" opacity="0.5" />
|
|
|
|
<defs>
|
|
<marker id="arrow-flow" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="var(--fg-muted)" />
|
|
</marker>
|
|
</defs>
|
|
|
|
<!-- Components -->
|
|
<!-- User / CLI -->
|
|
<rect x="30" y="40" width="180" height="50" rx="6" class="svg-node-box" />
|
|
<text x="45" y="70" class="svg-txt-main" font-size="11">User/Delegator</text>
|
|
|
|
<!-- delegate-job submit -->
|
|
<rect x="250" y="40" width="180" height="50" rx="6" class="svg-node-box" />
|
|
<text x="265" y="65" class="svg-txt-main" font-size="11">delegate-job submit</text>
|
|
<text x="265" y="80" class="svg-txt-sub" font-size="8">Wrapper CLI</text>
|
|
|
|
<!-- Registry -->
|
|
<rect x="250" y="160" width="180" height="50" rx="6" class="svg-node-box" />
|
|
<text x="265" y="185" class="svg-txt-main" font-size="11">Job Registry</text>
|
|
<text x="265" y="200" class="svg-txt-sub" font-size="8">.hermes/jobs/<id>.json</text>
|
|
|
|
<!-- Subscriber bg -->
|
|
<rect x="500" y="40" width="160" height="50" rx="6" class="svg-node-box" />
|
|
<text x="515" y="65" class="svg-txt-main" font-size="11">job_subscriber.py</text>
|
|
<text x="515" y="80" class="svg-txt-sub" font-size="8">Background Process</text>
|
|
|
|
<!-- Agent -->
|
|
<rect x="500" y="160" width="160" height="50" rx="6" class="svg-node-box" />
|
|
<text x="515" y="185" class="svg-txt-main" font-size="11">Agent (tmux/print)</text>
|
|
<text x="515" y="200" class="svg-txt-sub" font-size="8">claude-code / codex</text>
|
|
|
|
<!-- publish_event -->
|
|
<rect x="500" y="270" width="160" height="50" rx="6" class="svg-node-box" />
|
|
<text x="515" y="295" class="svg-txt-main" font-size="11">publish_event.py</text>
|
|
<text x="515" y="310" class="svg-txt-sub" font-size="8">Worker Utility</text>
|
|
|
|
<!-- MQTT Broker -->
|
|
<rect x="710" y="100" width="160" height="110" rx="8" class="svg-node-box" />
|
|
<text x="725" y="130" class="svg-txt-main" font-size="13">MQTT Broker</text>
|
|
<text x="725" y="150" class="svg-txt-sub" font-size="9" fill="var(--accent-teal)">HiveMQ / Mosquitto</text>
|
|
<text x="725" y="170" class="svg-txt-sub" font-size="8" font-family="var(--font-mono)">jobs/<id>/events</text>
|
|
|
|
<!-- Audit Log -->
|
|
<rect x="710" y="270" width="160" height="60" rx="6" class="svg-node-box" />
|
|
<text x="725" y="295" class="svg-txt-main" font-size="11">Audit Log</text>
|
|
<text x="725" y="315" class="svg-txt-sub" font-size="8" font-family="var(--font-mono)">events.ndjson</text>
|
|
|
|
<!-- Arrow paths -->
|
|
<!-- User -> submit -->
|
|
<path d="M 210,65 L 242,65" class="svg-arrow-line" marker-end="url(#arrow-flow)" />
|
|
<!-- submit -> Registry -->
|
|
<path d="M 340,90 L 340,152" class="svg-arrow-line" marker-end="url(#arrow-flow)" />
|
|
<!-- submit -> Subscriber -->
|
|
<path d="M 430,55 L 492,55" class="svg-arrow-line" marker-end="url(#arrow-flow)" />
|
|
<!-- submit -> Agent -->
|
|
<path d="M 430,75 L 460,75 L 460,185 L 492,185" class="svg-arrow-line" marker-end="url(#arrow-flow)" />
|
|
<!-- Subscriber -> Broker (Sub) -->
|
|
<path d="M 660,55 L 790,55 L 790,92" class="svg-arrow-line" marker-end="url(#arrow-flow)" />
|
|
<!-- Agent -> publish_event -->
|
|
<path d="M 580,210 L 580,262" class="svg-arrow-line" marker-end="url(#arrow-flow)" />
|
|
<!-- publish_event -> Broker (Pub) -->
|
|
<path d="M 660,295 L 790,295 L 790,218" class="svg-arrow-line" marker-end="url(#arrow-flow)" />
|
|
<!-- Broker -> Subscriber (Deliver) -->
|
|
<path d="M 770,100 L 770,80 L 668,80" class="svg-arrow-line accent-teal svg-flow-path" marker-end="url(#arrow-flow)" />
|
|
<!-- Subscriber -> Audit Log -->
|
|
<path d="M 660,65 L 685,65 L 685,250 L 730,250 L 730,262" class="svg-arrow-line" marker-end="url(#arrow-flow)" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Diagram 2: Sequence Diagram -->
|
|
<div id="diag-seq" class="diagram-content">
|
|
<div class="svg-container">
|
|
<svg viewBox="0 0 850 580" width="100%" height="auto" class="diagram-svg">
|
|
<rect width="850" height="580" fill="none" />
|
|
<defs>
|
|
<marker id="seq-arrow-indigo" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#6366f1" />
|
|
</marker>
|
|
<marker id="seq-arrow-teal" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#0d9488" />
|
|
</marker>
|
|
<marker id="seq-arrow-muted" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="var(--fg-subtle)" />
|
|
</marker>
|
|
</defs>
|
|
|
|
<!-- Lifelines -->
|
|
<!-- Labels -->
|
|
<rect x="20" y="20" width="100" height="30" rx="4" class="svg-node-box" />
|
|
<text x="70" y="38" class="svg-txt-main" font-size="10" text-anchor="middle">CLI Wrapper</text>
|
|
<line x1="70" y1="50" x2="70" y2="560" class="svg-line" />
|
|
|
|
<rect x="160" y="20" width="100" height="30" rx="4" class="svg-node-box" />
|
|
<text x="210" y="38" class="svg-txt-main" font-size="10" text-anchor="middle">Registry</text>
|
|
<line x1="210" y1="50" x2="210" y2="560" class="svg-line" />
|
|
|
|
<rect x="300" y="20" width="100" height="30" rx="4" class="svg-node-box" />
|
|
<text x="350" y="38" class="svg-txt-main" font-size="10" text-anchor="middle">Subscriber</text>
|
|
<line x1="350" y1="50" x2="350" y2="560" class="svg-line" />
|
|
|
|
<rect x="440" y="20" width="100" height="30" rx="4" class="svg-node-box" />
|
|
<text x="490" y="38" class="svg-txt-main" font-size="10" text-anchor="middle">AI Agent (tmux)</text>
|
|
<line x1="490" y1="50" x2="490" y2="560" class="svg-line" />
|
|
|
|
<rect x="580" y="20" width="100" height="30" rx="4" class="svg-node-box" />
|
|
<text x="630" y="38" class="svg-txt-main" font-size="10" text-anchor="middle">MQTT Broker</text>
|
|
<line x1="630" y1="50" x2="630" y2="560" class="svg-line" />
|
|
|
|
<rect x="720" y="20" width="100" height="30" rx="4" class="svg-node-box" />
|
|
<text x="770" y="38" class="svg-txt-main" font-size="10" text-anchor="middle">Audit Log</text>
|
|
<line x1="770" y1="50" x2="770" y2="560" class="svg-line" />
|
|
|
|
<!-- Messages -->
|
|
<!-- 1. register -->
|
|
<line x1="70" y1="90" x2="210" y2="90" stroke="#6366f1" stroke-width="1.5" marker-end="url(#seq-arrow-indigo)" />
|
|
<text x="140" y="84" font-size="9" text-anchor="middle" fill="var(--fg-muted)">1. register_job (pending)</text>
|
|
|
|
<!-- 2. spawn subscriber -->
|
|
<line x1="70" y1="130" x2="350" y2="130" stroke="#6366f1" stroke-width="1.5" marker-end="url(#seq-arrow-indigo)" />
|
|
<text x="210" y="124" font-size="9" text-anchor="middle" fill="var(--fg-muted)">2. spawn subscriber (bg)</text>
|
|
|
|
<!-- 3. connect & subscribe -->
|
|
<line x1="350" y1="170" x2="630" y2="170" stroke="#0d9488" stroke-width="1.5" marker-end="url(#seq-arrow-teal)" />
|
|
<text x="490" y="164" font-size="9" text-anchor="middle" fill="var(--fg-muted)">3. connect & subscribe QoS 1</text>
|
|
|
|
<!-- 4. launch agent -->
|
|
<line x1="70" y1="210" x2="490" y2="210" stroke="var(--fg-muted)" stroke-width="1.2" marker-end="url(#seq-arrow-muted)" />
|
|
<text x="280" y="204" font-size="9" text-anchor="middle" fill="var(--fg-muted)">4. tmux launch (instructions with JID)</text>
|
|
|
|
<!-- 5. agent starts & publishes -->
|
|
<line x1="490" y1="260" x2="630" y2="260" stroke="#0d9488" stroke-width="1.5" marker-end="url(#seq-arrow-teal)" />
|
|
<text x="560" y="254" font-size="9" text-anchor="middle" fill="var(--fg-muted)">5. publish event="started"</text>
|
|
|
|
<!-- 6. broker delivers to subscriber -->
|
|
<line x1="630" y1="300" x2="350" y2="300" stroke="#0d9488" stroke-dasharray="2 2" stroke-width="1.5" marker-end="url(#seq-arrow-teal)" />
|
|
<text x="490" y="294" font-size="9" text-anchor="middle" fill="var(--fg-muted)">6. deliver event="started"</text>
|
|
|
|
<!-- 7. subscriber writes status running -->
|
|
<line x1="350" y1="340" x2="770" y2="340" stroke="var(--fg-muted)" stroke-width="1.2" marker-end="url(#seq-arrow-muted)" />
|
|
<text x="560" y="334" font-size="9" text-anchor="middle" fill="var(--fg-muted)">7. write logs (meta snapshot + events & status = "running")</text>
|
|
|
|
<!-- 8. agent completes tasks -->
|
|
<line x1="490" y1="400" x2="630" y2="400" stroke="#0d9488" stroke-width="1.5" marker-end="url(#seq-arrow-teal)" />
|
|
<text x="560" y="394" font-size="9" text-anchor="middle" fill="var(--fg-muted)">8. publish event="completed"</text>
|
|
|
|
<!-- 9. broker delivers completed -->
|
|
<line x1="630" y1="440" x2="350" y2="440" stroke="#0d9488" stroke-dasharray="2 2" stroke-width="1.5" marker-end="url(#seq-arrow-teal)" />
|
|
<text x="490" y="434" font-size="9" text-anchor="middle" fill="var(--fg-muted)">9. deliver event="completed"</text>
|
|
|
|
<!-- 10. log completed -->
|
|
<line x1="350" y1="480" x2="770" y2="480" stroke="var(--fg-muted)" stroke-width="1.2" marker-end="url(#seq-arrow-muted)" />
|
|
<text x="560" y="474" font-size="9" text-anchor="middle" fill="var(--fg-muted)">10. write logs (status = "completed")</text>
|
|
|
|
<!-- 11. subscriber exit -->
|
|
<line x1="350" y1="520" x2="70" y2="520" stroke="#6366f1" stroke-width="1.5" marker-end="url(#seq-arrow-indigo)" />
|
|
<text x="210" y="514" font-size="9" text-anchor="middle" fill="var(--fg-muted)">11. exit process (exit 0)</text>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Diagram 3: Component Map -->
|
|
<div id="diag-map" class="diagram-content">
|
|
<div class="svg-container">
|
|
<svg viewBox="0 0 800 320" width="100%" height="auto" class="diagram-svg">
|
|
<rect width="800" height="320" fill="none" />
|
|
<defs>
|
|
<marker id="map-arrow" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="var(--fg-muted)" />
|
|
</marker>
|
|
</defs>
|
|
|
|
<!-- Component Nodes -->
|
|
<!-- delegate-job (Bash) -->
|
|
<rect x="30" y="110" width="160" height="60" rx="6" class="svg-node-box" />
|
|
<text x="45" y="135" class="svg-txt-main" font-size="11">delegate-job</text>
|
|
<text x="45" y="152" class="svg-txt-sub" font-size="8">Bash orchestrator CLI</text>
|
|
|
|
<!-- registry.py -->
|
|
<rect x="260" y="30" width="160" height="60" rx="6" class="svg-node-box" />
|
|
<text x="275" y="55" class="svg-txt-main" font-size="11">registry.py</text>
|
|
<text x="275" y="72" class="svg-txt-sub" font-size="8">Registry CRUD CLI/Lib</text>
|
|
|
|
<!-- job_subscriber.py -->
|
|
<rect x="260" y="120" width="160" height="60" rx="6" class="svg-node-box" />
|
|
<text x="275" y="145" class="svg-txt-main" font-size="11">job_subscriber.py</text>
|
|
<text x="275" y="162" class="svg-txt-sub" font-size="8">MQTT event monitor</text>
|
|
|
|
<!-- publish_event.py -->
|
|
<rect x="260" y="210" width="160" height="60" rx="6" class="svg-node-box" />
|
|
<text x="275" y="235" class="svg-txt-main" font-size="11">publish_event.py</text>
|
|
<text x="275" y="252" class="svg-txt-sub" font-size="8">Status event emitter</text>
|
|
|
|
<!-- mqtt_common.py -->
|
|
<rect x="520" y="110" width="220" height="80" rx="6" class="svg-node-box" style="stroke: var(--accent)" />
|
|
<text x="535" y="135" class="svg-txt-main" font-size="12" fill="var(--accent)">mqtt_common.py</text>
|
|
<text x="535" y="155" class="svg-txt-sub" font-size="8.5">Shared library: Broker configuration,</text>
|
|
<text x="535" y="170" class="svg-txt-sub" font-size="8.5">Advisory fcntl lock, logging, files IO</text>
|
|
|
|
<!-- Arrows -->
|
|
<!-- delegate-job calls registry -->
|
|
<path d="M 190,125 L 215,125 L 215,60 L 252,60" class="svg-arrow-line" marker-end="url(#map-arrow)" />
|
|
<!-- delegate-job calls subscriber -->
|
|
<path d="M 190,140 L 252,140" class="svg-arrow-line" marker-end="url(#map-arrow)" />
|
|
|
|
<!-- Registry imports mqtt_common -->
|
|
<path d="M 420,60 L 480,60 L 480,125 L 512,125" class="svg-arrow-line" marker-end="url(#map-arrow)" />
|
|
<!-- Subscriber imports mqtt_common -->
|
|
<path d="M 420,150 L 512,150" class="svg-arrow-line" marker-end="url(#map-arrow)" />
|
|
<!-- Publisher imports mqtt_common -->
|
|
<path d="M 420,240 L 480,240 L 480,175 L 512,175" class="svg-arrow-line" marker-end="url(#map-arrow)" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 1 -->
|
|
<section id="what" class="section-card">
|
|
<h2>1. 이 스킬이 하는 일</h2>
|
|
<p><code>delegate-job</code> 스킬은 복잡한 자율 에이전트 오케스트레이션(작업 위임) 흐름을 단 한 줄의 명령어로 단순화합니다.</p>
|
|
|
|
<div class="callout tip">
|
|
<div class="callout-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
|
</div>
|
|
<div class="callout-body">
|
|
<h4>자동화 범위</h4>
|
|
<p>1. Job ID 신규 발급 및 독자적인 샌드박스 레지스트리 레코드 생성<br>
|
|
2. 이벤트 전송 유실 방지를 위해 에이전트 시작 전에 백그라운드 수신기를 선(先) 실행<br>
|
|
3. 격리된 <code>tmux</code> 인터랙티브 터미널 세션을 열어 작업 위임<br>
|
|
4. 에이전트가 발행한 실시간 상태 이벤트를 감지하여 NDJSON 포맷 감사 로그(Audit Log) 축적<br>
|
|
5. 최종 성공(completed) 또는 실패(error) 감지 시 자동으로 자원 반환 및 모니터링 해제</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 2 -->
|
|
<section id="install" class="section-card">
|
|
<h2>2. 설치 & 사전조건</h2>
|
|
<p>이 스킬은 POSIX 표준 개발 규격을 준수하며, 파이썬 기반의 백그라운드 구독 유틸리티와 통신 드라이버가 필요합니다.</p>
|
|
|
|
<h3>사전 시스템 사양</h3>
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>인프라 항목</th>
|
|
<th>기본 요건</th>
|
|
<th>활용 세부 영역</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>OS</td>
|
|
<td>macOS / Linux (Windows 환경인 경우 WSL 가상 환경 권장)</td>
|
|
<td>런타임 커널 지원</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Runtime</td>
|
|
<td>Python 3.9+</td>
|
|
<td>Registry / Publisher / Subscriber 연동</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Multiplexer</td>
|
|
<td>tmux (미설치 시 wrapper 기동 시 경고)</td>
|
|
<td>인터랙티브 모드 세션 격리 기동</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Library</td>
|
|
<td>paho-mqtt ≥ 2.0 (VERSION2 콜백 API 지원 사양)</td>
|
|
<td>MQTT 브로커 통신을 위한 로컬 파이썬 모듈</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3>스킬 저장소 파일 디렉터리 구성</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">Directory layout</span>
|
|
</div>
|
|
<pre><code>~/.hermes/skills/autonomous-ai-agents/delegate-job/
|
|
├── SKILL.md <span class="c"># Claude 에이전트가 해석하는 프롬프트 가이드 지침</span>
|
|
├── USER_MANUAL.html <span class="c"># 사용자 분석용 HTML 지침서 (본 문서)</span>
|
|
├── README.md <span class="c"># 간략한 프로젝트 정보</span>
|
|
├── job-protocol.md <span class="c"># MQTT JSON 페이로드 규격 및 스키마 명세</span>
|
|
├── registry.md <span class="c"># 레지스트리 레코드의 속성 값 정의 및 락 매커니즘</span>
|
|
├── mqtt-broker-setup.html <span class="c"># PoC → TLS 운영 브로커 컷오버 가이드 (HTML)</span>
|
|
├── delegate-job <span class="c"># 오케스트레이션 역할을 하는 Bash wrapper script</span>
|
|
└── scripts/
|
|
├── mqtt_common.py <span class="c"># 브로커 연결 빌드, 파일 락, 감사 로그 공통 모듈</span>
|
|
├── registry.py <span class="c"># Job 등록/수정을 관장하는 CLI 헬퍼 라이브러리</span>
|
|
├── publish_event.py <span class="c"># 구동 중인 에이전트가 이벤트를 쏠 때 쓰는 송신기</span>
|
|
└── job_subscriber.py <span class="c"># 위임 작업 종료 시점까지 백그라운드에서 감시하는 수신기</span></code></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 3 -->
|
|
<section id="quickstart" class="section-card">
|
|
<h2>3. Quick Start (작업 위임)</h2>
|
|
<p>단일 통합 명령어를 활용하여 즉시 위임 환경을 기동하거나, 빌드 파이프라인 연동 시 단계별 조작이 용이하도록 파이썬 직접 구동을 지원합니다.</p>
|
|
|
|
<div class="quickstart-tabs">
|
|
<div class="qs-tab-headers">
|
|
<button class="qs-tab-btn active" onclick="switchQsTab('qs-wrapper')" data-tab="qs-wrapper">1. 통합 래퍼 (Submit)</button>
|
|
<button class="qs-tab-btn" onclick="switchQsTab('qs-manual')" data-tab="qs-manual">2. 수동 5단계 조작</button>
|
|
</div>
|
|
|
|
<!-- Wrapper tab -->
|
|
<div id="qs-wrapper" class="qs-tab-panel active">
|
|
<p><code>delegate-job submit</code> 명령을 통하여 백그라운드 수신기를 구동하고 에이전트를 안전하게 격리 기동합니다.</p>
|
|
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">bash</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code>delegate-job submit \
|
|
--agent claude-code \
|
|
--prompt "정렬 문제 10개를 만들어 sort_problems.md로 저장" \
|
|
--workdir /path/to/project \
|
|
--agent-session tmux:demo \
|
|
--timeout 600 --idle-timeout 120</code></pre>
|
|
</div>
|
|
|
|
<h3>위임 시작 시 콘솔 실시간 로그 (stdout 예시)</h3>
|
|
<div class="code-container">
|
|
<pre><code>registered job: <span class="k">2971fbf8</span>
|
|
subscriber pid: <span class="n">34311</span> (log: .hermes/jobs/2971fbf8.subscriber.out)
|
|
agent launched in tmux session: demo (attach with: tmux attach -t demo)
|
|
subscriber output:
|
|
2026-06-19T13:16:00Z job=2971fbf8 seq=1 started Job 2971fbf8 started
|
|
2026-06-19T13:18:20Z job=2971fbf8 seq=2 completed saved sort_problems.md
|
|
subscriber exit code: 0
|
|
/path/to/project/.hermes/delegate_job_logs/2971fbf8 <span class="c"># Audit log 디렉터리 경로 반환</span></code></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manual Tab -->
|
|
<div id="qs-manual" class="qs-tab-panel">
|
|
<p>의존 관계 설정 및 디버깅을 위해 단계를 세분화하여 파이썬 런타임 스크립트를 기동할 수 있습니다.</p>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">bash</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code>PY=.venv/bin/python
|
|
SKILL=~/.hermes/skills/autonomous-ai-agents/delegate-job/scripts
|
|
|
|
<span class="c"># 1) 작업 정보 레코드 생성</span>
|
|
JID=$($PY "$SKILL/registry.py" register \
|
|
--prompt "정렬 파일 구성" --agent claude-code --agent-session tmux:demo \
|
|
--timeout 600 --idle-timeout 120)
|
|
|
|
<span class="c"># 2) 백그라운드 수신기 우선 실행 (순서 의존성 주의)</span>
|
|
$PY "$SKILL/job_subscriber.py" --job "$JID" --timeout 600 --idle-timeout 120 &
|
|
|
|
<span class="c"># 3) 시작(started) 상태 신호 수동 전송</span>
|
|
$PY "$SKILL/publish_event.py" --job "$JID" --event started
|
|
|
|
<span class="c"># 4) 에이전트 구동 과정... (에이전트가 직접 completed/error 신호를 쏘도록 지시)</span>
|
|
|
|
<span class="c"># 5) 조회 및 로그 상태 검증</span>
|
|
$PY "$SKILL/registry.py" get --job "$JID"
|
|
$PY "$SKILL/registry.py" logs "$JID"
|
|
$PY "$SKILL/registry.py" logs --list</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 4 -->
|
|
<section id="lifecycle" class="section-card">
|
|
<h2>4. Job Lifecycle</h2>
|
|
<p>각 위임 작업은 독립적인 분기가 적용되어 라이프사이클을 돌며 수신기의 exit code를 결정 짓습니다.</p>
|
|
|
|
<div class="lifecycle-viz">
|
|
<div class="lifecycle-state-card pending">
|
|
<div class="lifecycle-state-dot"></div>
|
|
<span class="lifecycle-state-name">pending</span>
|
|
<span class="lifecycle-state-desc">최초 등록 상태</span>
|
|
</div>
|
|
<div class="lifecycle-flow-arrow">→</div>
|
|
|
|
<div class="lifecycle-state-card running">
|
|
<div class="lifecycle-state-dot"></div>
|
|
<span class="lifecycle-state-name">running</span>
|
|
<span class="lifecycle-state-desc">시작 신호 접수</span>
|
|
</div>
|
|
<div class="lifecycle-flow-arrow">→</div>
|
|
|
|
<div class="lifecycle-state-card completed">
|
|
<div class="lifecycle-state-dot"></div>
|
|
<span class="lifecycle-state-name">completed</span>
|
|
<span class="lifecycle-state-desc">성공 종료</span>
|
|
</div>
|
|
|
|
<div class="lifecycle-state-card error">
|
|
<div class="lifecycle-state-dot"></div>
|
|
<span class="lifecycle-state-name">error</span>
|
|
<span class="lifecycle-state-desc">실패 종료</span>
|
|
</div>
|
|
|
|
<div class="lifecycle-state-card cancel">
|
|
<div class="lifecycle-state-dot"></div>
|
|
<span class="lifecycle-state-name">cancelled</span>
|
|
<span class="lifecycle-state-desc">취소 상태</span>
|
|
</div>
|
|
|
|
<div class="lifecycle-state-card timeout">
|
|
<div class="lifecycle-state-dot"></div>
|
|
<span class="lifecycle-state-name">timeout</span>
|
|
<span class="lifecycle-state-desc">제한시간 초과</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>프로세스 반환 코드(Exit Code) 사양</h3>
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>기동 스크립트</th>
|
|
<th>Exit Code</th>
|
|
<th>반환 의미 및 트리거 사유</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>job_subscriber.py</code></td>
|
|
<td><span style="color:var(--state-completed); font-weight:600">0</span></td>
|
|
<td>에이전트로부터 작업을 성공적으로 마쳤다는 <code>completed</code> 이벤트 수신 완료</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>job_subscriber.py</code></td>
|
|
<td><span style="color:var(--state-error); font-weight:600">1</span></td>
|
|
<td>작업 수행 실패 또는 처리 예외로 에이전트가 <code>error</code> 이벤트를 송출한 경우</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>job_subscriber.py</code></td>
|
|
<td><span style="color:var(--state-pending); font-weight:600">2</span></td>
|
|
<td>설정한 허용 제한 시간(timeout_sec) 또는 정체 제한(idle_timeout_sec) 임계치 도달</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>publish_event.py</code></td>
|
|
<td><span style="color:var(--state-completed); font-weight:600">0</span></td>
|
|
<td>MQTT 브로커 대상 QoS 1 발행 성공 및 PUBACK 응답 수신 완료</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>publish_event.py</code></td>
|
|
<td><span style="color:var(--state-error); font-weight:600">1</span></td>
|
|
<td>필수 파라미터 누락, 존재하지 않는 JID 참조 등 호출 설정 오류</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>publish_event.py</code></td>
|
|
<td><span style="color:var(--state-pending); font-weight:600">2</span></td>
|
|
<td>브로커 접속 실패 또는 네트워크 결함으로 지수 백오프 기반 3회 재시도 유실</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 5 -->
|
|
<section id="protocol" class="section-card">
|
|
<h2>5. 이벤트 프로토콜</h2>
|
|
<p>메시지 전송은 지연 및 간섭을 피하기 위해 <strong>1 Job당 1 전용 토픽</strong>을 준수합니다.</p>
|
|
|
|
<h3>MQTT 통신 토픽 규격</h3>
|
|
<div class="code-container">
|
|
<pre><code>python/mqtt/jobs/<span class="k"><job_id></span>/events</code></pre>
|
|
</div>
|
|
|
|
<h3>메시지 페이로드 스키마 명세</h3>
|
|
<p>전송하는 모든 데이터는 UTF-8 규격의 JSON 오브젝트 포맷을 따릅니다.</p>
|
|
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">JSON (schema_version = 1)</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code>{
|
|
"schema_version": 1,
|
|
"seq": 7, <span class="c">// 동일 JID 내 단조 증가하는 정수형 카운터</span>
|
|
"job_id": "abc12345",
|
|
"event": "started", <span class="c">// started | permission_required | progress | completed | error</span>
|
|
"timestamp": "2026-06-19T09:32:00Z", <span class="c">// ISO-8601 UTC 규격 시각</span>
|
|
"detail": "데이터베이스 쿼리 아티팩트 보관 완료",
|
|
"data": { <span class="c">// 확장 요구사항 반영을 위한 옵션 메타 딕셔너리</span>
|
|
"auth_token": "a1b2c3d4..."
|
|
}
|
|
}</code></pre>
|
|
</div>
|
|
|
|
<h3>Event Catalogue</h3>
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>이벤트명</th>
|
|
<th>의무 여부</th>
|
|
<th>발행 유발 시점</th>
|
|
<th>detail 기술 방식</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>started</code></td>
|
|
<td>필수 (seq=1)</td>
|
|
<td>에이전트가 작업을 할당받아 수행에 착수할 때</td>
|
|
<td><code>"Job <id> started"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>permission_required</code></td>
|
|
<td>옵션</td>
|
|
<td>에이전트가 시스템 파일 수정 등 사용자 승인이 필요할 때</td>
|
|
<td><code>"needs to write sort_problems.md"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>progress</code></td>
|
|
<td>옵션</td>
|
|
<td>중간 단계 수행 결과 알림</td>
|
|
<td><code>"creating problem 5/10"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>completed</code></td>
|
|
<td>종단 필수</td>
|
|
<td>위임 요구 명세를 온전히 이행하고 정상 종료할 때</td>
|
|
<td><code>"saved sort_problems.md"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>error</code></td>
|
|
<td>종단 필수</td>
|
|
<td>수행 도중 예외가 발생하거나 에이전트가 실패 종료될 때</td>
|
|
<td><code>"internal exception occurred"</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="callout warning">
|
|
<div class="callout-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
|
</div>
|
|
<div class="callout-body">
|
|
<h4>방어적 메시지 파싱 규칙 (Defensive Parsing)</h4>
|
|
<p>공개 인터넷 브로커(HiveMQ 등)는 제3자의 데이터 변조가 손쉽게 가능하므로, <code>job_subscriber.py</code>는 아래 검증을 거쳐 맞지 않는 유해 메시지를 무시하도록 설계되었습니다.</p>
|
|
<ul style="margin-top: 10px; font-size: 0.88rem; list-style-position: inside;">
|
|
<li>JSON 구문 포맷 에러 감지 시 무시 및 폐기</li>
|
|
<li><code>schema_version</code> 속성이 구독자 지원 범위(1)와 불일치 시 기각</li>
|
|
<li>수집 대기 중인 대상 JID가 아닌 엉뚱한 <code>job_id</code>를 가진 페이로드 차단</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 6 -->
|
|
<section id="audit" class="section-card">
|
|
<h2>6. Audit Log 자동 기록</h2>
|
|
<p>작업 실행 주기 동안 실시간 상태 흐름을 추적할 수 있도록 <code>.hermes/delegate_job_logs/<job_id>/</code> 폴더에 3종의 감사 추적 파일이 자동 축적됩니다.</p>
|
|
|
|
<h3>감사 기록 파일 정의</h3>
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>감사 로그 파일</th>
|
|
<th>포맷 규격</th>
|
|
<th>기록 내용 및 정보 범위</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>meta.json</code></td>
|
|
<td>JSON Object</td>
|
|
<td>최초 작업 할당 시점의 프롬프트 전문, 타임아웃 구성, 브로커 접속 스냅샷</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>events.ndjson</code></td>
|
|
<td>NDJSON</td>
|
|
<td>생성, 송신, 수신, 상태 전이의 단계별 세부 이력이 발생 시간순으로 한 줄씩 적재</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>status.json</code></td>
|
|
<td>JSON Object</td>
|
|
<td>현재 시점의 최종 런타임 결과값 저장 (속도 개선을 위한 포인트 필터용)</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3>시간 흐름별 감지 포인트</h3>
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>기록 액션 시점</th>
|
|
<th>events.ndjson 저장 포맷</th>
|
|
<th>쓰기 명령의 수행 주체</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Job 등록 완료 시</td>
|
|
<td><code>registered</code> (메타 데이터 구성 및 status.json 상태 pending 선 생성)</td>
|
|
<td><code>registry.py</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>작업 상태 변경 시</td>
|
|
<td><code>status_changed</code> (이전 상태 → 변경 상태 흐름)</td>
|
|
<td><code>job_subscriber.py</code> / <code>registry.py</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>에이전트 송신 완료</td>
|
|
<td><code>published</code> (송출 타임스탬프와 페이로드 스냅샷 포함)</td>
|
|
<td><code>publish_event.py</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>수신기 수신 완료</td>
|
|
<td><code>received</code> (수신 감지 성공 시각 및 세부 속성)</td>
|
|
<td><code>job_subscriber.py</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<p style="margin-top: 20px; font-size: 0.92rem;">
|
|
💡 <strong>운영 전환 검증 가이드</strong>: 위 종단 검증 시나리오는 공개 PoC 브로커를 활용한 테스트입니다.
|
|
사설 운영 브로커로 전환하여 이를 검증하고자 한다면, <a href="./mqtt-broker-setup.html" style="font-weight: 600; color: var(--accent); text-decoration: underline;">MQTT 브로커 셋업 가이드 →</a>의 연동 검증 시나리오 절차를 확인하십시오.
|
|
</p>
|
|
|
|
<div class="callout tip">
|
|
<div class="callout-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
|
</div>
|
|
<div class="callout-body">
|
|
<h4>로그 쓰기 트랜잭션 격리</h4>
|
|
<p>감사 로그 아티팩트 파일 쓰기는 메인 에이전트 동작 및 MQTT 발행 스레드를 보호하기 위해 철저한 <strong>Best-effort 방식</strong>으로 감싸져 실행됩니다. 파일 쓰기 중 동시성 경합 시 <code>try/except</code> 처리 및 OS fcntl 락, <code>logger.warning</code> 백업 처리를 통해 메인 통신 채널을 보장합니다.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 7 -->
|
|
<section id="commands" class="section-card">
|
|
<h2>7. 명령어 Reference</h2>
|
|
<p>통합 오케스트레이션 및 상태 점검을 지원하는 CLI 명령어입니다.</p>
|
|
|
|
<div class="search-box-container">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
|
<input type="text" id="cmd-search-input" class="search-box-input" placeholder="명령어 검색..." aria-label="Search command reference">
|
|
</div>
|
|
|
|
<div class="table-wrapper">
|
|
<table id="cmd-ref-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:140px">서브 커맨드</th>
|
|
<th>상세 역할 설명</th>
|
|
<th>기본 사용 기법</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>submit</code></td>
|
|
<td>작업 등록, 수신기 실행, 에이전트 tmux 기동 단계를 일괄 지시합니다.</td>
|
|
<td><code>delegate-job submit --agent claude-code --prompt "..."</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>status</code></td>
|
|
<td>레지스트리 레코드를 질의하여 작업의 세부 구성 정보를 조회합니다.</td>
|
|
<td><code>delegate-job status --job <id></code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>list</code></td>
|
|
<td>저장소에 보관된 작업 내역을 목록 형태로 일괄 조회합니다.</td>
|
|
<td><code>delegate-job list</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>verify</code></td>
|
|
<td>에이전트가 추출한 작업 산출물 아티팩트의 무결성 검증 스크립트를 작동시킵니다.</td>
|
|
<td><code>delegate-job verify --job <id> --validate ./validate.sh</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>wait</code></td>
|
|
<td>위임된 작업이 종단(completed/error) 상태를 맞이할 때까지 동기 대기합니다.</td>
|
|
<td><code>delegate-job wait --job <id></code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>logs</code></td>
|
|
<td>감사 로그 이력을 판독하여 타임라인 순으로 가공된 수행 기록을 출력합니다.</td>
|
|
<td><code>delegate-job logs <id></code> 또는 <code>delegate-job logs --list</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 8 -->
|
|
<section id="verify" class="section-card">
|
|
<h2>8. 종단 Smoke 검증 결과</h2>
|
|
<p>실제 로컬 호스트 및 공개 HiveMQ 브로커 상에서 정상 구동된 종단 검증 데이터(<code>job cb32569f</code>)의 정합성 리포트입니다.</p>
|
|
|
|
<h3>검증 재현 시나리오 명령어</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">bash</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code><span class="c"># 1) human 에이전트 용 임시 smoke 작업 생성</span>
|
|
JID=$(.venv/bin/python skills/delegate-job/scripts/registry.py \
|
|
--registry-dir .hermes/jobs register \
|
|
--prompt "smoke: flatten+resplit" --agent human --agent-session tmux:flatten-smoke \
|
|
--timeout 60 --idle-timeout 30 | tail -1)
|
|
|
|
<span class="c"># 2) 백그라운드 이벤트 감시 모니터 기동</span>
|
|
.venv/bin/python skills/delegate-job/scripts/job_subscriber.py \
|
|
--registry-dir .hermes/jobs --job "$JID" --timeout 60 --idle-timeout 30 &
|
|
|
|
<span class="c"># 3)started 라이프사이클 이벤트 강제 발행</span>
|
|
.venv/bin/python skills/delegate-job/scripts/publish_event.py \
|
|
--registry-dir .hermes/jobs --job "$JID" --event started \
|
|
--detail "flatten smoke started"
|
|
|
|
<span class="c"># 4) completed 종단 이벤트 강제 발행</span>
|
|
.venv/bin/python skills/delegate-job/scripts/publish_event.py \
|
|
--registry-dir .hermes/jobs --job "$JID" --event completed \
|
|
--detail "flatten smoke done"
|
|
|
|
<span class="c"># 5) 모니터 프로세스가 completed 접수 후 자동으로 자원 회수 종료 확인</span>
|
|
wait %1</code></pre>
|
|
</div>
|
|
|
|
<h3>실제 생성된 로그 아티팩트 명세 (job cb32569f)</h3>
|
|
|
|
<p><strong>events.ndjson 감사 기록 전문:</strong></p>
|
|
<div class="code-container">
|
|
<pre><code>{"event": "registered", "status": "pending", "agent": "human", "agent_session": "tmux:flatten-smoke", "topic_prefix": "python/mqtt/jobs/cb32569f", "timestamp": "2026-06-19T04:09:50Z", "logged_at": "2026-06-19T04:09:50.846Z"}
|
|
{"event": "received", "source_event": "started", "seq": 1, "topic": "python/mqtt/jobs/cb32569f/events", "timestamp": "2026-06-19T04:10:03Z", "detail": "flatten smoke started", "logged_at": "2026-06-19T04:10:11.548Z"}
|
|
{"event": "published", "source_event": "started", "seq": 1, "topic": "python/mqtt/jobs/cb32569f/events", "retain": false, "timestamp": "2026-06-19T04:10:03Z", "detail": "flatten smoke started", "payload": {"schema_version": 1, "seq": 1, "job_id": "cb32569f", "event": "started", "timestamp": "2026-06-19T04:10:03Z", "detail": "flatten smoke started", "data": {}}, "logged_at": "2026-06-19T04:10:12.555Z"}
|
|
{"event": "status_changed", "from": "pending", "to": "running", "timestamp": "2026-06-19T04:10:12Z", "logged_at": "2026-06-19T04:10:12.558Z"}
|
|
{"event": "received", "source_event": "completed", "seq": 2, "topic": "python/mqtt/jobs/cb32569f/events", "timestamp": "2026-06-19T04:10:13Z", "detail": "flatten smoke done", "logged_at": "2026-06-19T04:10:16.927Z"}
|
|
{"event": "published", "source_event": "completed", "seq": 2, "topic": "python/mqtt/jobs/cb32569f/events", "retain": true, "timestamp": "2026-06-19T04:10:13Z", "detail": "flatten smoke done", "payload": {"schema_version": 1, "seq": 2, "job_id": "cb32569f", "event": "completed", "timestamp": "2026-06-19T04:10:13Z", "detail": "flatten smoke done", "data": {}}, "logged_at": "2026-06-19T04:10:17.932Z"}
|
|
{"event": "status_changed", "from": "running", "to": "completed", "timestamp": "2026-06-19T04:10:17Z", "logged_at": "2026-06-19T04:10:17.935Z"}</code></pre>
|
|
</div>
|
|
|
|
<p><strong>status.json 실시간 결과 캐시 스냅샷:</strong></p>
|
|
<div class="code-container">
|
|
<pre><code>{
|
|
"job_id": "cb32569f",
|
|
"status": "completed",
|
|
"updated_at": "2026-06-19T04:10:17Z"
|
|
}</code></pre>
|
|
</div>
|
|
|
|
<ul class="checklist">
|
|
<li class="ok"><strong>이벤트 정합성 검증 완료:</strong> 등록 → 구독 → 2회 발행 후 수신기가 정상 복귀 코드 0을 반환하며 종료</li>
|
|
<li class="ok"><strong>감사 로그 누락 방지 확인:</strong> <code>events.ndjson</code> 파일 내 시간순 정합 기록 확인 완료</li>
|
|
<li class="ok"><strong>데이터 캐싱 정합성 검증:</strong> 레지스트리의 status와 감사 로그 내 status.json 캐시 값이 완성 동기화됨</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<!-- Section 9 -->
|
|
<section id="pitfalls" class="section-card">
|
|
<h2>9. 자주 빠지는 함정</h2>
|
|
<p>자율 에이전트 비동기 위임 환경 구성 및 운영 셋업 중에 봉착하기 쉬운 결함 시나리오입니다.</p>
|
|
|
|
<div style="display:flex; flex-direction:column; gap:24px;">
|
|
<div style="border-left: 3px solid var(--state-error); padding-left: 16px;">
|
|
<h4 style="font-size:1.05rem; font-family:var(--font-display); margin-bottom:6px;">① 오래된 Job ID의 에이전트 프롬프트 하드코딩 에러</h4>
|
|
<p style="font-size:0.92rem; color:var(--fg-muted);"><strong>사유:</strong> 에이전트 기동 시 무작위 ID가 새로이 발급됩니다. 과거 세션의 ID를 에이전트 기동 프롬프트에 고정 지정해 주면, 에이전트가 다른 Job ID 채널로 이벤트를 쏘고, 감시 수신기는 무반응 타임아웃(exit 2)에 처하게 됩니다.</p>
|
|
<p style="font-size:0.92rem; color:var(--accent-teal); font-weight:600; margin-top:4px;">→ 해결책: 래퍼 실행 프롬프트에 <code>--job "$JOB_ID"</code> 옵션을 넘겨 신규 JID 값을 주입 하십시오.</p>
|
|
</div>
|
|
|
|
<div style="border-left: 3px solid var(--state-error); padding-left: 16px;">
|
|
<h4 style="font-size:1.05rem; font-family:var(--font-display); margin-bottom:6px;">② 수신기 실행 전 이벤트 선 송출 유실 결함</h4>
|
|
<p style="font-size:0.92rem; color:var(--fg-muted);"><strong>사유:</strong> MQTT 브로커 통신의 일반 비영속화 메시지는 구독 대기 중인 채널이 없으면 소멸합니다. 수신기를 백그라운드에 올리지 않은 상태에서 에이전트가 이벤트를 쏘면 데이터가 소실되어 프로세스가 정지합니다.</p>
|
|
<p style="font-size:0.92rem; color:var(--accent-teal); font-weight:600; margin-top:4px;">→ 해결책: 래퍼 submit 동작을 쓰거나, <code>completed</code> 발행 시 MQTT <code>retain=true</code> 처리를 병행하십시오.</p>
|
|
</div>
|
|
|
|
<div style="border-left: 3px solid var(--state-error); padding-left: 16px;">
|
|
<h4 style="font-size:1.05rem; font-family:var(--font-display); margin-bottom:6px;">③ tmux 인터랙티브 모드에서의 세션 명칭 중복 간섭</h4>
|
|
<p style="font-size:0.92rem; color:var(--fg-muted);"><strong>사유:</strong> <code>--agent-session</code>에 고정 문자열(예: tmux:claude)을 기입한 채 복수 작업을 동시 실행하면, tmux 세션이 이미 점유 상태여서 프로세스가 기동 실패를 반환합니다.</p>
|
|
<p style="font-size:0.92rem; color:var(--accent-teal); font-weight:600; margin-top:4px;">→ 해결책: 위임 건수마다 UUID 또는 JID 기반 고유 세션명을 매핑 기동하십시오.</p>
|
|
</div>
|
|
|
|
<div style="border-left: 3px solid var(--state-error); padding-left: 16px;">
|
|
<h4 style="font-size:1.05rem; font-family:var(--font-display); margin-bottom:6px;">④ 공개 테스트 브로커 보안 위협 요소</h4>
|
|
<p style="font-size:0.92rem; color:var(--fg-muted);"><strong>사유:</strong> HiveMQ 공개 브로커는 누구든지 메시지를 구독/발행 가능하므로 스니핑 및 인젝션 해킹 위협에 완전 노출되어 있습니다.</p>
|
|
<p style="font-size:0.92rem; color:var(--accent-teal); font-weight:600; margin-top:4px;">→ 해결책: 내부망에 전용 Mosquitto 브로커를 올리고 TLS 8883 및 Bearer <code>auth_token</code> 인증 로직을 전개하십시오.</p>
|
|
</div>
|
|
|
|
<div style="border-left: 3px solid var(--state-error); padding-left: 16px;">
|
|
<h4 style="font-size:1.05rem; font-family:var(--font-display); margin-bottom:6px;">⑤ detail / data 속성 내 보안 자격 키 노출 위험</h4>
|
|
<p style="font-size:0.92rem; color:var(--fg-muted);"><strong>사유:</strong> MQTT 전송 페이로드의 detail 및 data에 로컬 절대 경로, 액세스 토큰 등을 여과 없이 주입하면, 타인에게 시스템 정보가 노출됩니다.</p>
|
|
<p style="font-size:0.92rem; color:var(--accent-teal); font-weight:600; margin-top:4px;">→ 해결책: 전송 값은 추상화된 이력 텍스트만 싣고, 기밀 파라미터는 로컬 Registry 레코드를 통해서만 참조 하십시오.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 10 -->
|
|
<section id="production" class="section-card">
|
|
<h2>10. 운영 Broker 전환</h2>
|
|
<p>보안 통제 및 격리가 요구되는 비즈니스 인프라로 이관하기 위한 독립 브로커 구성 기법입니다. 스크립트 수정 없이 환경변수 조율만으로 이행됩니다.</p>
|
|
|
|
<!-- Detailed Step-by-Step Guide Link Card -->
|
|
<div class="setup-guide-card">
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" style="flex-shrink: 0;">
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
</svg>
|
|
<span style="font-size: 0.92rem; font-weight: 600; color: var(--fg-base);">자세한 단계별 가이드: MQTT 브로커 셋업 (Mosquitto/EMQX, TLS, ACL, Cut-over)</span>
|
|
</div>
|
|
<a href="./mqtt-broker-setup.html" class="setup-banner-btn" style="padding: 8px 14px; font-size: 0.82rem; flex-shrink: 0;">가이드 읽기 →</a>
|
|
</div>
|
|
|
|
<h3>환경 주입 파일 (mqtt.env 예시)</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">bash</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code>export MQTT_BROKER=mqtt.internal.example.com
|
|
export MQTT_PORT=8883
|
|
export MQTT_TLS=1
|
|
export MQTT_CA_CERTS=/etc/ssl/certs/internal-ca.pem
|
|
export MQTT_USERNAME=hermes-operator
|
|
export MQTT_PASSWORD=secure_token_key
|
|
|
|
<span class="c"># 환경 주입</span>
|
|
source mqtt.env
|
|
|
|
<span class="c"># 위임 submit 스크립트 재시동 (스크립트 소스코드 수정 무)</span>
|
|
delegate-job submit --agent claude-code --prompt "운영 서버 이관 점검"</code></pre>
|
|
</div>
|
|
|
|
<div class="callout warning">
|
|
<div class="callout-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
|
</div>
|
|
<div class="callout-body">
|
|
<h4>우선순위 역전 장애 (Broker Precedence Pitfall)</h4>
|
|
<p>공통 모듈인 <code>broker_config_from_job()</code>는 환경 변수 로드 후 레지스트리 JSON 파일의 <code>broker.*</code> 속성을 덮어씌워 합성(Merge)합니다.</p>
|
|
<p style="margin-top:8px;">즉, 쉘 터미널 상에 새로운 <code>MQTT_BROKER</code> 호스트 환경변수를 주입하더라도, <strong>이미 등록되어 구동 대기 중이던 과거 레지스트리 레코드 파일</strong> 내부에 HiveMQ PoC 브로커 주소가 하드코딩 되어 있다면, 변경된 주소가 무시되어 과거 브로커로 접속하게 됩니다. 반드시 신규 작업을 다시 등록(register)하거나, 레코드 JSON 파일(<code>.hermes/jobs/<id>.json</code>)을 수동 편집하여 <code>broker.host</code> 항목을 수동 업데이트한 후 구동 하십시오.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Bottom Nav Cards -->
|
|
<nav class="nav-footer-cards" aria-label="Documentation navigation">
|
|
<a href="./SKILL.md" class="nav-card prev">
|
|
<span class="nav-card-label">이전 문서</span>
|
|
<span class="nav-card-title">← SKILL.md 가이드</span>
|
|
</a>
|
|
<a href="./mqtt-broker-setup.html" class="nav-card next">
|
|
<span class="nav-card-label">다음 문서</span>
|
|
<span class="nav-card-title">MQTT 브로커 셋업 가이드 →</span>
|
|
</a>
|
|
</nav>
|
|
|
|
<div class="back-to-top">
|
|
<a href="#" class="back-to-top-link">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"></polyline></svg>
|
|
맨 위로 이동
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer>
|
|
<div>최종 검증 및 정합 완료일: 2026-06-19</div>
|
|
<div>참조 데이터 Job ID: <span style="font-family:var(--font-mono)">cb32569f</span></div>
|
|
<div>인프라: broker.hivemq.com:1883</div>
|
|
</footer>
|
|
|
|
</main>
|
|
|
|
<!-- 3. Right Sidebar outlining document layout -->
|
|
<aside class="right-sidebar">
|
|
<div class="sidebar-title" style="margin-bottom: 12px">본문 구성</div>
|
|
<nav aria-label="Table of contents">
|
|
<a href="#architecture" class="toc-link">00. 아키텍처 다이어그램</a>
|
|
<a href="#what" class="toc-link">01. 이 스킬이 하는 일</a>
|
|
<a href="#install" class="toc-link">02. 설치 & 사전조건</a>
|
|
<a href="#quickstart" class="toc-link">03. Quick Start (작업 위임)</a>
|
|
<a href="#lifecycle" class="toc-link">04. Job Lifecycle</a>
|
|
<a href="#protocol" class="toc-link">05. 이벤트 프로토콜</a>
|
|
<a href="#audit" class="toc-link">06. Audit Log 자동 기록</a>
|
|
<a href="#commands" class="toc-link">07. 명령어 Reference</a>
|
|
<a href="#verify" class="toc-link">08. 종단 Smoke 검증 결과</a>
|
|
<a href="#pitfalls" class="toc-link">09. 자주 빠지는 함정</a>
|
|
<a href="#production" class="toc-link">10. 운영 Broker 전환</a>
|
|
</nav>
|
|
</aside>
|
|
|
|
</div>
|
|
|
|
<!-- Client JavaScript Interactions -->
|
|
<script>
|
|
// 1. Switch Interactive Architecture SVGs
|
|
function switchDiagram(tabId) {
|
|
document.querySelectorAll('.diagram-tab-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.diagram-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
|
|
const activeBtn = document.querySelector(`[data-tab="${tabId}"]`);
|
|
const activeContent = document.getElementById(tabId);
|
|
if (activeBtn && activeContent) {
|
|
activeBtn.classList.add('active');
|
|
activeContent.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// 2. Switch Quick Start CLI Command Tab Panels
|
|
function switchQsTab(tabId) {
|
|
document.querySelectorAll('.qs-tab-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.qs-tab-panel').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
|
|
const activeBtn = document.querySelector(`[data-tab="${tabId}"]`);
|
|
const activeContent = document.getElementById(tabId);
|
|
if (activeBtn && activeContent) {
|
|
activeBtn.classList.add('active');
|
|
activeContent.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// 3. Clipboard Copy button triggers
|
|
function copyCodeText(btn) {
|
|
const container = btn.closest('.code-container');
|
|
const codeElement = container.querySelector('pre code');
|
|
navigator.clipboard.writeText(codeElement.innerText).then(() => {
|
|
const textSpan = btn.querySelector('span');
|
|
const originalText = textSpan.innerText;
|
|
btn.classList.add('copied');
|
|
textSpan.innerText = '복사됨!';
|
|
setTimeout(() => {
|
|
btn.classList.remove('copied');
|
|
textSpan.innerText = originalText;
|
|
}, 1500);
|
|
});
|
|
}
|
|
|
|
// 4. Command reference dynamic listing filtering
|
|
const searchInput = document.getElementById('cmd-search-input');
|
|
searchInput.addEventListener('input', (e) => {
|
|
const val = e.target.value.toLowerCase().trim();
|
|
document.querySelectorAll('#cmd-ref-table tbody tr').forEach(row => {
|
|
const name = row.cells[0].innerText.toLowerCase();
|
|
const desc = row.cells[1].innerText.toLowerCase();
|
|
if (name.includes(val) || desc.includes(val)) {
|
|
row.style.display = '';
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
// 5. Scroll spy outline navigation highlighter
|
|
const observerOptions = {
|
|
root: null,
|
|
rootMargin: '0px 0px -60% 0px',
|
|
threshold: 0
|
|
};
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const id = entry.target.getAttribute('id');
|
|
document.querySelectorAll('.toc-link').forEach(link => {
|
|
if (link.getAttribute('href') === `#${id}`) {
|
|
link.classList.add('active');
|
|
} else {
|
|
link.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
document.querySelectorAll('.section-card').forEach(sec => {
|
|
observer.observe(sec);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|