omniconductor 0.3.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 (118) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +219 -0
  3. package/NOTICE +11 -0
  4. package/README.md +526 -0
  5. package/THIRD_PARTY_NOTICES.md +31 -0
  6. package/TRADEMARKS.md +26 -0
  7. package/VISION.md +106 -0
  8. package/adapters/README.md +121 -0
  9. package/adapters/claude/README.md +102 -0
  10. package/adapters/claude/SUPPORTED-FEATURES.md +66 -0
  11. package/adapters/claude/hookify-templates/.recipe-scoped +9 -0
  12. package/adapters/claude/hookify-templates/README.md +93 -0
  13. package/adapters/claude/hookify-templates/block-completion-claim-without-push.local.md.template +56 -0
  14. package/adapters/claude/hookify-templates/block-direct-push-protected-branch.local.md.template +18 -0
  15. package/adapters/claude/hookify-templates/block-force-push-protected-branch.local.md.template +42 -0
  16. package/adapters/claude/hookify-templates/block-server-secret-in-client.local.md.template +36 -0
  17. package/adapters/claude/hookify-templates/warn-any-type-added.local.md.template +44 -0
  18. package/adapters/claude/hookify-templates/warn-commit-without-pre-commit-review.local.md.template +52 -0
  19. package/adapters/claude/hookify-templates/warn-console-direct.local.md.template +55 -0
  20. package/adapters/claude/hookify-templates/warn-create-table-without-access-control.local.md.template +35 -0
  21. package/adapters/claude/hookify-templates/warn-current-work-without-remaining-tasks.local.md.template +36 -0
  22. package/adapters/claude/hookify-templates/warn-gh-pr-create-without-pre-merge-review.local.md.template +38 -0
  23. package/adapters/claude/hookify-templates/warn-hardcoded-text-without-i18n-key.local.md.template +21 -0
  24. package/adapters/claude/hookify-templates/warn-on-gh-pr-merge.local.md.template +33 -0
  25. package/adapters/claude/hookify-templates/warn-plan-spec-without-remaining-tasks.local.md.template +44 -0
  26. package/adapters/claude/hookify-templates/warn-raw-hex-instead-of-token.local.md.template +21 -0
  27. package/adapters/claude/hookify-templates/warn-security-definer-without-search-path.local.md.template +30 -0
  28. package/adapters/claude/hookify-templates/warn-stop-commit-without-current-work.local.md.template +47 -0
  29. package/adapters/claude/hookify-templates/warn-user-manual-completion.local.md.template +40 -0
  30. package/adapters/claude/transform-spec.md +140 -0
  31. package/adapters/claude/transform.sh +1169 -0
  32. package/adapters/codex/README.md +87 -0
  33. package/adapters/codex/SUPPORTED-FEATURES.md +66 -0
  34. package/adapters/codex/transform-spec.md +90 -0
  35. package/adapters/codex/transform.sh +748 -0
  36. package/adapters/copilot/README.md +88 -0
  37. package/adapters/copilot/SUPPORTED-FEATURES.md +73 -0
  38. package/adapters/copilot/transform-spec.md +111 -0
  39. package/adapters/copilot/transform.sh +757 -0
  40. package/adapters/cursor/README.md +87 -0
  41. package/adapters/cursor/SUPPORTED-FEATURES.md +90 -0
  42. package/adapters/cursor/transform-spec.md +124 -0
  43. package/adapters/cursor/transform.sh +731 -0
  44. package/adapters/gemini/README.md +93 -0
  45. package/adapters/gemini/SUPPORTED-FEATURES.md +80 -0
  46. package/adapters/gemini/transform-spec.md +102 -0
  47. package/adapters/gemini/transform.sh +844 -0
  48. package/adapters/windsurf/README.md +93 -0
  49. package/adapters/windsurf/SUPPORTED-FEATURES.md +67 -0
  50. package/adapters/windsurf/transform-spec.md +99 -0
  51. package/adapters/windsurf/transform.sh +727 -0
  52. package/bin/omniconductor.js +130 -0
  53. package/core/README.md +64 -0
  54. package/core/anti-patterns/README.md +142 -0
  55. package/core/anti-patterns/dynamic-system-prompt.md +113 -0
  56. package/core/anti-patterns/frequent-rule-file-edit.md +83 -0
  57. package/core/anti-patterns/large-file-read-no-range.md +90 -0
  58. package/core/anti-patterns/no-sub-agent-dispatch.md +89 -0
  59. package/core/anti-patterns/single-monolithic-rule-file.md +99 -0
  60. package/core/anti-patterns/skill-eager-load.md +83 -0
  61. package/core/anti-patterns/tool-call-spam.md +93 -0
  62. package/core/docs-templates/CURRENT_WORK.md +64 -0
  63. package/core/docs-templates/INDEX.md +57 -0
  64. package/core/docs-templates/PLANS.md +44 -0
  65. package/core/docs-templates/README.md +40 -0
  66. package/core/docs-templates/REMAINING_TASKS.md +43 -0
  67. package/core/docs-templates/TASKS.md +51 -0
  68. package/core/docs-templates/specs/_example.md +121 -0
  69. package/core/hooks/README.md +62 -0
  70. package/core/hooks/pretool-agent-routing.sh.template +97 -0
  71. package/core/hooks/pretool-commit-current-work-check.sh.template +103 -0
  72. package/core/hooks/pretool-commit-test-coverage-check.sh.template +116 -0
  73. package/core/hooks/pretool-large-file-read-guard.sh.template +117 -0
  74. package/core/hooks/stop-cache-hit-baseline-check.sh.template +133 -0
  75. package/core/hooks/stop-r6-review-check.sh.template +80 -0
  76. package/core/hooks/stop-session-log-check.sh.template +101 -0
  77. package/core/hooks/stop-trajectory-log.sh.template +96 -0
  78. package/core/memory-pattern/EXAMPLES.md +114 -0
  79. package/core/memory-pattern/README.md +133 -0
  80. package/core/recipes/README.md +46 -0
  81. package/core/recipes/auto-mock-data.md +82 -0
  82. package/core/recipes/branch-strategy.md +100 -0
  83. package/core/recipes/coding-conventions.md +123 -0
  84. package/core/recipes/database-discipline.md +65 -0
  85. package/core/recipes/debugging.md +143 -0
  86. package/core/recipes/design-system.md +30 -0
  87. package/core/recipes/i18n.md +118 -0
  88. package/core/recipes/monorepo.md +101 -0
  89. package/core/recipes/self-improvement.md +61 -0
  90. package/core/recipes/tdd.md +128 -0
  91. package/core/recipes/web-mobile-parity.md +87 -0
  92. package/core/reflector/SCHEDULING.md +83 -0
  93. package/core/reflector/prune-lessons.sh +105 -0
  94. package/core/reflector/reflect-brief.md +11 -0
  95. package/core/reflector/reflect.command.md +13 -0
  96. package/core/reflector/run-weekly.sh +54 -0
  97. package/core/reflector/trajectory-log.sh +55 -0
  98. package/core/roles/README.md +61 -0
  99. package/core/roles/builder.md +75 -0
  100. package/core/roles/designer.md +93 -0
  101. package/core/roles/helper.md +70 -0
  102. package/core/roles/planner.md +123 -0
  103. package/core/roles/reflector.md +72 -0
  104. package/core/roles/reviewer.md +104 -0
  105. package/core/roles/scribe.md +89 -0
  106. package/core/universal-rules/README.md +71 -0
  107. package/core/universal-rules/meta-discipline.md +292 -0
  108. package/core/universal-rules/operations.md +162 -0
  109. package/core/universal-rules/quality-gates.md +191 -0
  110. package/core/universal-rules/spec-as-you-go.md +123 -0
  111. package/core/universal-rules/workflow.md +153 -0
  112. package/core/workflow/PHASES.md +134 -0
  113. package/core/workflow/README.md +56 -0
  114. package/docs/MANUAL-INSTALL.md +456 -0
  115. package/package.json +52 -0
  116. package/tools/check-framework-purity.sh +91 -0
  117. package/tools/measure-tokens.sh +169 -0
  118. package/tools/validate-adapter-output.sh +611 -0
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # CONDUCTOR — Adapter output format validator
4
+ #
5
+ # Validates that adapter-produced files conform to per-IDE expected formats.
6
+ # Used as a pre-flight check after running an adapter, or in CI/smoke tests.
7
+ #
8
+ # Usage:
9
+ # bash tools/validate-adapter-output.sh <target-dir> <adapter>
10
+ #
11
+ # Adapters: cursor | copilot | claude | gemini | codex | windsurf
12
+ #
13
+ # Exit codes:
14
+ # 0 all files PASS
15
+ # 1 one or more files FAIL
16
+ # 2 invocation error (bad args / missing dir)
17
+ #
18
+ # Implementation: pure bash + grep/sed/awk. No jq / yq.
19
+ #
20
+ # What gets checked (per adapter):
21
+ # cursor:
22
+ # - .cursor/rules/*.mdc exist
23
+ # - frontmatter delimited by ^---$ (open + close)
24
+ # - description: <string> field present
25
+ # - globs: array OR string present
26
+ # - alwaysApply: true|false (optional but if present must be bool)
27
+ # - body has at least one markdown heading (^#)
28
+ # - code fences ``` balanced (even count)
29
+ #
30
+ # copilot:
31
+ # - .github/copilot-instructions.md exists (top-level bundle); markdown body sane
32
+ # - if .github/instructions/*.instructions.md exist:
33
+ # - frontmatter present
34
+ # - applyTo: <CSV string in quotes> (NOT a YAML array)
35
+ # - body sane
36
+ #
37
+ # claude:
38
+ # - .claude/rules/*.md exist
39
+ # - frontmatter has paths: array
40
+ # - body sane
41
+ #
42
+ # gemini:
43
+ # - GEMINI.md exists, non-empty
44
+ # - all 5 universal-rule sections present (distinctive markers)
45
+ # - no unsubstituted ${...} template placeholders (outside code fences)
46
+ # - no reference-product leakage (tokens from .purity-banned-private; private repo only)
47
+ # - if .gemini/styleguide.md exists, it must be non-empty
48
+ #
49
+ # codex:
50
+ # - AGENTS.md exists, non-empty
51
+ # - all 5 universal-rule sections present
52
+ # - no unsubstituted ${...} placeholders (outside code fences)
53
+ # - no reference-product leakage
54
+ #
55
+ # windsurf:
56
+ # - .windsurfrules exists, non-empty
57
+ # - .windsurf/rules/ contains the 5 universal rule files
58
+ # (meta-discipline/operations/quality-gates/spec-as-you-go/workflow)
59
+ # - no unsubstituted ${...} placeholders (outside code fences)
60
+ # - no reference-product leakage
61
+ #
62
+ # ---------------------------------------------------------------------------
63
+
64
+ set -eu
65
+
66
+ TARGET="${1:-}"
67
+ ADAPTER="${2:-}"
68
+
69
+ if [ -z "$TARGET" ] || [ -z "$ADAPTER" ]; then
70
+ echo "Usage: $0 <target-dir> <adapter:cursor|copilot|claude|gemini|codex|windsurf>" >&2
71
+ exit 2
72
+ fi
73
+
74
+ if [ ! -d "$TARGET" ]; then
75
+ echo "ERROR: target dir does not exist: $TARGET" >&2
76
+ exit 2
77
+ fi
78
+
79
+ case "$ADAPTER" in
80
+ cursor|copilot|claude|gemini|codex|windsurf) ;;
81
+ *) echo "ERROR: unknown adapter '$ADAPTER'. Use cursor|copilot|claude|gemini|codex|windsurf." >&2; exit 2 ;;
82
+ esac
83
+
84
+ PASS=0
85
+ FAIL=0
86
+ FAILED_FILES=""
87
+
88
+ emit_pass() {
89
+ printf " PASS %s\n" "$1"
90
+ PASS=$((PASS + 1))
91
+ }
92
+
93
+ emit_fail() {
94
+ printf " FAIL %s :: %s\n" "$1" "$2"
95
+ FAIL=$((FAIL + 1))
96
+ FAILED_FILES="$FAILED_FILES\n $1 — $2"
97
+ }
98
+
99
+ # ---- shared helpers ------------------------------------------------------
100
+
101
+ # Returns line number of opening "---" frontmatter delimiter (1 if present at line 1, 0 if absent).
102
+ fm_open_line() {
103
+ awk 'NR==1 && /^---$/ { print 1; exit } NR>=2 { print 0; exit }' "$1"
104
+ }
105
+
106
+ # Returns line number of closing "---" delimiter (must be > 1).
107
+ fm_close_line() {
108
+ awk 'NR>1 && /^---$/ { print NR; exit }' "$1"
109
+ }
110
+
111
+ # Sanity check on the markdown body (after frontmatter close).
112
+ # Returns "OK" or a reason string.
113
+ body_sanity() {
114
+ local file="$1"
115
+ local body_start="$2"
116
+
117
+ # Must have at least one heading (# or ##) somewhere in body.
118
+ local heading_count
119
+ heading_count=$(awk -v s="$body_start" 'NR>s && /^#/ { c++ } END { print c+0 }' "$file")
120
+ if [ "$heading_count" -lt 1 ]; then
121
+ echo "no_markdown_heading"
122
+ return
123
+ fi
124
+
125
+ # Code fence balance: count occurrences of ^``` (start of line).
126
+ local fence_count
127
+ fence_count=$(awk -v s="$body_start" 'NR>s && /^```/ { c++ } END { print c+0 }' "$file")
128
+ if [ $((fence_count % 2)) -ne 0 ]; then
129
+ echo "unbalanced_code_fences (count=$fence_count)"
130
+ return
131
+ fi
132
+
133
+ echo "OK"
134
+ }
135
+
136
+ # Extract a single-line frontmatter field value (between ":" and EOL).
137
+ # Usage: fm_field <file> <field-name> → value or empty
138
+ fm_field() {
139
+ awk -v key="$2" '
140
+ BEGIN { in_fm=0 }
141
+ NR==1 && /^---$/ { in_fm=1; next }
142
+ in_fm && /^---$/ { exit }
143
+ in_fm {
144
+ idx = index($0, ":")
145
+ if (idx > 0) {
146
+ k = substr($0, 1, idx-1)
147
+ v = substr($0, idx+1)
148
+ sub(/^[ \t]+/, "", k)
149
+ sub(/[ \t]+$/, "", k)
150
+ sub(/^[ \t]+/, "", v)
151
+ sub(/[ \t]+$/, "", v)
152
+ if (k == key) { print v; exit }
153
+ }
154
+ }
155
+ ' "$1"
156
+ }
157
+
158
+ # Detect if a frontmatter field value indicates a YAML array literal: starts with "[".
159
+ # Or block array (multiline) — caller decides.
160
+ is_inline_array() {
161
+ case "$1" in
162
+ \[*\]) return 0 ;;
163
+ *) return 1 ;;
164
+ esac
165
+ }
166
+
167
+ # Check whether a frontmatter has `paths:` followed by indented `- ` lines (block array).
168
+ fm_block_array_present() {
169
+ awk -v key="$2" '
170
+ BEGIN { in_fm=0; in_key=0 }
171
+ NR==1 && /^---$/ { in_fm=1; next }
172
+ in_fm && /^---$/ { exit }
173
+ in_fm && $0 ~ "^"key":" { in_key=1; next }
174
+ in_fm && in_key && /^[ \t]+-[ \t]/ { print "yes"; exit }
175
+ in_fm && in_key && /^[^ \t]/ { in_key=0 }
176
+ ' "$1"
177
+ }
178
+
179
+ # Returns non-empty if file is missing OR empty (zero / whitespace-only bytes).
180
+ file_empty() {
181
+ [ ! -s "$1" ] && { echo "empty"; return; }
182
+ # -s catches zero-byte. Also treat whitespace-only as empty.
183
+ if ! grep -q '[^[:space:]]' "$1"; then echo "empty"; fi
184
+ }
185
+
186
+ # Detect unsubstituted ${...} template placeholders OUTSIDE fenced code blocks.
187
+ # Legit bash parameter expansions live inside ```...``` fences and are ignored.
188
+ # Prints the first offending "line: text" or empty if clean.
189
+ unsubstituted_placeholder() {
190
+ awk '
191
+ BEGIN { infence = 0 }
192
+ /^```/ { infence = !infence; next }
193
+ !infence && /\$\{[A-Za-z_][A-Za-z0-9_]*\}/ { print NR": "$0; exit }
194
+ ' "$1"
195
+ }
196
+
197
+ # Detect reference-product leakage. Tokens come from the PRIVATE-only .purity-banned-private
198
+ # (not synced to the public mirror / npm); if absent (public repo), this is a no-op.
199
+ # Prints the matching line or empty if clean.
200
+ leakage_scan() {
201
+ local priv pat
202
+ priv="$(dirname "$0")/../.purity-banned-private"
203
+ [ -f "$priv" ] || return 0
204
+ pat="$(grep -vE '^#|^[[:space:]]*$' "$priv" | paste -sd'|' -)"
205
+ [ -n "$pat" ] || return 0
206
+ grep -nE "$pat" "$1" 2>/dev/null | head -1 || true
207
+ }
208
+
209
+ # Check that a file contains all 5 universal-rule sections by distinctive markers.
210
+ # Markers are derived from the rule titles emitted into the bundled GEMINI.md/AGENTS.md.
211
+ # Prints the name of the first MISSING section, or empty if all present.
212
+ missing_rule_section() {
213
+ local file="$1"
214
+ grep -qE '^#+ +Workflow — Plan-First Order' "$file" || { echo "workflow"; return; }
215
+ grep -qE '^#+ +Spec-as-you-go — Same-Turn' "$file" || { echo "spec-as-you-go"; return; }
216
+ grep -qE '^#+ +Quality Gates — Two-Stage Review' "$file" || { echo "quality-gates"; return; }
217
+ grep -qE '^#+ +Operations — Session Continuity' "$file" || { echo "operations"; return; }
218
+ grep -qE '^#+ +Meta-Discipline — How CONDUCTOR Stays' "$file" || { echo "meta-discipline"; return; }
219
+ }
220
+
221
+ # ---- gemini mode ---------------------------------------------------------
222
+
223
+ run_gemini() {
224
+ local main="$TARGET/GEMINI.md"
225
+ if [ ! -f "$main" ]; then
226
+ emit_fail "GEMINI.md" "file missing"
227
+ return
228
+ fi
229
+ if [ -n "$(file_empty "$main")" ]; then
230
+ emit_fail "GEMINI.md" "file is empty"
231
+ return
232
+ fi
233
+
234
+ local miss
235
+ miss=$(missing_rule_section "$main")
236
+ if [ -n "$miss" ]; then
237
+ emit_fail "GEMINI.md" "missing universal-rule section: $miss"
238
+ return
239
+ fi
240
+
241
+ local ph
242
+ ph=$(unsubstituted_placeholder "$main")
243
+ if [ -n "$ph" ]; then
244
+ emit_fail "GEMINI.md" "unsubstituted placeholder at $ph"
245
+ return
246
+ fi
247
+
248
+ local leak
249
+ leak=$(leakage_scan "$main")
250
+ if [ -n "$leak" ]; then
251
+ emit_fail "GEMINI.md" "reference-product leakage: $leak"
252
+ return
253
+ fi
254
+
255
+ emit_pass "GEMINI.md"
256
+
257
+ # Optional styleguide — if present must be non-empty.
258
+ local style="$TARGET/.gemini/styleguide.md"
259
+ if [ -f "$style" ]; then
260
+ if [ -n "$(file_empty "$style")" ]; then
261
+ emit_fail ".gemini/styleguide.md" "file is empty"
262
+ else
263
+ local sleak
264
+ sleak=$(leakage_scan "$style")
265
+ if [ -n "$sleak" ]; then
266
+ emit_fail ".gemini/styleguide.md" "reference-product leakage: $sleak"
267
+ else
268
+ emit_pass ".gemini/styleguide.md"
269
+ fi
270
+ fi
271
+ fi
272
+ }
273
+
274
+ # ---- codex mode ----------------------------------------------------------
275
+
276
+ run_codex() {
277
+ local main="$TARGET/AGENTS.md"
278
+ if [ ! -f "$main" ]; then
279
+ emit_fail "AGENTS.md" "file missing"
280
+ return
281
+ fi
282
+ if [ -n "$(file_empty "$main")" ]; then
283
+ emit_fail "AGENTS.md" "file is empty"
284
+ return
285
+ fi
286
+
287
+ local miss
288
+ miss=$(missing_rule_section "$main")
289
+ if [ -n "$miss" ]; then
290
+ emit_fail "AGENTS.md" "missing universal-rule section: $miss"
291
+ return
292
+ fi
293
+
294
+ local ph
295
+ ph=$(unsubstituted_placeholder "$main")
296
+ if [ -n "$ph" ]; then
297
+ emit_fail "AGENTS.md" "unsubstituted placeholder at $ph"
298
+ return
299
+ fi
300
+
301
+ local leak
302
+ leak=$(leakage_scan "$main")
303
+ if [ -n "$leak" ]; then
304
+ emit_fail "AGENTS.md" "reference-product leakage: $leak"
305
+ return
306
+ fi
307
+
308
+ emit_pass "AGENTS.md"
309
+ }
310
+
311
+ # ---- windsurf mode -------------------------------------------------------
312
+
313
+ validate_windsurf_file() {
314
+ local file="$1"
315
+ local rel="${file#"$TARGET/"}"
316
+
317
+ if [ -n "$(file_empty "$file")" ]; then
318
+ emit_fail "$rel" "file is empty"
319
+ return
320
+ fi
321
+ local ph
322
+ ph=$(unsubstituted_placeholder "$file")
323
+ if [ -n "$ph" ]; then
324
+ emit_fail "$rel" "unsubstituted placeholder at $ph"
325
+ return
326
+ fi
327
+ local leak
328
+ leak=$(leakage_scan "$file")
329
+ if [ -n "$leak" ]; then
330
+ emit_fail "$rel" "reference-product leakage: $leak"
331
+ return
332
+ fi
333
+ emit_pass "$rel"
334
+ }
335
+
336
+ run_windsurf() {
337
+ local top="$TARGET/.windsurfrules"
338
+ if [ ! -f "$top" ]; then
339
+ emit_fail ".windsurfrules" "file missing"
340
+ else
341
+ validate_windsurf_file "$top"
342
+ fi
343
+
344
+ # Rules moved to .devin/rules/ (2026 Devin Desktop rebrand); legacy .windsurf/rules/
345
+ # is still read, so accept either — prefer .devin/rules/.
346
+ local rules_dir rules_label
347
+ if [ -d "$TARGET/.devin/rules" ]; then
348
+ rules_dir="$TARGET/.devin/rules"; rules_label=".devin/rules"
349
+ elif [ -d "$TARGET/.windsurf/rules" ]; then
350
+ rules_dir="$TARGET/.windsurf/rules"; rules_label=".windsurf/rules"
351
+ else
352
+ emit_fail ".devin/rules/ (or legacy .windsurf/rules/)" "directory missing"
353
+ return
354
+ fi
355
+
356
+ # The 5 required universal rule files must each be present.
357
+ local required="meta-discipline operations quality-gates spec-as-you-go workflow"
358
+ local r
359
+ for r in $required; do
360
+ local rf="$rules_dir/$r.md"
361
+ if [ ! -f "$rf" ]; then
362
+ emit_fail "$rules_label/$r.md" "required universal rule file missing"
363
+ else
364
+ validate_windsurf_file "$rf"
365
+ fi
366
+ done
367
+ }
368
+
369
+ # ---- cursor mode ---------------------------------------------------------
370
+
371
+ validate_cursor_mdc() {
372
+ local file="$1"
373
+ local rel="${file#"$TARGET/"}"
374
+
375
+ local fm_open
376
+ fm_open=$(fm_open_line "$file")
377
+ if [ "$fm_open" != "1" ]; then
378
+ emit_fail "$rel" "missing frontmatter open delimiter (---)"
379
+ return
380
+ fi
381
+
382
+ local fm_close
383
+ fm_close=$(fm_close_line "$file")
384
+ if [ -z "$fm_close" ]; then
385
+ emit_fail "$rel" "missing frontmatter close delimiter"
386
+ return
387
+ fi
388
+
389
+ # description: must exist
390
+ local desc
391
+ desc=$(fm_field "$file" "description")
392
+ if [ -z "$desc" ]; then
393
+ emit_fail "$rel" "frontmatter missing 'description:' field"
394
+ return
395
+ fi
396
+
397
+ # globs: must exist (inline array OR block array OR string)
398
+ local globs
399
+ globs=$(fm_field "$file" "globs")
400
+ local globs_block
401
+ globs_block=$(fm_block_array_present "$file" "globs")
402
+ if [ -z "$globs" ] && [ -z "$globs_block" ]; then
403
+ emit_fail "$rel" "frontmatter missing 'globs:' field"
404
+ return
405
+ fi
406
+
407
+ # alwaysApply if present must be true|false
408
+ local always
409
+ always=$(fm_field "$file" "alwaysApply")
410
+ if [ -n "$always" ] && [ "$always" != "true" ] && [ "$always" != "false" ]; then
411
+ emit_fail "$rel" "alwaysApply must be true|false (got '$always')"
412
+ return
413
+ fi
414
+
415
+ local body_check
416
+ body_check=$(body_sanity "$file" "$fm_close")
417
+ if [ "$body_check" != "OK" ]; then
418
+ emit_fail "$rel" "body: $body_check"
419
+ return
420
+ fi
421
+
422
+ emit_pass "$rel"
423
+ }
424
+
425
+ run_cursor() {
426
+ local rules_dir="$TARGET/.cursor/rules"
427
+ if [ ! -d "$rules_dir" ]; then
428
+ emit_fail ".cursor/rules/" "directory missing"
429
+ return
430
+ fi
431
+ local found=0
432
+ for f in "$rules_dir"/*.mdc; do
433
+ [ -e "$f" ] || continue
434
+ found=$((found + 1))
435
+ validate_cursor_mdc "$f"
436
+ done
437
+ if [ "$found" -eq 0 ]; then
438
+ emit_fail ".cursor/rules/" "no .mdc files found"
439
+ fi
440
+ }
441
+
442
+ # ---- copilot mode --------------------------------------------------------
443
+
444
+ validate_copilot_top_level() {
445
+ local file="$TARGET/.github/copilot-instructions.md"
446
+ if [ ! -f "$file" ]; then
447
+ # Top-level bundle is optional when using --per-rule mode; downgrade to warning emit_pass-equivalent only if instructions/ exists.
448
+ if [ -d "$TARGET/.github/instructions" ]; then
449
+ printf " SKIP .github/copilot-instructions.md (per-rule mode active)\n"
450
+ return
451
+ fi
452
+ emit_fail ".github/copilot-instructions.md" "file missing"
453
+ return
454
+ fi
455
+
456
+ # No frontmatter required at top-level; just body sanity from line 0.
457
+ local body_check
458
+ body_check=$(body_sanity "$file" "0")
459
+ if [ "$body_check" != "OK" ]; then
460
+ emit_fail ".github/copilot-instructions.md" "body: $body_check"
461
+ return
462
+ fi
463
+ emit_pass ".github/copilot-instructions.md"
464
+ }
465
+
466
+ validate_copilot_instruction() {
467
+ local file="$1"
468
+ local rel="${file#"$TARGET/"}"
469
+
470
+ local fm_open
471
+ fm_open=$(fm_open_line "$file")
472
+ if [ "$fm_open" != "1" ]; then
473
+ emit_fail "$rel" "missing frontmatter open delimiter (---)"
474
+ return
475
+ fi
476
+
477
+ local fm_close
478
+ fm_close=$(fm_close_line "$file")
479
+ if [ -z "$fm_close" ]; then
480
+ emit_fail "$rel" "missing frontmatter close delimiter"
481
+ return
482
+ fi
483
+
484
+ # applyTo: must be a CSV string (NOT a YAML array) per Copilot spec.
485
+ local apply
486
+ apply=$(fm_field "$file" "applyTo")
487
+ if [ -z "$apply" ]; then
488
+ emit_fail "$rel" "frontmatter missing 'applyTo:' field"
489
+ return
490
+ fi
491
+ # Reject YAML inline array form (Copilot rejects it).
492
+ if is_inline_array "$apply"; then
493
+ emit_fail "$rel" "applyTo must be a CSV string in quotes, not a YAML array (got '$apply')"
494
+ return
495
+ fi
496
+ # Reject block-array form.
497
+ local apply_block
498
+ apply_block=$(fm_block_array_present "$file" "applyTo")
499
+ if [ -n "$apply_block" ]; then
500
+ emit_fail "$rel" "applyTo must be a CSV string in quotes, not a YAML block array"
501
+ return
502
+ fi
503
+
504
+ local body_check
505
+ body_check=$(body_sanity "$file" "$fm_close")
506
+ if [ "$body_check" != "OK" ]; then
507
+ emit_fail "$rel" "body: $body_check"
508
+ return
509
+ fi
510
+
511
+ emit_pass "$rel"
512
+ }
513
+
514
+ run_copilot() {
515
+ validate_copilot_top_level
516
+
517
+ local instr_dir="$TARGET/.github/instructions"
518
+ if [ -d "$instr_dir" ]; then
519
+ for f in "$instr_dir"/*.instructions.md; do
520
+ [ -e "$f" ] || continue
521
+ validate_copilot_instruction "$f"
522
+ done
523
+ fi
524
+ }
525
+
526
+ # ---- claude mode ---------------------------------------------------------
527
+
528
+ validate_claude_rule() {
529
+ local file="$1"
530
+ local rel="${file#"$TARGET/"}"
531
+
532
+ local fm_open
533
+ fm_open=$(fm_open_line "$file")
534
+ if [ "$fm_open" != "1" ]; then
535
+ emit_fail "$rel" "missing frontmatter open delimiter (---)"
536
+ return
537
+ fi
538
+
539
+ local fm_close
540
+ fm_close=$(fm_close_line "$file")
541
+ if [ -z "$fm_close" ]; then
542
+ emit_fail "$rel" "missing frontmatter close delimiter"
543
+ return
544
+ fi
545
+
546
+ # paths: must be a block array (one or more "- " entries).
547
+ local paths_block
548
+ paths_block=$(fm_block_array_present "$file" "paths")
549
+ local paths_inline
550
+ paths_inline=$(fm_field "$file" "paths")
551
+ if [ -z "$paths_block" ] && ! is_inline_array "$paths_inline"; then
552
+ emit_fail "$rel" "frontmatter 'paths:' must be a YAML array (block or inline)"
553
+ return
554
+ fi
555
+
556
+ local body_check
557
+ body_check=$(body_sanity "$file" "$fm_close")
558
+ if [ "$body_check" != "OK" ]; then
559
+ emit_fail "$rel" "body: $body_check"
560
+ return
561
+ fi
562
+
563
+ emit_pass "$rel"
564
+ }
565
+
566
+ run_claude() {
567
+ local rules_dir="$TARGET/.claude/rules"
568
+ if [ ! -d "$rules_dir" ]; then
569
+ emit_fail ".claude/rules/" "directory missing"
570
+ return
571
+ fi
572
+ local found=0
573
+ for f in "$rules_dir"/*.md; do
574
+ [ -e "$f" ] || continue
575
+ found=$((found + 1))
576
+ validate_claude_rule "$f"
577
+ done
578
+ if [ "$found" -eq 0 ]; then
579
+ emit_fail ".claude/rules/" "no .md files found"
580
+ fi
581
+ }
582
+
583
+ # ---- main ---------------------------------------------------------------
584
+
585
+ echo "=========================================="
586
+ echo " CONDUCTOR adapter-output validator"
587
+ echo " target = $TARGET"
588
+ echo " adapter = $ADAPTER"
589
+ echo "=========================================="
590
+
591
+ case "$ADAPTER" in
592
+ cursor) run_cursor ;;
593
+ copilot) run_copilot ;;
594
+ claude) run_claude ;;
595
+ gemini) run_gemini ;;
596
+ codex) run_codex ;;
597
+ windsurf) run_windsurf ;;
598
+ esac
599
+
600
+ echo ""
601
+ echo "------------------------------------------"
602
+ echo " Aggregate: PASS=$PASS FAIL=$FAIL"
603
+ if [ "$FAIL" -gt 0 ]; then
604
+ printf " Failures:%b\n" "$FAILED_FILES"
605
+ fi
606
+ echo "------------------------------------------"
607
+
608
+ if [ "$FAIL" -gt 0 ]; then
609
+ exit 1
610
+ fi
611
+ exit 0