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.
- package/CHANGELOG.md +64 -0
- package/LICENSE +219 -0
- package/NOTICE +11 -0
- package/README.md +526 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/TRADEMARKS.md +26 -0
- package/VISION.md +106 -0
- package/adapters/README.md +121 -0
- package/adapters/claude/README.md +102 -0
- package/adapters/claude/SUPPORTED-FEATURES.md +66 -0
- package/adapters/claude/hookify-templates/.recipe-scoped +9 -0
- package/adapters/claude/hookify-templates/README.md +93 -0
- package/adapters/claude/hookify-templates/block-completion-claim-without-push.local.md.template +56 -0
- package/adapters/claude/hookify-templates/block-direct-push-protected-branch.local.md.template +18 -0
- package/adapters/claude/hookify-templates/block-force-push-protected-branch.local.md.template +42 -0
- package/adapters/claude/hookify-templates/block-server-secret-in-client.local.md.template +36 -0
- package/adapters/claude/hookify-templates/warn-any-type-added.local.md.template +44 -0
- package/adapters/claude/hookify-templates/warn-commit-without-pre-commit-review.local.md.template +52 -0
- package/adapters/claude/hookify-templates/warn-console-direct.local.md.template +55 -0
- package/adapters/claude/hookify-templates/warn-create-table-without-access-control.local.md.template +35 -0
- package/adapters/claude/hookify-templates/warn-current-work-without-remaining-tasks.local.md.template +36 -0
- package/adapters/claude/hookify-templates/warn-gh-pr-create-without-pre-merge-review.local.md.template +38 -0
- package/adapters/claude/hookify-templates/warn-hardcoded-text-without-i18n-key.local.md.template +21 -0
- package/adapters/claude/hookify-templates/warn-on-gh-pr-merge.local.md.template +33 -0
- package/adapters/claude/hookify-templates/warn-plan-spec-without-remaining-tasks.local.md.template +44 -0
- package/adapters/claude/hookify-templates/warn-raw-hex-instead-of-token.local.md.template +21 -0
- package/adapters/claude/hookify-templates/warn-security-definer-without-search-path.local.md.template +30 -0
- package/adapters/claude/hookify-templates/warn-stop-commit-without-current-work.local.md.template +47 -0
- package/adapters/claude/hookify-templates/warn-user-manual-completion.local.md.template +40 -0
- package/adapters/claude/transform-spec.md +140 -0
- package/adapters/claude/transform.sh +1169 -0
- package/adapters/codex/README.md +87 -0
- package/adapters/codex/SUPPORTED-FEATURES.md +66 -0
- package/adapters/codex/transform-spec.md +90 -0
- package/adapters/codex/transform.sh +748 -0
- package/adapters/copilot/README.md +88 -0
- package/adapters/copilot/SUPPORTED-FEATURES.md +73 -0
- package/adapters/copilot/transform-spec.md +111 -0
- package/adapters/copilot/transform.sh +757 -0
- package/adapters/cursor/README.md +87 -0
- package/adapters/cursor/SUPPORTED-FEATURES.md +90 -0
- package/adapters/cursor/transform-spec.md +124 -0
- package/adapters/cursor/transform.sh +731 -0
- package/adapters/gemini/README.md +93 -0
- package/adapters/gemini/SUPPORTED-FEATURES.md +80 -0
- package/adapters/gemini/transform-spec.md +102 -0
- package/adapters/gemini/transform.sh +844 -0
- package/adapters/windsurf/README.md +93 -0
- package/adapters/windsurf/SUPPORTED-FEATURES.md +67 -0
- package/adapters/windsurf/transform-spec.md +99 -0
- package/adapters/windsurf/transform.sh +727 -0
- package/bin/omniconductor.js +130 -0
- package/core/README.md +64 -0
- package/core/anti-patterns/README.md +142 -0
- package/core/anti-patterns/dynamic-system-prompt.md +113 -0
- package/core/anti-patterns/frequent-rule-file-edit.md +83 -0
- package/core/anti-patterns/large-file-read-no-range.md +90 -0
- package/core/anti-patterns/no-sub-agent-dispatch.md +89 -0
- package/core/anti-patterns/single-monolithic-rule-file.md +99 -0
- package/core/anti-patterns/skill-eager-load.md +83 -0
- package/core/anti-patterns/tool-call-spam.md +93 -0
- package/core/docs-templates/CURRENT_WORK.md +64 -0
- package/core/docs-templates/INDEX.md +57 -0
- package/core/docs-templates/PLANS.md +44 -0
- package/core/docs-templates/README.md +40 -0
- package/core/docs-templates/REMAINING_TASKS.md +43 -0
- package/core/docs-templates/TASKS.md +51 -0
- package/core/docs-templates/specs/_example.md +121 -0
- package/core/hooks/README.md +62 -0
- package/core/hooks/pretool-agent-routing.sh.template +97 -0
- package/core/hooks/pretool-commit-current-work-check.sh.template +103 -0
- package/core/hooks/pretool-commit-test-coverage-check.sh.template +116 -0
- package/core/hooks/pretool-large-file-read-guard.sh.template +117 -0
- package/core/hooks/stop-cache-hit-baseline-check.sh.template +133 -0
- package/core/hooks/stop-r6-review-check.sh.template +80 -0
- package/core/hooks/stop-session-log-check.sh.template +101 -0
- package/core/hooks/stop-trajectory-log.sh.template +96 -0
- package/core/memory-pattern/EXAMPLES.md +114 -0
- package/core/memory-pattern/README.md +133 -0
- package/core/recipes/README.md +46 -0
- package/core/recipes/auto-mock-data.md +82 -0
- package/core/recipes/branch-strategy.md +100 -0
- package/core/recipes/coding-conventions.md +123 -0
- package/core/recipes/database-discipline.md +65 -0
- package/core/recipes/debugging.md +143 -0
- package/core/recipes/design-system.md +30 -0
- package/core/recipes/i18n.md +118 -0
- package/core/recipes/monorepo.md +101 -0
- package/core/recipes/self-improvement.md +61 -0
- package/core/recipes/tdd.md +128 -0
- package/core/recipes/web-mobile-parity.md +87 -0
- package/core/reflector/SCHEDULING.md +83 -0
- package/core/reflector/prune-lessons.sh +105 -0
- package/core/reflector/reflect-brief.md +11 -0
- package/core/reflector/reflect.command.md +13 -0
- package/core/reflector/run-weekly.sh +54 -0
- package/core/reflector/trajectory-log.sh +55 -0
- package/core/roles/README.md +61 -0
- package/core/roles/builder.md +75 -0
- package/core/roles/designer.md +93 -0
- package/core/roles/helper.md +70 -0
- package/core/roles/planner.md +123 -0
- package/core/roles/reflector.md +72 -0
- package/core/roles/reviewer.md +104 -0
- package/core/roles/scribe.md +89 -0
- package/core/universal-rules/README.md +71 -0
- package/core/universal-rules/meta-discipline.md +292 -0
- package/core/universal-rules/operations.md +162 -0
- package/core/universal-rules/quality-gates.md +191 -0
- package/core/universal-rules/spec-as-you-go.md +123 -0
- package/core/universal-rules/workflow.md +153 -0
- package/core/workflow/PHASES.md +134 -0
- package/core/workflow/README.md +56 -0
- package/docs/MANUAL-INSTALL.md +456 -0
- package/package.json +52 -0
- package/tools/check-framework-purity.sh +91 -0
- package/tools/measure-tokens.sh +169 -0
- 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
|