ruby_reactor 0.5.4 → 0.6.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/speckit-agent-context-update/SKILL.md +32 -0
  3. data/.claude/skills/speckit-analyze/SKILL.md +262 -0
  4. data/.claude/skills/speckit-checklist/SKILL.md +374 -0
  5. data/.claude/skills/speckit-clarify/SKILL.md +286 -0
  6. data/.claude/skills/speckit-constitution/SKILL.md +157 -0
  7. data/.claude/skills/speckit-converge/SKILL.md +277 -0
  8. data/.claude/skills/speckit-implement/SKILL.md +224 -0
  9. data/.claude/skills/speckit-plan/SKILL.md +171 -0
  10. data/.claude/skills/speckit-specify/SKILL.md +346 -0
  11. data/.claude/skills/speckit-tasks/SKILL.md +215 -0
  12. data/.claude/skills/speckit-taskstoissues/SKILL.md +110 -0
  13. data/.release-please-manifest.json +1 -1
  14. data/.specify/extensions/.registry +19 -0
  15. data/.specify/extensions/agent-context/README.md +66 -0
  16. data/.specify/extensions/agent-context/agent-context-config.yml +5 -0
  17. data/.specify/extensions/agent-context/commands/speckit.agent-context.update.md +27 -0
  18. data/.specify/extensions/agent-context/extension.yml +34 -0
  19. data/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh +282 -0
  20. data/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +353 -0
  21. data/.specify/extensions.yml +23 -0
  22. data/.specify/feature.json +3 -0
  23. data/.specify/init-options.json +9 -0
  24. data/.specify/integration.json +15 -0
  25. data/.specify/integrations/claude.manifest.json +17 -0
  26. data/.specify/integrations/speckit.manifest.json +17 -0
  27. data/.specify/memory/constitution.md +134 -0
  28. data/.specify/scripts/bash/check-prerequisites.sh +189 -0
  29. data/.specify/scripts/bash/common.sh +619 -0
  30. data/.specify/scripts/bash/create-new-feature.sh +299 -0
  31. data/.specify/scripts/bash/setup-plan.sh +84 -0
  32. data/.specify/scripts/bash/setup-tasks.sh +91 -0
  33. data/.specify/templates/checklist-template.md +40 -0
  34. data/.specify/templates/constitution-template.md +50 -0
  35. data/.specify/templates/plan-template.md +113 -0
  36. data/.specify/templates/spec-template.md +131 -0
  37. data/.specify/templates/tasks-template.md +252 -0
  38. data/.specify/workflows/speckit/workflow.yml +77 -0
  39. data/.specify/workflows/workflow-registry.json +13 -0
  40. data/CHANGELOG.md +7 -0
  41. data/README.md +51 -24
  42. data/lib/ruby_reactor/adapters/active_job/compat.rb +24 -0
  43. data/lib/ruby_reactor/adapters/active_job/map_collector_worker.rb +19 -0
  44. data/lib/ruby_reactor/adapters/active_job/map_element_worker.rb +19 -0
  45. data/lib/ruby_reactor/adapters/active_job/router.rb +91 -0
  46. data/lib/ruby_reactor/adapters/active_job/sweeper_worker.rb +16 -0
  47. data/lib/ruby_reactor/adapters/active_job/worker.rb +24 -0
  48. data/lib/ruby_reactor/adapters/sidekiq/map_collector_worker.rb +15 -0
  49. data/lib/ruby_reactor/adapters/sidekiq/map_element_worker.rb +15 -0
  50. data/lib/ruby_reactor/adapters/sidekiq/router.rb +91 -0
  51. data/lib/ruby_reactor/adapters/sidekiq/sweeper_worker.rb +19 -0
  52. data/lib/ruby_reactor/adapters/sidekiq/worker.rb +25 -0
  53. data/lib/ruby_reactor/configuration.rb +24 -4
  54. data/lib/ruby_reactor/map/element_executor.rb +1 -1
  55. data/lib/ruby_reactor/rspec/active_job_helpers.rb +52 -0
  56. data/lib/ruby_reactor/rspec/async_test_helpers.rb +41 -0
  57. data/lib/ruby_reactor/rspec/sidekiq_helpers.rb +3 -3
  58. data/lib/ruby_reactor/rspec/test_subject.rb +11 -7
  59. data/lib/ruby_reactor/rspec.rb +4 -0
  60. data/lib/ruby_reactor/sweeper_job.rb +70 -0
  61. data/lib/ruby_reactor/version.rb +1 -1
  62. data/lib/ruby_reactor/worker.rb +226 -0
  63. data/lib/ruby_reactor.rb +40 -1
  64. data/specs/active_job.md +259 -0
  65. metadata +54 -6
  66. data/lib/ruby_reactor/sidekiq_adapter.rb +0 -87
  67. data/lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb +0 -13
  68. data/lib/ruby_reactor/sidekiq_workers/map_element_worker.rb +0 -13
  69. data/lib/ruby_reactor/sidekiq_workers/sweeper_worker.rb +0 -73
  70. data/lib/ruby_reactor/sidekiq_workers/worker.rb +0 -222
@@ -0,0 +1,17 @@
1
+ {
2
+ "integration": "claude",
3
+ "version": "0.11.6",
4
+ "installed_at": "2026-06-23T23:29:12.149990+00:00",
5
+ "files": {
6
+ ".claude/skills/speckit-analyze/SKILL.md": "0c6d0525b97b523851a3ea8fc0efdcf477dfa65bef8c4d976b6da2f220f32c86",
7
+ ".claude/skills/speckit-clarify/SKILL.md": "eb8907c57e9af2c54952370a45e3fb66cb21a64dc7b3d361615bab914c13becf",
8
+ ".claude/skills/speckit-constitution/SKILL.md": "c1a044aba243ca6aff627fb5e4404feb6f1108d4f7dd174631bee3ae477d6c15",
9
+ ".claude/skills/speckit-implement/SKILL.md": "042a0415ce60a5b66adf039da431d9a98cb4897ff330635e0bab0becbbaf3cd2",
10
+ ".claude/skills/speckit-converge/SKILL.md": "a6acccbb6630f04fa716967c68f3be9da96762805ddd2bf284fad36324c5c876",
11
+ ".claude/skills/speckit-plan/SKILL.md": "fd0e312109109d09d6b691d232925b54df11681e79e1dc333ad49b210d5efb00",
12
+ ".claude/skills/speckit-checklist/SKILL.md": "b05086a11d4b98dcb85822e8a468ef30b8353f132b6dac1359080b93833c4666",
13
+ ".claude/skills/speckit-specify/SKILL.md": "ebe19db388407058a6b078b3a66ea7198cb89795dc75680ba3c457f11bc8bf7a",
14
+ ".claude/skills/speckit-tasks/SKILL.md": "023b1c75a62dc6fd8bff67855ebb82ec54a8104e727571aa72200c8afcc3f99b",
15
+ ".claude/skills/speckit-taskstoissues/SKILL.md": "aa1151798a551fb52c439ca75c02d4f2e7b40c6c7d11fc025265cf1230c67cb1"
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "integration": "speckit",
3
+ "version": "0.11.6",
4
+ "installed_at": "2026-06-23T23:29:12.163635+00:00",
5
+ "files": {
6
+ ".specify/scripts/bash/common.sh": "af8a16f87b4f9084759c42ff9abf35c0b2a2025dffe58c298758ff86de2923b2",
7
+ ".specify/scripts/bash/setup-plan.sh": "4eb12c5b00f5c66a7d01b56c90898d320dcef4425d9b96652d57156c84948eda",
8
+ ".specify/scripts/bash/setup-tasks.sh": "cf21ba2212b4dd5b435c5ea8527500cfd27768b86c0bbc7ebc3207759f118d27",
9
+ ".specify/scripts/bash/check-prerequisites.sh": "afce0aa8db177320d83aa0b8e3619c06b865fd810781894e4a7a3f81664941ce",
10
+ ".specify/scripts/bash/create-new-feature.sh": "9ba116b64f0328eb69bc1a195d209074ea38823a73a554160d69df34a74daa65",
11
+ ".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3",
12
+ ".specify/templates/checklist-template.md": "c37695297e5d3153d64f82c21223509940b13932046c7961c42d1d669516130c",
13
+ ".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842",
14
+ ".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e",
15
+ ".specify/templates/plan-template.md": "cc7f7979cf8d8836ec26492785affd80791d3422a2b745062ec695be8c985ef7"
16
+ }
17
+ }
@@ -0,0 +1,134 @@
1
+ <!--
2
+ SYNC IMPACT REPORT
3
+ ==================
4
+ Version change: [unset] → 1.0.0 (MINOR: initial constitution, all principles defined from scratch)
5
+
6
+ Modified principles: N/A (first ratification)
7
+
8
+ Added sections:
9
+ - Core Principles (5 principles)
10
+ - Technical Constraints
11
+ - Development Workflow
12
+ - Governance
13
+
14
+ Templates checked:
15
+ - .specify/templates/plan-template.md ✅ Constitution Check gate present, no update needed
16
+ - .specify/templates/spec-template.md ✅ Aligned with Saga/library constraints
17
+ - .specify/templates/tasks-template.md ✅ Task phases align with Red-Green workflow
18
+
19
+ Deferred TODOs: none
20
+ -->
21
+
22
+ # RubyReactor Constitution
23
+
24
+ ## Core Principles
25
+
26
+ ### I. Gem-First Design
27
+
28
+ RubyReactor is a Ruby gem published to RubyGems. Every feature MUST be designed
29
+ as part of the gem's public API — self-contained, independently loadable, and
30
+ free of host-application coupling. New capabilities MUST ship via `lib/` and be
31
+ reachable through `require "ruby_reactor"` without requiring application-level
32
+ monkey-patching. Optional integrations (Sidekiq, OpenTelemetry) MUST be isolated
33
+ behind adapter modules so the gem remains usable in sync-only or minimal
34
+ environments.
35
+
36
+ **Rationale**: A gem that cannot be required cleanly, or that bleeds host
37
+ concerns into its core, is not a gem — it is a Rails engine in disguise.
38
+ Keeping gem boundaries honest protects downstream consumers.
39
+
40
+ ### II. Saga Pattern Integrity (NON-NEGOTIABLE)
41
+
42
+ Every workflow defined with RubyReactor MUST support compensation. Steps MUST
43
+ declare rollback logic where side effects are produced. DAG-based dependency
44
+ resolution governs execution order — no step may run before its declared
45
+ dependencies complete. Interrupts (pause/resume) MUST be first-class, not
46
+ bolted on. Partial execution without a recovery path (durability or compensation)
47
+ is forbidden.
48
+
49
+ **Rationale**: The Saga pattern's value is atomicity-like guarantees without
50
+ distributed transactions. Violating it — e.g., skipping compensation or allowing
51
+ orphaned steps — destroys the core promise and corrupts application state.
52
+
53
+ ### III. Test-First with Real Infrastructure
54
+
55
+ RSpec is the mandatory test framework. Tests MUST be written and confirmed
56
+ failing before implementation begins (Red-Green-Refactor). Redis MUST be
57
+ reachable for the test suite — mocking Redis or Sidekiq state is forbidden
58
+ for integration and contract tests. The in-memory Sidekiq testing mode
59
+ (`Sidekiq::Testing.inline!`) is acceptable only for unit-level step logic,
60
+ never for async orchestration paths.
61
+
62
+ **Rationale**: We were burned by mock/real divergence in async and locking
63
+ paths. Real Redis surfaces timing, serialization, and TTL bugs that mocks
64
+ hide. The spec_helper enforces Redis availability at suite start for this
65
+ reason.
66
+
67
+ ### IV. Observability by Default
68
+
69
+ Every failure MUST carry: reactor name, step name, redacted inputs, and
70
+ failure reason. OpenTelemetry instrumentation MUST be available via middleware
71
+ without requiring it as a hard dependency. The web dashboard MUST remain
72
+ current with the reactor state model. Structured logging is required for all
73
+ async execution paths — log lines MUST be machine-parseable (key=value or JSON).
74
+
75
+ **Rationale**: Async distributed workflows are black boxes without
76
+ observability. Operators need to reconstruct what happened and why from logs
77
+ and dashboards alone, especially after crash recovery.
78
+
79
+ ### V. Simplicity and Semantic Versioning
80
+
81
+ YAGNI governs design: add the abstraction when a second real use case exists,
82
+ not before. Complexity MUST be justified in the PR. SemVer is strictly
83
+ enforced: MAJOR for any breaking public API change, MINOR for backward-compatible
84
+ additions, PATCH for fixes and clarifications. Breaking changes MUST include a
85
+ migration note in CHANGELOG.md.
86
+
87
+ **Rationale**: RubyReactor is a library used in production applications. A
88
+ surprise breaking change in a MINOR bump costs downstream teams debugging time
89
+ they did not budget for. Simplicity keeps the library auditable and the
90
+ upgrade path predictable.
91
+
92
+ ## Technical Constraints
93
+
94
+ - **Ruby**: >= 3.0.0 required. No polyfills for older Rubies.
95
+ - **Sidekiq**: Core async dependency. Workers live in `lib/ruby_reactor/sidekiq_workers/`.
96
+ - **Redis**: Required for state persistence, locks, semaphores, rate limits, and
97
+ periods. The gem does NOT manage Redis connections — callers provide them.
98
+ - **dry-validation**: Input validation DSL. Schema definitions stay inside the
99
+ reactor/step DSL, not scattered across application code.
100
+ - **OpenTelemetry**: Optional instrumentation via the middleware stack
101
+ (`lib/ruby_reactor/open_telemetry.rb`). MUST NOT be a hard dependency.
102
+ - **RuboCop**: Style enforced via `rubocop-rspec` and `rubocop-rake`. All
103
+ commits MUST pass `bundle exec rubocop` without `--disable-pending-cops`.
104
+
105
+ ## Development Workflow
106
+
107
+ - Feature branches target `main`. PRs MUST pass CI (RSpec + RuboCop) before merge.
108
+ - Releases are managed by release-please. Version bump lives in
109
+ `lib/ruby_reactor/version.rb`. Do not manually edit the version in gemspec.
110
+ - New features MUST update `README.md` documentation and add entries to
111
+ `CHANGELOG.md` under the correct semantic heading (`Features`, `Bug Fixes`,
112
+ `documentation`).
113
+ - Class-based step definitions are the preferred authoring style (not inline
114
+ lambdas). Documentation and examples MUST reflect this.
115
+ - The `demo_app/` directory serves as a living integration example. Changes to
116
+ public API surface MUST be reflected there.
117
+
118
+ ## Governance
119
+
120
+ This constitution supersedes all other informal practices. Amendments require:
121
+
122
+ 1. A PR updating this file with rationale.
123
+ 2. Version bump per the SemVer policy in Principle V.
124
+ 3. Consistency propagation: update all `.specify/templates/` files that reference
125
+ amended principles before the PR merges.
126
+
127
+ All PR reviews MUST include a Constitution Check verifying the change does not
128
+ violate any principle. Complexity that appears to violate a principle MUST be
129
+ justified in the `Complexity Tracking` table of the plan.
130
+
131
+ Compliance review: at each MINOR or MAJOR gem release, confirm this constitution
132
+ still accurately reflects the codebase and update as needed.
133
+
134
+ **Version**: 1.0.0 | **Ratified**: 2025-10-02 | **Last Amended**: 2026-06-24
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Consolidated prerequisite checking script
4
+ #
5
+ # This script provides unified prerequisite checking for Spec-Driven Development workflow.
6
+ # It replaces the functionality previously spread across multiple scripts.
7
+ #
8
+ # Usage: ./check-prerequisites.sh [OPTIONS]
9
+ #
10
+ # OPTIONS:
11
+ # --json Output in JSON format
12
+ # --require-tasks Require tasks.md to exist (for implementation phase)
13
+ # --include-tasks Include tasks.md in AVAILABLE_DOCS list
14
+ # --paths-only Only output path variables (no validation)
15
+ # --help, -h Show help message
16
+ #
17
+ # OUTPUTS:
18
+ # JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
19
+ # Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
20
+ # Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
21
+
22
+ set -e
23
+
24
+ # Parse command line arguments
25
+ JSON_MODE=false
26
+ REQUIRE_TASKS=false
27
+ INCLUDE_TASKS=false
28
+ PATHS_ONLY=false
29
+
30
+ for arg in "$@"; do
31
+ case "$arg" in
32
+ --json)
33
+ JSON_MODE=true
34
+ ;;
35
+ --require-tasks)
36
+ REQUIRE_TASKS=true
37
+ ;;
38
+ --include-tasks)
39
+ INCLUDE_TASKS=true
40
+ ;;
41
+ --paths-only)
42
+ PATHS_ONLY=true
43
+ ;;
44
+ --help|-h)
45
+ cat << 'EOF'
46
+ Usage: check-prerequisites.sh [OPTIONS]
47
+
48
+ Consolidated prerequisite checking for Spec-Driven Development workflow.
49
+
50
+ OPTIONS:
51
+ --json Output in JSON format
52
+ --require-tasks Require tasks.md to exist (for implementation phase)
53
+ --include-tasks Include tasks.md in AVAILABLE_DOCS list
54
+ --paths-only Only output path variables (no prerequisite validation)
55
+ --help, -h Show this help message
56
+
57
+ EXAMPLES:
58
+ # Check task prerequisites (plan.md required)
59
+ ./check-prerequisites.sh --json
60
+
61
+ # Check implementation prerequisites (plan.md + tasks.md required)
62
+ ./check-prerequisites.sh --json --require-tasks --include-tasks
63
+
64
+ # Get feature paths only (no validation)
65
+ ./check-prerequisites.sh --paths-only
66
+
67
+ EOF
68
+ exit 0
69
+ ;;
70
+ *)
71
+ echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2
72
+ exit 1
73
+ ;;
74
+ esac
75
+ done
76
+
77
+ # Source common functions
78
+ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
79
+ source "$SCRIPT_DIR/common.sh"
80
+
81
+ # Get feature paths
82
+ _paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
83
+ eval "$_paths_output"
84
+ unset _paths_output
85
+
86
+ # If paths-only mode, output paths and exit (no validation)
87
+ if $PATHS_ONLY; then
88
+ if $JSON_MODE; then
89
+ # Minimal JSON paths payload (no validation performed)
90
+ if has_jq; then
91
+ jq -cn \
92
+ --arg repo_root "$REPO_ROOT" \
93
+ --arg branch "$CURRENT_BRANCH" \
94
+ --arg feature_dir "$FEATURE_DIR" \
95
+ --arg feature_spec "$FEATURE_SPEC" \
96
+ --arg impl_plan "$IMPL_PLAN" \
97
+ --arg tasks "$TASKS" \
98
+ '{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'
99
+ else
100
+ printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
101
+ "$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"
102
+ fi
103
+ else
104
+ echo "REPO_ROOT: $REPO_ROOT"
105
+ echo "BRANCH: $CURRENT_BRANCH"
106
+ echo "FEATURE_DIR: $FEATURE_DIR"
107
+ echo "FEATURE_SPEC: $FEATURE_SPEC"
108
+ echo "IMPL_PLAN: $IMPL_PLAN"
109
+ echo "TASKS: $TASKS"
110
+ fi
111
+ exit 0
112
+ fi
113
+
114
+ # Validate required directories and files
115
+ if [[ ! -d "$FEATURE_DIR" ]]; then
116
+ echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
117
+ echo "Run /speckit-specify first to create the feature structure." >&2
118
+ exit 1
119
+ fi
120
+
121
+ if [[ ! -f "$IMPL_PLAN" ]]; then
122
+ echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
123
+ echo "Run /speckit-plan first to create the implementation plan." >&2
124
+ exit 1
125
+ fi
126
+
127
+ # Check for tasks.md if required
128
+ if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
129
+ echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
130
+ echo "Run /speckit-tasks first to create the task list." >&2
131
+ exit 1
132
+ fi
133
+
134
+ # Build list of available documents
135
+ docs=()
136
+
137
+ # Always check these optional docs
138
+ [[ -f "$RESEARCH" ]] && docs+=("research.md")
139
+ [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
140
+
141
+ # Check contracts directory (only if it exists and has files)
142
+ if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
143
+ docs+=("contracts/")
144
+ fi
145
+
146
+ [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
147
+
148
+ # Include tasks.md if requested and it exists
149
+ if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
150
+ docs+=("tasks.md")
151
+ fi
152
+
153
+ # Output results
154
+ if $JSON_MODE; then
155
+ # Build JSON array of documents
156
+ if has_jq; then
157
+ if [[ ${#docs[@]} -eq 0 ]]; then
158
+ json_docs="[]"
159
+ else
160
+ json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
161
+ fi
162
+ jq -cn \
163
+ --arg feature_dir "$FEATURE_DIR" \
164
+ --argjson docs "$json_docs" \
165
+ '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'
166
+ else
167
+ if [[ ${#docs[@]} -eq 0 ]]; then
168
+ json_docs="[]"
169
+ else
170
+ json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
171
+ json_docs="[${json_docs%,}]"
172
+ fi
173
+ printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"
174
+ fi
175
+ else
176
+ # Text output
177
+ echo "FEATURE_DIR:$FEATURE_DIR"
178
+ echo "AVAILABLE_DOCS:"
179
+
180
+ # Show status of each potential document
181
+ check_file "$RESEARCH" "research.md"
182
+ check_file "$DATA_MODEL" "data-model.md"
183
+ check_dir "$CONTRACTS_DIR" "contracts/"
184
+ check_file "$QUICKSTART" "quickstart.md"
185
+
186
+ if $INCLUDE_TASKS; then
187
+ check_file "$TASKS" "tasks.md"
188
+ fi
189
+ fi