rainskills 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +1 -1
  3. package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
  4. package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
  5. package/marketplace/rainskills/skills/rainskills/SKILL.md +2 -2
  6. package/package.json +2 -2
  7. package/rainbond-app-assistant/SKILL.md +40 -1452
  8. package/rainbond-app-assistant/references/operational-reference.md +27 -0
  9. package/rainbond-app-assistant/references/routing.md +15 -0
  10. package/rainbond-app-assistant/references/runtime-gate.md +167 -0
  11. package/rainbond-app-assistant/references/workflow-rules.md +344 -0
  12. package/rainbond-app-assistant/scripts/validate_cross_skill_routing.py +637 -0
  13. package/rainbond-app-assistant/scripts/validate_progressive_loading.py +149 -0
  14. package/rainbond-app-version-assistant/SKILL.md +1 -1
  15. package/rainbond-delivery-verifier/SKILL.md +1 -1
  16. package/rainbond-env-sync/SKILL.md +1 -1
  17. package/rainbond-fullstack-bootstrap/SKILL.md +1 -1
  18. package/rainbond-fullstack-troubleshooter/SKILL.md +1 -1
  19. package/rainbond-opensource-app-deploy/SKILL.md +30 -301
  20. package/rainbond-opensource-app-deploy/agents/openai.yaml +1 -1
  21. package/rainbond-opensource-app-deploy/references/deployment-workflow.md +168 -0
  22. package/rainbond-opensource-app-deploy/references/runtime-gate.md +147 -0
  23. package/rainbond-platform-installer/SKILL.md +1 -1
  24. package/rainbond-platform-installer/scripts/installed-version.js +1 -1
  25. package/rainbond-platform-query/SKILL.md +1 -1
  26. package/rainbond-project-init/SKILL.md +1 -1
  27. package/rainbond-template-installer/SKILL.md +1 -1
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Validate mutually exclusive routing for the two deployment entry skills."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import re
9
+ import sys
10
+ import unicodedata
11
+ from pathlib import Path
12
+
13
+ import yaml
14
+
15
+
16
+ DEFAULT_REPO_ROOT = Path(__file__).resolve().parents[2]
17
+ OPEN_STAGE_ROWS = (
18
+ "| Phase 0:描述符未确认 | 不加载 reference;只做上述静态资格判断 |",
19
+ "| 描述符已确认,首次需要连接或调用 Rainbond | 只读取自己的 "
20
+ "[runtime gate](references/runtime-gate.md) |",
21
+ "| operation/context 已建立,需要建模、部署、排障或交付 | 读取 "
22
+ "[deployment workflow](references/deployment-workflow.md) |",
23
+ "| 新鲜证据命中已知部署故障模式 | 再读取 "
24
+ "[failure-mode playbook](references/failure-mode-playbook.md) |",
25
+ )
26
+ NEGATIONS = ("不得", "禁止", "不加载", "never", "must not", "do not", "cannot")
27
+ ROUTE_MARKERS = (
28
+ "route",
29
+ "use",
30
+ "goes",
31
+ "go to",
32
+ "转到",
33
+ "改走",
34
+ "应转",
35
+ "留在",
36
+ "remain",
37
+ "stay",
38
+ )
39
+
40
+
41
+ def require(condition: bool, message: str, failures: list[str]) -> None:
42
+ if not condition:
43
+ failures.append(message)
44
+
45
+
46
+ def normalize(value: str) -> str:
47
+ value = unicodedata.normalize("NFKC", value).casefold()
48
+ value = re.sub(r"[-_/]", " ", value)
49
+ value = re.sub(r"[^\w\u3400-\u9fff]+", " ", value)
50
+ return " ".join(value.split())
51
+
52
+
53
+ def contains(value: str, phrase: str) -> bool:
54
+ return normalize(phrase) in normalize(value)
55
+
56
+
57
+ def statements(value: str) -> list[str]:
58
+ return [
59
+ normalize(part)
60
+ for part in re.split(r"[!?。!?;;\n]+|\.(?=\s|$)", value)
61
+ if normalize(part)
62
+ ]
63
+
64
+
65
+ def description_statements(value: str) -> list[str]:
66
+ return [
67
+ normalize(part)
68
+ for part in re.split(r"[!?。!?\n]+|\.(?=\s|$)", value)
69
+ if normalize(part)
70
+ ]
71
+
72
+
73
+ def parse_description(source: str) -> str:
74
+ match = re.match(r"\A---\s*\n(.*?)\n---\s*\n", source, re.DOTALL)
75
+ if not match:
76
+ return ""
77
+ metadata = yaml.safe_load(match.group(1))
78
+ if not isinstance(metadata, dict) or not isinstance(metadata.get("description"), str):
79
+ return ""
80
+ return metadata["description"]
81
+
82
+
83
+ def markdown_body(source: str) -> str:
84
+ return re.sub(r"\A---\s*\n.*?\n---\s*\n", "", source, count=1, flags=re.DOTALL)
85
+
86
+
87
+ def bounded_section(source: str, start_heading: str, end_heading: str) -> str:
88
+ start = source.find(start_heading)
89
+ end = source.find(end_heading, start + len(start_heading)) if start >= 0 else -1
90
+ if start < 0 or end < 0 or start >= end:
91
+ return ""
92
+ return source[start:end]
93
+
94
+
95
+ def has_route_marker(statement: str) -> bool:
96
+ return any(marker in statement for marker in ROUTE_MARKERS)
97
+
98
+
99
+ def has_negation(statement: str) -> bool:
100
+ return any(normalize(marker) in statement for marker in NEGATIONS)
101
+
102
+
103
+ def has_local_negation(value: str) -> bool:
104
+ value = normalize(value)
105
+ return bool(re.search(r"\b(?:not|never)\b", value)) or any(
106
+ marker in value for marker in ("不得", "禁止", "不能", "不可", "不应")
107
+ )
108
+
109
+
110
+ def category_is_locally_negated(statement: str, category: str) -> bool:
111
+ statement = normalize(statement)
112
+ category = normalize(category)
113
+ category_at = statement.find(category)
114
+ if category_at < 0:
115
+ return False
116
+ prefix = statement[:category_at]
117
+ boundaries = ("but", "while", "whereas", "although", "however", "instead", "而是", "但是", "但", "不过")
118
+ boundary_at = -1
119
+ for boundary in boundaries:
120
+ normalized_boundary = normalize(boundary)
121
+ if re.fullmatch(r"[a-z]+", normalized_boundary):
122
+ matches = list(re.finditer(rf"\b{re.escape(normalized_boundary)}\b", prefix))
123
+ candidate = matches[-1].start() if matches else -1
124
+ else:
125
+ candidate = prefix.rfind(normalized_boundary)
126
+ boundary_at = max(boundary_at, candidate)
127
+ clause_prefix = prefix[boundary_at:] if boundary_at >= 0 else prefix
128
+ return has_local_negation(clause_prefix)
129
+
130
+
131
+ def relation_has_positive_cue(relation: str, cues: tuple[str, ...]) -> bool:
132
+ relation = normalize(relation)
133
+ positions = [relation.rfind(normalize(cue)) for cue in cues]
134
+ cue_at = max(positions, default=-1)
135
+ if cue_at < 0:
136
+ return False
137
+ prefix_tokens = relation[:cue_at].split()[-4:]
138
+ local_window = " ".join(prefix_tokens + relation[cue_at:].split())
139
+ return not has_local_negation(local_window)
140
+
141
+
142
+ def target_relation_after_contrast(
143
+ statement: str,
144
+ target_phrases: tuple[str, ...],
145
+ ) -> bool:
146
+ statement = normalize(statement)
147
+ explicit_alternatives = ("instead", "rather", "而是", "改为")
148
+ controlled_but = ("but", "但是", "但")
149
+ cues = ("route", "use", "to", "with", "delegate", "handled", "goes", "stay")
150
+
151
+ def starts_with_inherited_cue(value: str) -> bool:
152
+ value = normalize(value)
153
+ cue = r"(?:routes?|uses?|go(?:es)?|delegates?|handled|转到|改走|应转|留在)"
154
+ pronoun = r"(?:it|they|this|that|these|those|request|requests|它|其|该请求|这些请求)"
155
+ return bool(re.match(rf"^(?:{cue}|{pronoun}\s*{cue})(?:\s|$)", value))
156
+
157
+ for marker in explicit_alternatives + controlled_but:
158
+ marker_at = statement.rfind(marker)
159
+ if marker_at < 0:
160
+ continue
161
+ suffix = statement[marker_at + len(marker):]
162
+ for target in target_phrases:
163
+ target = normalize(target)
164
+ target_at = suffix.find(target)
165
+ if target_at < 0:
166
+ continue
167
+ relation = suffix[:target_at]
168
+ if marker in controlled_but:
169
+ if starts_with_inherited_cue(relation):
170
+ return True
171
+ elif relation_has_positive_cue(relation, cues):
172
+ return True
173
+ return False
174
+
175
+
176
+ def directed_relation(
177
+ statement: str,
178
+ category_phrases: tuple[str, ...],
179
+ target_phrases: tuple[str, ...],
180
+ *,
181
+ allow_negated_category: bool = False,
182
+ ) -> bool:
183
+ statement = normalize(statement)
184
+ categories = [normalize(phrase) for phrase in category_phrases]
185
+ targets = [normalize(phrase) for phrase in target_phrases]
186
+ forward_cues = (
187
+ "route",
188
+ "use",
189
+ " to ",
190
+ " with ",
191
+ "delegate",
192
+ "handled",
193
+ "goes",
194
+ "stay",
195
+ "转到",
196
+ "改走",
197
+ "应转",
198
+ "留在",
199
+ )
200
+ reverse_cues = ("handles", "owns", "accepts", "receives", "responsible for")
201
+ contrast = ("while", "but", "whereas", "although", "however", "instead", "unrelated")
202
+
203
+ for category in categories:
204
+ category_at = statement.find(category)
205
+ if category_at < 0 or (
206
+ not allow_negated_category
207
+ and category_is_locally_negated(statement, category)
208
+ ):
209
+ continue
210
+ for target in targets:
211
+ target_at = statement.find(target)
212
+ if target_at < 0:
213
+ continue
214
+ if category_at < target_at:
215
+ relation = statement[category_at + len(category):target_at]
216
+ cues = forward_cues
217
+ else:
218
+ relation = statement[target_at + len(target):category_at]
219
+ cues = reverse_cues
220
+ if len(relation) > 360 or any(word in relation for word in contrast):
221
+ continue
222
+ if relation_has_positive_cue(relation, cues):
223
+ return True
224
+ return False
225
+
226
+
227
+ def validate_description_boundaries(
228
+ app_description: str,
229
+ open_description: str,
230
+ failures: list[str],
231
+ ) -> None:
232
+ app_statements = description_statements(app_description)
233
+ open_statements = description_statements(open_description)
234
+ app_normalized = normalize(app_description)
235
+
236
+ app_targets = ("rainbond app assistant", "app assistant")
237
+ open_targets = ("rainbond opensource app deploy", "open source")
238
+ template_targets = ("rainbond template installer", "template installer")
239
+
240
+ def app_category_owned(
241
+ statement: str,
242
+ category_phrases: tuple[str, ...],
243
+ ) -> bool:
244
+ implicit_owner = statement.startswith("use when") and any(
245
+ contains(statement, category)
246
+ and not category_is_locally_negated(statement, category)
247
+ for category in category_phrases
248
+ )
249
+ return implicit_owner or directed_relation(statement, category_phrases, app_targets)
250
+
251
+ app_owner = all(
252
+ any(predicate(statement) and app_category_owned(statement, categories) for statement in app_statements)
253
+ for predicate, categories in (
254
+ (lambda statement: "source code" in statement, ("source code",)),
255
+ (lambda statement: "current project" in statement, ("current project",)),
256
+ (
257
+ lambda statement: "source directory" in statement and "package" in statement,
258
+ ("source directory", "source package"),
259
+ ),
260
+ (lambda statement: "bare git" in statement, ("bare git",)),
261
+ (
262
+ lambda statement: "named application" in statement and "without a descriptor" in statement,
263
+ ("named application", "named app"),
264
+ ),
265
+ )
266
+ )
267
+ app_actions = all(
268
+ action in app_normalized
269
+ for action in ("deploy", "run", "deliver", "inspect", "repair", "troubleshoot")
270
+ )
271
+ require(
272
+ app_owner and app_actions,
273
+ "App description must positively own source and descriptor-less requests",
274
+ failures,
275
+ )
276
+ require(
277
+ any(
278
+ "supplied" in statement
279
+ and all(kind in statement for kind in ("compose", "helm", "image set", "descriptor"))
280
+ and directed_relation(
281
+ statement,
282
+ ("descriptor", "compose", "helm", "image set"),
283
+ open_targets,
284
+ allow_negated_category=True,
285
+ )
286
+ for statement in app_statements
287
+ ),
288
+ "App description must exclude supplied descriptors to Open-source Deploy",
289
+ failures,
290
+ )
291
+ require(
292
+ any(
293
+ "market template" in statement
294
+ and directed_relation(
295
+ statement,
296
+ ("market template",),
297
+ template_targets,
298
+ allow_negated_category=True,
299
+ )
300
+ for statement in app_statements
301
+ ),
302
+ "App description must exclude confirmed market templates",
303
+ failures,
304
+ )
305
+
306
+ open_owner = any(
307
+ (
308
+ statement.startswith("use only when")
309
+ or statement.startswith("use this skill only when")
310
+ or directed_relation(
311
+ statement,
312
+ ("descriptor", "compose", "helm", "image set"),
313
+ open_targets,
314
+ )
315
+ )
316
+ and ("actual" in statement or "supplied" in statement)
317
+ and all(kind in statement for kind in ("compose", "helm", "image set", "descriptor"))
318
+ for statement in open_statements
319
+ )
320
+ require(
321
+ open_owner,
322
+ "Open-source description must positively own only supplied descriptors",
323
+ failures,
324
+ )
325
+ open_exclusion = any(
326
+ "bare git" in statement
327
+ and "source project" in statement
328
+ and "directory" in statement
329
+ and "package" in statement
330
+ and "named app" in statement
331
+ and ("without a descriptor" in statement or "without descriptor" in statement)
332
+ and "private image project" in statement
333
+ and all(
334
+ directed_relation(
335
+ statement,
336
+ categories,
337
+ app_targets,
338
+ allow_negated_category=True,
339
+ )
340
+ for categories in (
341
+ ("bare git",),
342
+ ("source project", "source directory", "source package"),
343
+ ("named application", "named app"),
344
+ )
345
+ )
346
+ for statement in open_statements
347
+ ) and any(
348
+ "market template" in statement
349
+ and directed_relation(
350
+ statement,
351
+ ("market template",),
352
+ template_targets,
353
+ allow_negated_category=True,
354
+ )
355
+ for statement in open_statements
356
+ )
357
+ require(
358
+ open_exclusion,
359
+ "Open-source description must exclude source, named-only, and market routes",
360
+ failures,
361
+ )
362
+
363
+
364
+ def validate_routing_conflicts(
365
+ app_root: str,
366
+ app_description: str,
367
+ open_root: str,
368
+ open_description: str,
369
+ failures: list[str],
370
+ ) -> None:
371
+ phase_zero = bounded_section(open_root, "## Phase 0:静态资格判断", "## 渐进加载")
372
+ staged_loading = bounded_section(open_root, "## 渐进加载", "## Runtime 与安全边界")
373
+ require(bool(phase_zero), "Open-source Phase 0 section bounds are invalid", failures)
374
+ require(bool(staged_loading), "Open-source staged-loading section bounds are invalid", failures)
375
+
376
+ app_body_statements = statements(markdown_body(app_root))
377
+ open_body_statements = statements(markdown_body(open_root))
378
+ body_statements = app_body_statements + open_body_statements
379
+ all_root_statements = (
380
+ statements(app_description)
381
+ + statements(open_description)
382
+ + body_statements
383
+ )
384
+
385
+ source_categories = (
386
+ "bare git",
387
+ "source project",
388
+ "source code",
389
+ "source directory",
390
+ "source package",
391
+ "current project",
392
+ "named only",
393
+ "named application",
394
+ )
395
+
396
+ def open_source_target(statement: str) -> bool:
397
+ return contains(statement, "rainbond opensource app deploy") or "open source" in statement
398
+
399
+ source_to_open = any(
400
+ any(category in statement for category in source_categories)
401
+ and open_source_target(statement)
402
+ and (
403
+ directed_relation(statement, source_categories, ("rainbond opensource app deploy", "open source"))
404
+ or target_relation_after_contrast(
405
+ statement,
406
+ ("rainbond opensource app deploy", "open source"),
407
+ )
408
+ )
409
+ for statement in all_root_statements
410
+ )
411
+ require(not source_to_open, "source ownership boundary conflict", failures)
412
+
413
+ descriptor_to_app = any(
414
+ any(kind in statement for kind in ("compose", "helm", "image set"))
415
+ and contains(statement, "rainbond app assistant")
416
+ and (
417
+ directed_relation(
418
+ statement,
419
+ ("compose", "helm", "image set", "descriptor"),
420
+ ("rainbond app assistant", "app assistant"),
421
+ )
422
+ or target_relation_after_contrast(
423
+ statement,
424
+ ("rainbond app assistant", "app assistant"),
425
+ )
426
+ )
427
+ for statement in all_root_statements
428
+ )
429
+ require(not descriptor_to_app, "descriptor ownership boundary conflict", failures)
430
+
431
+ market_conflict = False
432
+ for statement in all_root_statements:
433
+ if not contains(statement, "market template"):
434
+ continue
435
+ rejects_installer = re.search(
436
+ r"(?:not route|do not route|不转)\s+rainbond template installer",
437
+ statement,
438
+ )
439
+ stays_open = contains(statement, "rainbond opensource app deploy") and any(
440
+ marker in statement for marker in ("留在", "remain", "stay")
441
+ )
442
+ routes_to_app = bool(
443
+ re.search(
444
+ r"market templates?.{0,40}(?:route|goes|go to|转到|改走|应转).{0,40}rainbond app assistant",
445
+ statement,
446
+ )
447
+ or re.search(
448
+ r"(?:route|转到|改走|应转).{0,20}market templates?.{0,40}rainbond app assistant",
449
+ statement,
450
+ )
451
+ ) and not has_negation(statement)
452
+ if rejects_installer or stays_open or routes_to_app:
453
+ market_conflict = True
454
+ break
455
+ require(not market_conflict, "market template routing boundary conflict", failures)
456
+
457
+ def unconfirmed(statement: str) -> bool:
458
+ return any(
459
+ marker in statement
460
+ for marker in (
461
+ "未确认描述符",
462
+ "描述符未确认",
463
+ "描述符确认前",
464
+ "未确认 descriptor",
465
+ "descriptor 未确认",
466
+ "before descriptor confirmation",
467
+ "before confirming descriptor",
468
+ )
469
+ )
470
+
471
+ def pre_descriptor_action(statement: str) -> bool:
472
+ non_gate_actions = (
473
+ "查询",
474
+ "连接环境",
475
+ "连接 rainbond",
476
+ "连接 runtime",
477
+ "connect environment",
478
+ "connect runtime",
479
+ "clone",
480
+ "browse git",
481
+ "克隆",
482
+ "浏览 git",
483
+ )
484
+ positive_markers = ("允许", "先", "可以", "may", "can", "should", "must load", "must read")
485
+ has_gate_action = "runtime gate" in statement and any(
486
+ action in statement for action in ("读取", "加载", "read", "load", "允许")
487
+ )
488
+ has_action = has_gate_action or any(action in statement for action in non_gate_actions)
489
+ explicitly_positive = any(marker in statement for marker in positive_markers)
490
+ return (
491
+ unconfirmed(statement)
492
+ and has_action
493
+ and (explicitly_positive or not has_negation(statement))
494
+ )
495
+
496
+ require(
497
+ not any(pre_descriptor_action(statement) for statement in open_body_statements),
498
+ "pre-descriptor action boundary conflict",
499
+ failures,
500
+ )
501
+
502
+ stage_conflict = any(pre_descriptor_action(statement) for statement in statements(staged_loading)) or any(
503
+ contains(statement, "deployment workflow")
504
+ and contains(statement, "operation context")
505
+ and ("前" in statement or "before" in statement)
506
+ and not has_negation(statement)
507
+ for statement in statements(staged_loading)
508
+ )
509
+ require(not stage_conflict, "Open-source stage ordering conflict", failures)
510
+
511
+ guard_scope = normalize(phase_zero)
512
+ guard_complete = (
513
+ unconfirmed(guard_scope)
514
+ and contains(guard_scope, "runtime gate")
515
+ and "rainbond" in guard_scope
516
+ and ("克隆" in guard_scope or "clone" in guard_scope)
517
+ and ("浏览 git" in guard_scope or "browse git" in guard_scope)
518
+ and has_negation(guard_scope)
519
+ and not any(pre_descriptor_action(statement) for statement in statements(phase_zero))
520
+ )
521
+ require(guard_complete, "Open-source descriptor guard is missing or reversed", failures)
522
+
523
+
524
+ def validate_cross_skill_routing(repo_root: Path) -> list[str]:
525
+ failures: list[str] = []
526
+ app_dir = repo_root / "rainbond-app-assistant"
527
+ open_dir = repo_root / "rainbond-opensource-app-deploy"
528
+ app_root = (app_dir / "SKILL.md").read_text(encoding="utf-8")
529
+ open_root = (open_dir / "SKILL.md").read_text(encoding="utf-8")
530
+ app_description = parse_description(app_root)
531
+ open_description = parse_description(open_root)
532
+
533
+ validate_description_boundaries(app_description, open_description, failures)
534
+
535
+ open_bytes = len(open_root.encode("utf-8"))
536
+ open_lines = len(open_root.splitlines())
537
+ require(open_bytes <= 6_500, f"Open-source root is too large: {open_bytes} bytes", failures)
538
+ require(open_lines <= 140, f"Open-source root is too long: {open_lines} lines", failures)
539
+
540
+ for label, root in (("App", app_root), ("Open-source", open_root)):
541
+ for forbidden in (
542
+ "rainskills.skill-runtime-contract.v1",
543
+ "rainskills.single-runtime-contract.v1",
544
+ "<!-- rainskills-runtime-gate:start -->",
545
+ '"runtime_status":',
546
+ '"input_commands":',
547
+ ):
548
+ require(forbidden not in root, f"{label} root embeds runtime content: {forbidden}", failures)
549
+
550
+ order_statement = next(
551
+ (
552
+ statement
553
+ for statement in statements(open_root)
554
+ if contains(statement, "先验证描述符")
555
+ and contains(statement, "再加载 runtime gate")
556
+ ),
557
+ None,
558
+ )
559
+ require(order_statement is not None, "Open-source root must state descriptor-before-gate ordering", failures)
560
+ validate_routing_conflicts(
561
+ app_root,
562
+ app_description,
563
+ open_root,
564
+ open_description,
565
+ failures,
566
+ )
567
+
568
+ for forbidden in (
569
+ "## 0. Derive the official topology",
570
+ "## 6. Pass the delivery gate",
571
+ "## Progress checklist",
572
+ ):
573
+ require(forbidden not in open_root, f"Open-source root contains forbidden staged content: {forbidden}", failures)
574
+
575
+ for row in OPEN_STAGE_ROWS:
576
+ require(row in open_root, f"Open-source stage mapping is invalid: {row}", failures)
577
+ if all(row in open_root for row in OPEN_STAGE_ROWS):
578
+ positions = [open_root.index(row) for row in OPEN_STAGE_ROWS]
579
+ require(
580
+ positions == sorted(positions),
581
+ "Open-source stages must progress from static qualification to gate, workflow, then playbook",
582
+ failures,
583
+ )
584
+
585
+ for label, skill_dir in (("App", app_dir), ("Open-source", open_dir)):
586
+ gate_path = skill_dir / "references" / "runtime-gate.md"
587
+ require(gate_path.is_file(), f"{label} runtime gate is missing", failures)
588
+ if gate_path.is_file():
589
+ gate = gate_path.read_text(encoding="utf-8")
590
+ require(
591
+ "rainskills.skill-runtime-contract.v1" in gate,
592
+ f"{label} runtime gate lacks the progressive-loading contract marker",
593
+ failures,
594
+ )
595
+ require(
596
+ "rainskills.single-runtime-contract.v1" in gate,
597
+ f"{label} runtime gate lacks its contract",
598
+ failures,
599
+ )
600
+
601
+ deployment_workflow = open_dir / "references" / "deployment-workflow.md"
602
+ failure_playbook = open_dir / "references" / "failure-mode-playbook.md"
603
+ require(deployment_workflow.is_file(), "missing deployment-workflow.md", failures)
604
+ require(failure_playbook.is_file(), "missing failure-mode-playbook.md", failures)
605
+ if deployment_workflow.is_file():
606
+ workflow = deployment_workflow.read_text(encoding="utf-8")
607
+ require("## 0. Derive the official topology" in workflow, "deployment workflow lacks phase 0", failures)
608
+ require("## 6. Pass the delivery gate" in workflow, "deployment workflow lacks delivery gate", failures)
609
+
610
+ openai_yaml = (open_dir / "agents" / "openai.yaml").read_text(encoding="utf-8")
611
+ require(
612
+ 'short_description: "Only supplied Compose, Helm, or image-set descriptors"' in openai_yaml,
613
+ "Open-source short_description is not the required value",
614
+ failures,
615
+ )
616
+
617
+ return failures
618
+
619
+
620
+ def main() -> int:
621
+ parser = argparse.ArgumentParser(description=__doc__)
622
+ parser.add_argument("--repo-root", type=Path, default=DEFAULT_REPO_ROOT)
623
+ args = parser.parse_args()
624
+ failures = validate_cross_skill_routing(args.repo_root.resolve())
625
+
626
+ if failures:
627
+ print("FAIL: cross-skill routing is not mutually exclusive")
628
+ for failure in failures:
629
+ print(f" - {failure}")
630
+ return 1
631
+
632
+ print("PASS: cross-skill routing is mutually exclusive")
633
+ return 0
634
+
635
+
636
+ if __name__ == "__main__":
637
+ sys.exit(main())