@smilintux/skcapstone 0.4.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/skcapstone/blueprint_registry.py +78 -0
- package/src/skcapstone/cli/__init__.py +2 -0
- package/src/skcapstone/cli/itil.py +434 -0
- package/src/skcapstone/cli/skills_cmd.py +90 -26
- package/src/skcapstone/cli/soul.py +47 -24
- package/src/skcapstone/consciousness_config.py +27 -0
- package/src/skcapstone/coordination.py +1 -0
- package/src/skcapstone/daemon.py +28 -9
- package/src/skcapstone/dreaming.py +761 -0
- package/src/skcapstone/itil.py +1104 -0
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +1 -1
- package/src/skcapstone/mcp_tools/itil_tools.py +657 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +12 -11
- package/src/skcapstone/notifications.py +40 -27
- package/src/skcapstone/scheduled_tasks.py +107 -0
- package/src/skcapstone/service_health.py +81 -2
- package/src/skcapstone/soul.py +19 -0
package/package.json
CHANGED
|
@@ -355,3 +355,81 @@ class BlueprintRegistryClient:
|
|
|
355
355
|
return True
|
|
356
356
|
except BlueprintRegistryError:
|
|
357
357
|
return False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# --------------------------------------------------------------------------
|
|
361
|
+
# GitHub-based fallback — reads blueprints directly from the repo
|
|
362
|
+
# --------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
_GITHUB_API_URL = "https://api.github.com/repos/smilinTux/soul-blueprints/contents/blueprints"
|
|
365
|
+
_GITHUB_RAW_URL = "https://raw.githubusercontent.com/smilinTux/soul-blueprints/main/blueprints"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _fetch_github_blueprints(query: str = "") -> Optional[list[dict[str, Any]]]:
|
|
369
|
+
"""Fetch blueprint listings from the soul-blueprints GitHub repo.
|
|
370
|
+
|
|
371
|
+
Uses the GitHub Contents API to list category directories, then
|
|
372
|
+
fetches file names from each. Lightweight header parsing is done
|
|
373
|
+
via raw file fetch for descriptions.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
query: Optional search filter (case-insensitive).
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
List of blueprint dicts, or None on failure.
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
# Get top-level categories
|
|
383
|
+
req = urllib.request.Request(
|
|
384
|
+
_GITHUB_API_URL,
|
|
385
|
+
headers={"User-Agent": "skcapstone", "Accept": "application/json"},
|
|
386
|
+
)
|
|
387
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
388
|
+
categories = json.loads(resp.read().decode("utf-8"))
|
|
389
|
+
except Exception as exc:
|
|
390
|
+
logger.debug("GitHub blueprint fetch failed: %s", exc)
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
blueprints: list[dict[str, Any]] = []
|
|
394
|
+
q = query.lower()
|
|
395
|
+
|
|
396
|
+
for cat_entry in categories:
|
|
397
|
+
if cat_entry.get("type") != "dir":
|
|
398
|
+
continue
|
|
399
|
+
cat_name = cat_entry["name"]
|
|
400
|
+
|
|
401
|
+
# Fetch files in this category
|
|
402
|
+
try:
|
|
403
|
+
cat_req = urllib.request.Request(
|
|
404
|
+
cat_entry["url"],
|
|
405
|
+
headers={"User-Agent": "skcapstone", "Accept": "application/json"},
|
|
406
|
+
)
|
|
407
|
+
with urllib.request.urlopen(cat_req, timeout=10) as resp:
|
|
408
|
+
files = json.loads(resp.read().decode("utf-8"))
|
|
409
|
+
except Exception:
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
for file_entry in files:
|
|
413
|
+
fname = file_entry.get("name", "")
|
|
414
|
+
if not fname.lower().endswith((".md", ".yaml", ".yml")):
|
|
415
|
+
continue
|
|
416
|
+
if fname.lower() in ("readme.md", "index.html"):
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
stem = fname.rsplit(".", 1)[0]
|
|
420
|
+
slug = stem.lower().replace("_", "-").replace(" ", "-")
|
|
421
|
+
display = stem.replace("_", " ").replace("-", " ").title()
|
|
422
|
+
|
|
423
|
+
# Apply search filter
|
|
424
|
+
if q and q not in slug and q not in cat_name.lower() and q not in display.lower():
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
blueprints.append({
|
|
428
|
+
"name": slug,
|
|
429
|
+
"display_name": display,
|
|
430
|
+
"category": cat_name,
|
|
431
|
+
"source": "github",
|
|
432
|
+
"raw_url": f"{_GITHUB_RAW_URL}/{cat_name}/{fname}",
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
return sorted(blueprints, key=lambda d: (d["category"], d["name"]))
|
|
@@ -86,6 +86,7 @@ from .search_cmd import register_search_commands
|
|
|
86
86
|
from .mood_cmd import register_mood_commands
|
|
87
87
|
from .register_cmd import register_register_commands
|
|
88
88
|
from .gtd import register_gtd_commands
|
|
89
|
+
from .itil import register_itil_commands
|
|
89
90
|
from .skseed import register_skseed_commands
|
|
90
91
|
from .service_cmd import register_service_commands
|
|
91
92
|
from .telegram import register_telegram_commands
|
|
@@ -138,6 +139,7 @@ register_search_commands(main)
|
|
|
138
139
|
register_mood_commands(main)
|
|
139
140
|
register_register_commands(main)
|
|
140
141
|
register_gtd_commands(main)
|
|
142
|
+
register_itil_commands(main)
|
|
141
143
|
register_skseed_commands(main)
|
|
142
144
|
register_service_commands(main)
|
|
143
145
|
register_telegram_commands(main)
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""ITIL service management CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ._common import AGENT_HOME, SHARED_ROOT, console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_itil_commands(main: click.Group) -> None:
|
|
15
|
+
"""Register the itil command group."""
|
|
16
|
+
|
|
17
|
+
@main.group()
|
|
18
|
+
def itil():
|
|
19
|
+
"""ITIL service management — incidents, problems, changes."""
|
|
20
|
+
|
|
21
|
+
# ── itil status ───────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
@itil.command("status")
|
|
24
|
+
def itil_status():
|
|
25
|
+
"""Show ITIL dashboard: open incidents, active problems, pending changes."""
|
|
26
|
+
from ..itil import ITILManager
|
|
27
|
+
|
|
28
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
29
|
+
status = mgr.get_status()
|
|
30
|
+
|
|
31
|
+
inc = status["incidents"]
|
|
32
|
+
prb = status["problems"]
|
|
33
|
+
chg = status["changes"]
|
|
34
|
+
kedb = status["kedb"]
|
|
35
|
+
|
|
36
|
+
console.print(f"\n[bold]ITIL Dashboard[/bold]")
|
|
37
|
+
console.print(f" Incidents: [red]{inc['open']}[/red] open / {inc['total']} total")
|
|
38
|
+
for sev, count in inc.get("by_severity", {}).items():
|
|
39
|
+
if count:
|
|
40
|
+
console.print(f" {sev}: {count}")
|
|
41
|
+
console.print(f" Problems: [yellow]{prb['active']}[/yellow] active / {prb['total']} total")
|
|
42
|
+
console.print(f" Changes: [blue]{chg['pending']}[/blue] pending / {chg['total']} total")
|
|
43
|
+
console.print(f" KEDB: {kedb['total']} entries")
|
|
44
|
+
|
|
45
|
+
if inc["open_list"]:
|
|
46
|
+
console.print(f"\n[bold red]Open Incidents:[/bold red]")
|
|
47
|
+
for i in inc["open_list"]:
|
|
48
|
+
console.print(
|
|
49
|
+
f" [{i['id']}] {i['severity'].upper()} {i['title']} "
|
|
50
|
+
f"({i['status']}) @{i['managed_by']}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if chg["pending_list"]:
|
|
54
|
+
console.print(f"\n[bold blue]Pending Changes:[/bold blue]")
|
|
55
|
+
for c in chg["pending_list"]:
|
|
56
|
+
console.print(
|
|
57
|
+
f" [{c['id']}] {c['title']} ({c['status']}, "
|
|
58
|
+
f"{c['change_type']}) @{c['managed_by']}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
console.print()
|
|
62
|
+
|
|
63
|
+
# ── itil incident ─────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
@itil.group()
|
|
66
|
+
def incident():
|
|
67
|
+
"""Incident management."""
|
|
68
|
+
|
|
69
|
+
@incident.command("create")
|
|
70
|
+
@click.option("--title", "-t", required=True, help="Incident title")
|
|
71
|
+
@click.option(
|
|
72
|
+
"--severity", "-s", default="sev3",
|
|
73
|
+
type=click.Choice(["sev1", "sev2", "sev3", "sev4"]),
|
|
74
|
+
help="Severity level",
|
|
75
|
+
)
|
|
76
|
+
@click.option("--service", multiple=True, help="Affected service(s)")
|
|
77
|
+
@click.option("--impact", default="", help="Business impact")
|
|
78
|
+
@click.option("--by", "managed_by", default="human", help="Managing agent")
|
|
79
|
+
@click.option("--tag", multiple=True, help="Tags")
|
|
80
|
+
def incident_create(title, severity, service, impact, managed_by, tag):
|
|
81
|
+
"""Create a new incident."""
|
|
82
|
+
from ..itil import ITILManager
|
|
83
|
+
|
|
84
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
85
|
+
inc = mgr.create_incident(
|
|
86
|
+
title=title,
|
|
87
|
+
severity=severity,
|
|
88
|
+
affected_services=list(service),
|
|
89
|
+
impact=impact,
|
|
90
|
+
managed_by=managed_by,
|
|
91
|
+
created_by=managed_by,
|
|
92
|
+
tags=list(tag),
|
|
93
|
+
)
|
|
94
|
+
console.print(
|
|
95
|
+
f"\n [green]Created:[/green] {inc.id} — {inc.title} "
|
|
96
|
+
f"({inc.severity.value}, {inc.status.value})"
|
|
97
|
+
)
|
|
98
|
+
if inc.gtd_item_ids:
|
|
99
|
+
console.print(f" [dim]GTD item(s): {', '.join(inc.gtd_item_ids)}[/dim]")
|
|
100
|
+
console.print()
|
|
101
|
+
|
|
102
|
+
@incident.command("list")
|
|
103
|
+
@click.option(
|
|
104
|
+
"--status", type=click.Choice([
|
|
105
|
+
"detected", "acknowledged", "investigating",
|
|
106
|
+
"escalated", "resolved", "closed",
|
|
107
|
+
]),
|
|
108
|
+
help="Filter by status",
|
|
109
|
+
)
|
|
110
|
+
@click.option(
|
|
111
|
+
"--severity",
|
|
112
|
+
type=click.Choice(["sev1", "sev2", "sev3", "sev4"]),
|
|
113
|
+
help="Filter by severity",
|
|
114
|
+
)
|
|
115
|
+
@click.option("--service", help="Filter by affected service")
|
|
116
|
+
def incident_list(status, severity, service):
|
|
117
|
+
"""List incidents."""
|
|
118
|
+
from ..itil import ITILManager
|
|
119
|
+
|
|
120
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
121
|
+
incidents = mgr.list_incidents(status=status, severity=severity, service=service)
|
|
122
|
+
|
|
123
|
+
if not incidents:
|
|
124
|
+
console.print("\n [dim]No incidents found[/dim]\n")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
console.print(f"\n[bold]Incidents ({len(incidents)}):[/bold]")
|
|
128
|
+
for i in incidents:
|
|
129
|
+
sev = i.severity.value.upper()
|
|
130
|
+
console.print(
|
|
131
|
+
f" [{i.id}] {sev} {i.title} ({i.status.value}) @{i.managed_by}"
|
|
132
|
+
)
|
|
133
|
+
console.print()
|
|
134
|
+
|
|
135
|
+
@incident.command("update")
|
|
136
|
+
@click.argument("incident_id")
|
|
137
|
+
@click.option("--agent", default="human", help="Agent making the update")
|
|
138
|
+
@click.option(
|
|
139
|
+
"--status", "new_status",
|
|
140
|
+
type=click.Choice([
|
|
141
|
+
"acknowledged", "investigating", "escalated", "resolved", "closed",
|
|
142
|
+
]),
|
|
143
|
+
help="New status",
|
|
144
|
+
)
|
|
145
|
+
@click.option(
|
|
146
|
+
"--severity",
|
|
147
|
+
type=click.Choice(["sev1", "sev2", "sev3", "sev4"]),
|
|
148
|
+
help="New severity",
|
|
149
|
+
)
|
|
150
|
+
@click.option("--note", default="", help="Timeline note")
|
|
151
|
+
@click.option("--resolution", default=None, help="Resolution summary")
|
|
152
|
+
def incident_update(incident_id, agent, new_status, severity, note, resolution):
|
|
153
|
+
"""Update an incident status or metadata."""
|
|
154
|
+
from ..itil import ITILManager
|
|
155
|
+
|
|
156
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
157
|
+
try:
|
|
158
|
+
inc = mgr.update_incident(
|
|
159
|
+
incident_id=incident_id,
|
|
160
|
+
agent=agent,
|
|
161
|
+
new_status=new_status,
|
|
162
|
+
severity=severity,
|
|
163
|
+
note=note,
|
|
164
|
+
resolution_summary=resolution,
|
|
165
|
+
)
|
|
166
|
+
console.print(
|
|
167
|
+
f"\n [green]Updated:[/green] {inc.id} -> {inc.status.value} "
|
|
168
|
+
f"({inc.severity.value})\n"
|
|
169
|
+
)
|
|
170
|
+
except ValueError as exc:
|
|
171
|
+
console.print(f"\n [red]Error:[/red] {exc}\n")
|
|
172
|
+
|
|
173
|
+
# ── itil problem ──────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
@itil.group()
|
|
176
|
+
def problem():
|
|
177
|
+
"""Problem management."""
|
|
178
|
+
|
|
179
|
+
@problem.command("create")
|
|
180
|
+
@click.option("--title", "-t", required=True, help="Problem title")
|
|
181
|
+
@click.option("--by", "managed_by", default="human", help="Managing agent")
|
|
182
|
+
@click.option("--incident", "incident_ids", multiple=True, help="Related incident ID(s)")
|
|
183
|
+
@click.option("--workaround", default="", help="Known workaround")
|
|
184
|
+
@click.option("--tag", multiple=True, help="Tags")
|
|
185
|
+
def problem_create(title, managed_by, incident_ids, workaround, tag):
|
|
186
|
+
"""Create a new problem record."""
|
|
187
|
+
from ..itil import ITILManager
|
|
188
|
+
|
|
189
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
190
|
+
prb = mgr.create_problem(
|
|
191
|
+
title=title,
|
|
192
|
+
managed_by=managed_by,
|
|
193
|
+
created_by=managed_by,
|
|
194
|
+
related_incident_ids=list(incident_ids),
|
|
195
|
+
workaround=workaround,
|
|
196
|
+
tags=list(tag),
|
|
197
|
+
)
|
|
198
|
+
console.print(
|
|
199
|
+
f"\n [green]Created:[/green] {prb.id} — {prb.title} ({prb.status.value})\n"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@problem.command("list")
|
|
203
|
+
@click.option(
|
|
204
|
+
"--status",
|
|
205
|
+
type=click.Choice(["identified", "analyzing", "known_error", "resolved"]),
|
|
206
|
+
help="Filter by status",
|
|
207
|
+
)
|
|
208
|
+
def problem_list(status):
|
|
209
|
+
"""List problems."""
|
|
210
|
+
from ..itil import ITILManager
|
|
211
|
+
|
|
212
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
213
|
+
problems = mgr.list_problems(status=status)
|
|
214
|
+
|
|
215
|
+
if not problems:
|
|
216
|
+
console.print("\n [dim]No problems found[/dim]\n")
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
console.print(f"\n[bold]Problems ({len(problems)}):[/bold]")
|
|
220
|
+
for p in problems:
|
|
221
|
+
console.print(
|
|
222
|
+
f" [{p.id}] {p.title} ({p.status.value}) @{p.managed_by}"
|
|
223
|
+
)
|
|
224
|
+
console.print()
|
|
225
|
+
|
|
226
|
+
@problem.command("update")
|
|
227
|
+
@click.argument("problem_id")
|
|
228
|
+
@click.option("--agent", default="human", help="Agent making the update")
|
|
229
|
+
@click.option(
|
|
230
|
+
"--status", "new_status",
|
|
231
|
+
type=click.Choice(["analyzing", "known_error", "resolved"]),
|
|
232
|
+
help="New status",
|
|
233
|
+
)
|
|
234
|
+
@click.option("--root-cause", default=None, help="Root cause description")
|
|
235
|
+
@click.option("--workaround", default=None, help="Workaround")
|
|
236
|
+
@click.option("--note", default="", help="Timeline note")
|
|
237
|
+
@click.option("--create-kedb", is_flag=True, help="Create KEDB entry")
|
|
238
|
+
def problem_update(problem_id, agent, new_status, root_cause, workaround, note, create_kedb):
|
|
239
|
+
"""Update a problem record."""
|
|
240
|
+
from ..itil import ITILManager
|
|
241
|
+
|
|
242
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
243
|
+
try:
|
|
244
|
+
prb = mgr.update_problem(
|
|
245
|
+
problem_id=problem_id,
|
|
246
|
+
agent=agent,
|
|
247
|
+
new_status=new_status,
|
|
248
|
+
root_cause=root_cause,
|
|
249
|
+
workaround=workaround,
|
|
250
|
+
note=note,
|
|
251
|
+
create_kedb=create_kedb,
|
|
252
|
+
)
|
|
253
|
+
console.print(
|
|
254
|
+
f"\n [green]Updated:[/green] {prb.id} -> {prb.status.value}\n"
|
|
255
|
+
)
|
|
256
|
+
if prb.kedb_id:
|
|
257
|
+
console.print(f" [dim]KEDB entry: {prb.kedb_id}[/dim]\n")
|
|
258
|
+
except ValueError as exc:
|
|
259
|
+
console.print(f"\n [red]Error:[/red] {exc}\n")
|
|
260
|
+
|
|
261
|
+
# ── itil change ───────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
@itil.group()
|
|
264
|
+
def change():
|
|
265
|
+
"""Change management (RFC)."""
|
|
266
|
+
|
|
267
|
+
@change.command("propose")
|
|
268
|
+
@click.option("--title", "-t", required=True, help="Change title")
|
|
269
|
+
@click.option(
|
|
270
|
+
"--type", "change_type", default="normal",
|
|
271
|
+
type=click.Choice(["standard", "normal", "emergency"]),
|
|
272
|
+
help="Change type",
|
|
273
|
+
)
|
|
274
|
+
@click.option(
|
|
275
|
+
"--risk", default="medium",
|
|
276
|
+
type=click.Choice(["low", "medium", "high"]),
|
|
277
|
+
help="Risk level",
|
|
278
|
+
)
|
|
279
|
+
@click.option("--rollback", default="", help="Rollback plan")
|
|
280
|
+
@click.option("--test-plan", default="", help="Test plan")
|
|
281
|
+
@click.option("--by", "managed_by", default="human", help="Managing agent")
|
|
282
|
+
@click.option("--implementer", default=None, help="Implementing agent")
|
|
283
|
+
@click.option("--problem", "related_problem_id", default=None, help="Related problem ID")
|
|
284
|
+
@click.option("--tag", multiple=True, help="Tags")
|
|
285
|
+
def change_propose(title, change_type, risk, rollback, test_plan,
|
|
286
|
+
managed_by, implementer, related_problem_id, tag):
|
|
287
|
+
"""Propose a new change (RFC)."""
|
|
288
|
+
from ..itil import ITILManager
|
|
289
|
+
|
|
290
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
291
|
+
chg = mgr.propose_change(
|
|
292
|
+
title=title,
|
|
293
|
+
change_type=change_type,
|
|
294
|
+
risk=risk,
|
|
295
|
+
rollback_plan=rollback,
|
|
296
|
+
test_plan=test_plan,
|
|
297
|
+
managed_by=managed_by,
|
|
298
|
+
created_by=managed_by,
|
|
299
|
+
implementer=implementer,
|
|
300
|
+
related_problem_id=related_problem_id,
|
|
301
|
+
tags=list(tag),
|
|
302
|
+
)
|
|
303
|
+
console.print(
|
|
304
|
+
f"\n [green]Proposed:[/green] {chg.id} — {chg.title} "
|
|
305
|
+
f"({chg.change_type.value}, {chg.status.value})\n"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
@change.command("list")
|
|
309
|
+
@click.option(
|
|
310
|
+
"--status",
|
|
311
|
+
type=click.Choice([
|
|
312
|
+
"proposed", "reviewing", "approved", "rejected",
|
|
313
|
+
"implementing", "deployed", "verified", "failed", "closed",
|
|
314
|
+
]),
|
|
315
|
+
help="Filter by status",
|
|
316
|
+
)
|
|
317
|
+
def change_list(status):
|
|
318
|
+
"""List changes."""
|
|
319
|
+
from ..itil import ITILManager
|
|
320
|
+
|
|
321
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
322
|
+
changes = mgr.list_changes(status=status)
|
|
323
|
+
|
|
324
|
+
if not changes:
|
|
325
|
+
console.print("\n [dim]No changes found[/dim]\n")
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
console.print(f"\n[bold]Changes ({len(changes)}):[/bold]")
|
|
329
|
+
for c in changes:
|
|
330
|
+
console.print(
|
|
331
|
+
f" [{c.id}] {c.title} ({c.status.value}, "
|
|
332
|
+
f"{c.change_type.value}) @{c.managed_by}"
|
|
333
|
+
)
|
|
334
|
+
console.print()
|
|
335
|
+
|
|
336
|
+
@change.command("update")
|
|
337
|
+
@click.argument("change_id")
|
|
338
|
+
@click.option("--agent", default="human", help="Agent making the update")
|
|
339
|
+
@click.option(
|
|
340
|
+
"--status", "new_status",
|
|
341
|
+
type=click.Choice([
|
|
342
|
+
"reviewing", "approved", "rejected", "implementing",
|
|
343
|
+
"deployed", "verified", "failed", "closed",
|
|
344
|
+
]),
|
|
345
|
+
help="New status",
|
|
346
|
+
)
|
|
347
|
+
@click.option("--note", default="", help="Timeline note")
|
|
348
|
+
def change_update(change_id, agent, new_status, note):
|
|
349
|
+
"""Update a change status."""
|
|
350
|
+
from ..itil import ITILManager
|
|
351
|
+
|
|
352
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
353
|
+
try:
|
|
354
|
+
chg = mgr.update_change(
|
|
355
|
+
change_id=change_id,
|
|
356
|
+
agent=agent,
|
|
357
|
+
new_status=new_status,
|
|
358
|
+
note=note,
|
|
359
|
+
)
|
|
360
|
+
console.print(
|
|
361
|
+
f"\n [green]Updated:[/green] {chg.id} -> {chg.status.value}\n"
|
|
362
|
+
)
|
|
363
|
+
except ValueError as exc:
|
|
364
|
+
console.print(f"\n [red]Error:[/red] {exc}\n")
|
|
365
|
+
|
|
366
|
+
# ── itil cab ──────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
@itil.group()
|
|
369
|
+
def cab():
|
|
370
|
+
"""Change Advisory Board voting."""
|
|
371
|
+
|
|
372
|
+
@cab.command("vote")
|
|
373
|
+
@click.argument("change_id")
|
|
374
|
+
@click.option("--agent", default="human", help="Voting agent")
|
|
375
|
+
@click.option(
|
|
376
|
+
"--decision", default="approved",
|
|
377
|
+
type=click.Choice(["approved", "rejected", "abstain"]),
|
|
378
|
+
help="Vote decision",
|
|
379
|
+
)
|
|
380
|
+
@click.option("--conditions", default="", help="Approval conditions")
|
|
381
|
+
def cab_vote(change_id, agent, decision, conditions):
|
|
382
|
+
"""Submit a CAB vote for a change."""
|
|
383
|
+
from ..itil import ITILManager
|
|
384
|
+
|
|
385
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
386
|
+
vote = mgr.submit_cab_vote(
|
|
387
|
+
change_id=change_id,
|
|
388
|
+
agent=agent,
|
|
389
|
+
decision=decision,
|
|
390
|
+
conditions=conditions,
|
|
391
|
+
)
|
|
392
|
+
console.print(
|
|
393
|
+
f"\n [green]Voted:[/green] {vote.agent} -> {vote.decision.value} "
|
|
394
|
+
f"on {vote.change_id}\n"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# ── itil kedb ─────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
@itil.group()
|
|
400
|
+
def kedb():
|
|
401
|
+
"""Known Error Database."""
|
|
402
|
+
|
|
403
|
+
@kedb.command("search")
|
|
404
|
+
@click.argument("query")
|
|
405
|
+
def kedb_search(query):
|
|
406
|
+
"""Search the Known Error Database."""
|
|
407
|
+
from ..itil import ITILManager
|
|
408
|
+
|
|
409
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
410
|
+
results = mgr.search_kedb(query)
|
|
411
|
+
|
|
412
|
+
if not results:
|
|
413
|
+
console.print(f"\n [dim]No KEDB entries matching '{query}'[/dim]\n")
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
console.print(f"\n[bold]KEDB Results ({len(results)}):[/bold]")
|
|
417
|
+
for e in results:
|
|
418
|
+
console.print(f" [{e.id}] {e.title}")
|
|
419
|
+
if e.workaround:
|
|
420
|
+
console.print(f" [dim]Workaround: {e.workaround[:100]}[/dim]")
|
|
421
|
+
if e.root_cause:
|
|
422
|
+
console.print(f" [dim]Root cause: {e.root_cause[:100]}[/dim]")
|
|
423
|
+
console.print()
|
|
424
|
+
|
|
425
|
+
# ── itil board ────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
@itil.command("board")
|
|
428
|
+
def itil_board():
|
|
429
|
+
"""Generate ITIL-BOARD.md overview."""
|
|
430
|
+
from ..itil import ITILManager
|
|
431
|
+
|
|
432
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
433
|
+
path = mgr.write_board_md()
|
|
434
|
+
console.print(f"\n [green]Generated:[/green] {path}\n")
|