Problem summary
Conductor v0.1.17's --workspace-instructions discovers .github/instructions/**/*.instructions.md but filters them through _is_always_on_instructions_file, which loads ONLY files with applyTo: "**" (config/instructions.py:145-166). Files with any scoped applyTo glob — **/*.cs, tests/**, src/**, services/foo/**, etc. — are silently skipped.
GitHub Copilot's documented applyTo semantic is per-path scoping: a file with applyTo: "**/*.cs" applies whenever any C# file is involved; one with applyTo: "services/foo/**" applies to that service's tree. These are correct uses of the convention, not misuse. Conductor's all-or-nothing filter doesn't honor them — it treats anything other than the literal "**" as "skip unconditionally," which excludes the bulk of real-world per-area instructions.
The gap applies to any repo that uses applyTo for scoping — single-service flat repos with per-language scoping, library repos splitting src/ vs examples/ vs docs/, and monorepos with per-service scoping.
Evidence (ground-truthed against upstream/main, conductor v0.1.17)
Where the filter lives
src/conductor/config/instructions.py:
# L177
CONVENTIONS: list[Convention] = [
ConventionFile("AGENTS.md"),
ConventionFile(".github/copilot-instructions.md"),
ConventionFile("CLAUDE.md"),
ConventionDirectory(
path=".github/instructions",
pattern="*.instructions.md",
include_file=_is_always_on_instructions_file, # ← the filter
recursive=True,
),
]
# L145
def _is_always_on_instructions_file(path: Path) -> bool:
fm = _parse_frontmatter(path)
if fm is None:
return False
return fm.get("applyTo") == "**" # L166 — exact-equality "**" only
CWD is already available at the right seam: cli/run.py:1223 passes Path.cwd() as auto_discover_dir to discover_workspace_instructions. The plumbing for CWD-implicit scoping exists; only the per-file filter needs to learn how to use it.
Real-world data (Azure Chaos Studio)
Inventory of .github/instructions/*.instructions.md files in a real Azure repo. Most globs are language- or area-scoped rather than service-scoped — i.e., the same pattern that appears in single-service repos:
| File |
applyTo |
Loaded by conductor today? |
arm-rpc-guidelines.instructions.md |
services/GW/**;services/BE/** |
❌ |
csharp-coding-standards.instructions.md |
**/*.cs |
❌ |
eng_ms.instructions.md |
/docs/eng.ms/*.*; /docs/eng.ms/**/*.* |
❌ |
engineering-standards.instructions.md |
**/*.cs,**/*.csproj,**/Directory.Packages.props,**/global.json,**/*.bicep,**/*.md |
❌ |
poc-warning.instructions.md |
(uses non-standard glob: field, no applyTo) |
❌ |
portal-ux-patterns.instructions.md |
**/portal-extension/**,**/portal-ux/**,**/ux-accelerator/** |
❌ |
specs-arm-api.instructions.md |
docs/specs/arm-api-published/*.*; docs/specs/arm-api-published/**/*.* |
❌ |
testing-standards.instructions.md |
**/*Tests.cs,**/*Tests/**,**/*UnitTests*/**,**/*HermeticTests*/** |
❌ |
workload-identity.instructions.md |
**/kubectl/*.yaml,**/kubectl/*.yml,**/identity.bicep,**/ev2/** |
❌ |
Total: 52 KB / ~13K tokens. 0 / 9 files loaded today. All 9 contain real engineering guidance (ARM RPC patterns, async/await rules, testing categories, AKS workload identity setup, etc.).
These globs are not misuse of applyTo — they correctly express the convention's per-path scoping. Promoting them all to applyTo: "**" would falsify the convention's intent and inject (e.g.) AKS workload-identity guidance into agents working on portal-extension code.
Repro
mkdir repro && cd repro
git init -q
mkdir -p .github/instructions src tests
echo "Repo-wide rule." > .github/copilot-instructions.md
cat > .github/instructions/csharp.instructions.md <<'EOF'
---
description: C# coding standards
applyTo: "**/*.cs"
---
Use file-scoped namespaces.
EOF
cat > .github/instructions/testing.instructions.md <<'EOF'
---
description: Test-specific patterns
applyTo: "tests/**"
---
Tests must use the Arrange-Act-Assert pattern.
EOF
cat > wf.yaml <<'EOF'
workflow:
name: repro
entry_point: agent_a
agents:
- name: agent_a
prompt: "Echo your workspace_instructions."
routes:
- to: $end
EOF
conductor run --workspace-instructions wf.yaml
Expected: agent prompt's <workspace_instructions> block contains content from csharp.instructions.md and testing.instructions.md.
Actual: block contains only copilot-instructions.md. Both scoped files are silently skipped.
Proposed design
Three pieces. The first is the correctness fix; the second is the minimum generalization to keep the seam clean for future scoped conventions; the third is inspectability so the implicit behavior is debuggable.
Up-front honest framing
At repo-root CWD — which is the dominant case for programmatic launchers (skyship Invoke-Ship.ps1, octane run-phases.py, ATS-Copilot pr-reviewer, all of which cd to the user's repo root before invoking conductor) — bidirectional overlap means "every scoped file overlaps with the root subtree" → load everything. At the root, this design is effectively "drop the applyTo filter." That is strictly better than today's silent skip (correctness fix), but it is not token narrowing for programmatic callers. Narrowing only kicks in when a human cds into a subdir (cd services/GW/ && conductor run …).
This is a deliberate tradeoff: correctness over token economy for v1. Programmatic narrowing can be tackled later if/when token bloat at the repo root becomes a measured problem.
Piece 1 — CWD-implicit scope filter
Replace _is_always_on_instructions_file's applyTo == "**" check with bidirectional glob-overlap against the existing auto_discover_dir (CWD).
Algorithm:
- From CWD, walk up to find the workspace root (git root, or deepest ancestor with a known convention — same logic conductor already uses for discovery).
- Compute CWD-relative-to-workspace-root (e.g.,
services/GW/, or . when CWD is the workspace root).
- For each discovered instructions file, ask the convention for the scope glob (see Piece 2).
- Load the file if any of:
- The convention has no scope concept (e.g.,
AGENTS.md) — load unconditionally, matching today's behavior.
extract_scope returns None — equivalent to "always on" (covers both applyTo: "**" and unscoped frontmatter).
- The scope glob's subtree overlaps with the CWD-relative subtree, in either direction.
Bidirectional overlap. applyTo: services/GW/** matches when CWD is services/GW/src/ (user is inside the scope) and when CWD is . (the scope is inside the user's CWD subtree). Strict one-direction glob match would exclude one of these common cases. A "shares-any-literal-prefix-or-could-match-any-file-under-CWD" approximation is sufficient; precise glob intersection is undecidable in general but unnecessary here.
Piece 2 — Minimal generalization of the existing seam
ConventionDirectory.include_file: Callable[[Path], bool] | None (line 77) is the existing pluggable filter slot — exactly the right place to extend. Two minimal changes:
@dataclass(frozen=True)
class ConventionDirectory:
path: str
pattern: str
extract_scope: Callable[[Path], str | None] | None = None # NEW
recursive: bool = True
include_file: Callable[[Path], bool] | None = None # retained briefly for back-compat
The convention author declares only how to read their scope field. The universal overlap test lives in core (one source of truth):
# .github/instructions/ registration
ConventionDirectory(
path=".github/instructions",
pattern="*.instructions.md",
extract_scope=_extract_apply_to,
)
def _extract_apply_to(path: Path) -> str | None:
fm = _parse_frontmatter(path)
if fm is None:
return None # no frontmatter → no scope claim → load
apply_to = fm.get("applyTo")
if apply_to is None or apply_to == "**":
return None # "always on" semantic → load
return apply_to
_walk_directory_convention does the overlap test against auto_discover_dir once, generically:
if convention.extract_scope is not None:
scope = convention.extract_scope(file_path)
if scope is not None and not _scope_overlaps(scope, cwd_rel_to_root):
continue
When a second scoped convention shows up (e.g., Cursor's .cursor/rules/*.mdc with globs: frontmatter), it registers its own extract_scope — no new abstraction, no core changes, same overlap semantic.
Not introducing: a ConventionHandler Protocol / registry. With exactly one scoped convention today, the right shape for a registry is undervalidated; extending the existing ConventionDirectory slot is the YAGNI-honoring minimum. Revisit if a third scoped convention reveals a shape the dataclass can't accommodate.
Piece 3 — --print-loaded-instructions (v1 deliverable)
Implicit behavior trades discoverability for ergonomics — same command in different directories loads different files. Ship inspectability alongside the implicit filter, not after.
conductor run --workspace-instructions --print-loaded-instructions wf.yaml
# prints the resolved list of loaded instruction files (with their effective
# scope and the overlap reason) to stderr before launching the workflow
Could also fold into existing --verbose output instead of a new flag if that's preferred — open to either. The requirement is that "why isn't my instruction loading" is answerable without instrumenting source code.
What we're explicitly NOT doing
- No new
--workspace-scope flag. The repeatable --instructions <path> flag (cli/app.py:326-329, instructions.py:410-424) already exists as the force-load escape hatch for callers that know exactly what they want. Adding a third knob would be one too many. Programmatic launchers that need finer narrowing than CWD can list specific files via --instructions, or cd first.
- No
ConventionHandler Protocol / registry. YAGNI — extend the existing ConventionDirectory slot until a second scoped convention validates the registry shape.
Principles satisfied
- Convention over configuration. CWD is the universal CLI primitive every tool (git, grep, npm) already uses. No new concept for users.
- Single source of truth. CWD is where the user "is"; no parallel flag to keep in sync.
- Faithful to the convention's own semantics.
applyTo is path-glob scoping; evaluating against actual filesystem paths is what the convention describes. The overlap test lives in core (one place), not duplicated across convention authors.
- Right architectural seam. The fix lives in conductor's discovery layer (
config/instructions.py), upstream of every provider adapter. One change, all providers and all callers benefit.
- Graceful degradation. Conventions without a
extract_scope keep working unchanged. New conventions plug in with one callable.
- Symmetric primitive. Same mechanism (CWD) handles both ends of the scope spectrum:
cd repo-root = broad, cd services/GW/src/ = narrow.
- Composable with existing tools. Anyone wrapping conductor in a script can narrow scope by
cd-ing first. Standard Unix composition.
- YAGNI. No
--workspace-scope flag, no ConventionHandler registry — both can land later if real cases demand them.
- Backward compatibility.
extract_scope is a new optional field; include_file retained during migration. No existing convention author is broken.
Honest tensions
- Implicit behavior trades discoverability for ergonomics. Addressed by Piece 3 (
--print-loaded-instructions) shipping in the same change, not as a follow-up.
- At repo-root CWD, this loads all scoped files. Stated plainly above. Acceptable v1 tradeoff (correctness > token economy); programmatic narrowing can come later as
--workspace-scope or similar if measured to matter.
extract_scope -> str | None implicitly commits to "scope = path glob." Today's two known scoped conventions (applyTo, Cursor's globs:) both fit. A wildly different scope semantic (RBAC role, language ID) would need a different mechanism — acceptable per YAGNI.
- Glob-overlap semantics need care. The bidirectional-overlap predicate is more nuanced than strict glob match. Mitigation: an approximation (shared literal prefix + wildcard tolerance) is sufficient; precise intersection is unnecessary and undecidable in general.
Comparison with the existing --instructions <path> flag
| Axis |
--instructions <path> (today) |
This proposal (CWD-implicit + extract_scope) |
| What you declare |
Specific files to load |
Nothing (CWD is implicit) |
| Who decides relevance |
Caller (must enumerate files) |
Conductor (consults each convention's scope predicate) |
| Convention semantics |
Bypassed — files loaded unconditionally |
Honored — applyTo / globs / etc. respected via convention's extract_scope |
| Per-call cardinality |
N flags (one per file) |
Zero — CWD-implicit |
| Caller knowledge required |
File inventory + per-file relevance |
Just cd to the right place |
Complementary, not redundant. --instructions remains as the force-load escape hatch ("ignore the convention, give me these files"). This proposal is the principled mechanism ("honor the convention; here's where I'm working").
Integration impact: most callers need zero changes
Programmatic launchers across the Microsoft scenarios all already cd to the user's repo root before invoking conductor:
| Caller |
How it invokes conductor today |
Behavior after fix |
Skyship Invoke-Ship.ps1 (stack / fix-review) |
conductor run from repo root |
✅ All applyTo-matching files load automatically (= correctness fix; not narrowing) |
Octane pr-orchestrator run-phases.py |
conductor run from repo root |
✅ Same |
Octane octane-workflow-implement |
conductor run from repo root |
✅ Same |
| ATS-Copilot pr-reviewer |
conductor run from repo root |
✅ Same |
Manual conductor run from SKILL.md docs |
User's CWD |
✅ Honors wherever the user is |
User cd services/GW && conductor run … |
services/GW/ |
✅ Narrows to GW automatically — no flag needed |
No follow-up integration PRs needed in skyship / octane / ATS for the correctness fix. The --workspace-instructions adoption PRs already shipped (or pending review) are exactly the right pre-work; the conductor fix lights them up.
Affected users
Anyone using --workspace-instructions against a repo whose .github/instructions/ files use scoped applyTo globs. Affected categories include single-service flat repos (**/*.cs, tests/**), library repos (src/ vs examples/ vs docs/), framework-scoped repos (**/*.tsx, scripts/**), and monorepos with per-service scoping (the most acute case).
Direct evidence: Azure Chaos Studio (9 files, 0 loaded today; 6/9 globs are language- or area-scoped and would appear identically in non-monorepo repos). Copilot Chat's docs explicitly recommend applyTo for per-area scoping in any repo, so this pattern is conventional, not anomalous.
Problem summary
Conductor v0.1.17's
--workspace-instructionsdiscovers.github/instructions/**/*.instructions.mdbut filters them through_is_always_on_instructions_file, which loads ONLY files withapplyTo: "**"(config/instructions.py:145-166). Files with any scopedapplyToglob —**/*.cs,tests/**,src/**,services/foo/**, etc. — are silently skipped.GitHub Copilot's documented
applyTosemantic is per-path scoping: a file withapplyTo: "**/*.cs"applies whenever any C# file is involved; one withapplyTo: "services/foo/**"applies to that service's tree. These are correct uses of the convention, not misuse. Conductor's all-or-nothing filter doesn't honor them — it treats anything other than the literal"**"as "skip unconditionally," which excludes the bulk of real-world per-area instructions.The gap applies to any repo that uses
applyTofor scoping — single-service flat repos with per-language scoping, library repos splittingsrc/vsexamples/vsdocs/, and monorepos with per-service scoping.Evidence (ground-truthed against upstream/main, conductor v0.1.17)
Where the filter lives
src/conductor/config/instructions.py:CWD is already available at the right seam:
cli/run.py:1223passesPath.cwd()asauto_discover_dirtodiscover_workspace_instructions. The plumbing for CWD-implicit scoping exists; only the per-file filter needs to learn how to use it.Real-world data (Azure Chaos Studio)
Inventory of
.github/instructions/*.instructions.mdfiles in a real Azure repo. Most globs are language- or area-scoped rather than service-scoped — i.e., the same pattern that appears in single-service repos:arm-rpc-guidelines.instructions.mdservices/GW/**;services/BE/**csharp-coding-standards.instructions.md**/*.cseng_ms.instructions.md/docs/eng.ms/*.*; /docs/eng.ms/**/*.*engineering-standards.instructions.md**/*.cs,**/*.csproj,**/Directory.Packages.props,**/global.json,**/*.bicep,**/*.mdpoc-warning.instructions.mdglob:field, noapplyTo)portal-ux-patterns.instructions.md**/portal-extension/**,**/portal-ux/**,**/ux-accelerator/**specs-arm-api.instructions.mddocs/specs/arm-api-published/*.*; docs/specs/arm-api-published/**/*.*testing-standards.instructions.md**/*Tests.cs,**/*Tests/**,**/*UnitTests*/**,**/*HermeticTests*/**workload-identity.instructions.md**/kubectl/*.yaml,**/kubectl/*.yml,**/identity.bicep,**/ev2/**Total: 52 KB / ~13K tokens. 0 / 9 files loaded today. All 9 contain real engineering guidance (ARM RPC patterns, async/await rules, testing categories, AKS workload identity setup, etc.).
These globs are not misuse of
applyTo— they correctly express the convention's per-path scoping. Promoting them all toapplyTo: "**"would falsify the convention's intent and inject (e.g.) AKS workload-identity guidance into agents working on portal-extension code.Repro
Expected: agent prompt's
<workspace_instructions>block contains content fromcsharp.instructions.mdandtesting.instructions.md.Actual: block contains only
copilot-instructions.md. Both scoped files are silently skipped.Proposed design
Three pieces. The first is the correctness fix; the second is the minimum generalization to keep the seam clean for future scoped conventions; the third is inspectability so the implicit behavior is debuggable.
Up-front honest framing
At repo-root CWD — which is the dominant case for programmatic launchers (skyship
Invoke-Ship.ps1, octanerun-phases.py, ATS-Copilot pr-reviewer, all of whichcdto the user's repo root before invoking conductor) — bidirectional overlap means "every scoped file overlaps with the root subtree" → load everything. At the root, this design is effectively "drop theapplyTofilter." That is strictly better than today's silent skip (correctness fix), but it is not token narrowing for programmatic callers. Narrowing only kicks in when a humancds into a subdir (cd services/GW/ && conductor run …).This is a deliberate tradeoff: correctness over token economy for v1. Programmatic narrowing can be tackled later if/when token bloat at the repo root becomes a measured problem.
Piece 1 — CWD-implicit scope filter
Replace
_is_always_on_instructions_file'sapplyTo == "**"check with bidirectional glob-overlap against the existingauto_discover_dir(CWD).Algorithm:
services/GW/, or.when CWD is the workspace root).AGENTS.md) — load unconditionally, matching today's behavior.extract_scopereturnsNone— equivalent to "always on" (covers bothapplyTo: "**"and unscoped frontmatter).Bidirectional overlap.
applyTo: services/GW/**matches when CWD isservices/GW/src/(user is inside the scope) and when CWD is.(the scope is inside the user's CWD subtree). Strict one-direction glob match would exclude one of these common cases. A "shares-any-literal-prefix-or-could-match-any-file-under-CWD" approximation is sufficient; precise glob intersection is undecidable in general but unnecessary here.Piece 2 — Minimal generalization of the existing seam
ConventionDirectory.include_file: Callable[[Path], bool] | None(line 77) is the existing pluggable filter slot — exactly the right place to extend. Two minimal changes:The convention author declares only how to read their scope field. The universal overlap test lives in core (one source of truth):
_walk_directory_conventiondoes the overlap test againstauto_discover_dironce, generically:When a second scoped convention shows up (e.g., Cursor's
.cursor/rules/*.mdcwithglobs:frontmatter), it registers its ownextract_scope— no new abstraction, no core changes, same overlap semantic.Not introducing: a
ConventionHandlerProtocol / registry. With exactly one scoped convention today, the right shape for a registry is undervalidated; extending the existingConventionDirectoryslot is the YAGNI-honoring minimum. Revisit if a third scoped convention reveals a shape the dataclass can't accommodate.Piece 3 —
--print-loaded-instructions(v1 deliverable)Implicit behavior trades discoverability for ergonomics — same command in different directories loads different files. Ship inspectability alongside the implicit filter, not after.
Could also fold into existing
--verboseoutput instead of a new flag if that's preferred — open to either. The requirement is that "why isn't my instruction loading" is answerable without instrumenting source code.What we're explicitly NOT doing
--workspace-scopeflag. The repeatable--instructions <path>flag (cli/app.py:326-329,instructions.py:410-424) already exists as the force-load escape hatch for callers that know exactly what they want. Adding a third knob would be one too many. Programmatic launchers that need finer narrowing than CWD can list specific files via--instructions, orcdfirst.ConventionHandlerProtocol / registry. YAGNI — extend the existingConventionDirectoryslot until a second scoped convention validates the registry shape.Principles satisfied
applyTois path-glob scoping; evaluating against actual filesystem paths is what the convention describes. The overlap test lives in core (one place), not duplicated across convention authors.config/instructions.py), upstream of every provider adapter. One change, all providers and all callers benefit.extract_scopekeep working unchanged. New conventions plug in with one callable.cd repo-root= broad,cd services/GW/src/= narrow.cd-ing first. Standard Unix composition.--workspace-scopeflag, noConventionHandlerregistry — both can land later if real cases demand them.extract_scopeis a new optional field;include_fileretained during migration. No existing convention author is broken.Honest tensions
--print-loaded-instructions) shipping in the same change, not as a follow-up.--workspace-scopeor similar if measured to matter.extract_scope -> str | Noneimplicitly commits to "scope = path glob." Today's two known scoped conventions (applyTo, Cursor'sglobs:) both fit. A wildly different scope semantic (RBAC role, language ID) would need a different mechanism — acceptable per YAGNI.Comparison with the existing
--instructions <path>flag--instructions <path>(today)extract_scope)applyTo/globs/ etc. respected via convention'sextract_scopecdto the right placeComplementary, not redundant.
--instructionsremains as the force-load escape hatch ("ignore the convention, give me these files"). This proposal is the principled mechanism ("honor the convention; here's where I'm working").Integration impact: most callers need zero changes
Programmatic launchers across the Microsoft scenarios all already
cdto the user's repo root before invoking conductor:Invoke-Ship.ps1(stack / fix-review)conductor runfrom repo rootapplyTo-matching files load automatically (= correctness fix; not narrowing)pr-orchestratorrun-phases.pyconductor runfrom repo rootoctane-workflow-implementconductor runfrom repo rootconductor runfrom repo rootconductor runfrom SKILL.md docscd services/GW && conductor run …services/GW/No follow-up integration PRs needed in skyship / octane / ATS for the correctness fix. The
--workspace-instructionsadoption PRs already shipped (or pending review) are exactly the right pre-work; the conductor fix lights them up.Affected users
Anyone using
--workspace-instructionsagainst a repo whose.github/instructions/files use scopedapplyToglobs. Affected categories include single-service flat repos (**/*.cs,tests/**), library repos (src/vsexamples/vsdocs/), framework-scoped repos (**/*.tsx,scripts/**), and monorepos with per-service scoping (the most acute case).Direct evidence: Azure Chaos Studio (9 files, 0 loaded today; 6/9 globs are language- or area-scoped and would appear identically in non-monorepo repos). Copilot Chat's docs explicitly recommend
applyTofor per-area scoping in any repo, so this pattern is conventional, not anomalous.