Skip to content

Commit 9cd8a1c

Browse files
MrHinshclaude
andauthored
Close all 9 DSL migration gaps (spec 038): identity resolution pipeline + Nodes/Teams/OTel fixes (#122)
## Summary Closes **all 9 open gaps** in `analysis/dsl-gaps-detected.md` (GAP-001 → GAP-009) — spec [`038-close-dsl-gaps`](specs/038-close-dsl-gaps/spec.md). All gaps are marked `Status: RESOLVED` (0 OPEN). Green on **net10 and net481**; full suite **1,411 passed / 2 skipped (live ADO) / 0 failed**. ## What changed **Identity resolution pipeline (GAP-001)** — the headline work: - New abstractions in `Abstractions.Agent/Identity`: `IIdentityAdapter`, `IdentityCandidate`, `IIdentityMatchingStrategy` (+ `IdentityMatch`). - `IdentitiesOrchestrator.PrepareAsync` implements resolution steps 2–3 (UPN → display-name) against the **target tenant**, caches results, writes `prepare-report.json`, emits the `identity.prepare` span + `identity.adapter.query` child spans + the full prepare metric set. - `IIdentityTranslationTool.Translate()` (renamed from `IIdentityLookupTool.Resolve()` — **history preserved via `git mv`**) reads the cache synchronously. Adds `DefaultIdentity`. - **All three connectors** implement `IIdentityAdapter` (FR-005/019): `SimulatedIdentityAdapter`, `AzureDevOpsIdentityAdapter` (ADO SDK `IdentityHttpClient`), `TfsIdentityAdapter` (net481, reduced-capability empty+Warning). Dispatched by `CompositeIdentityAdapter` on target connector type via `AddIdentityAdapter<T>`. - Matching strategies **own their adapter fetch** (`ResolveAsync`) — no magic-string dispatch in the orchestrator. **Other gaps:** - GAP-002/003: `NodesModule` `ReplicateSourceTree` skip-guard; `_NodeTransformTool`→`_nodeTranslationTool`; `INodeEnsurer` confirmed absent. - GAP-004: default-team structured warning verified. - GAP-005: `TeamImportOrchestrator.TranslatePath()` returns `null` on untranslatable (no silent pass-through); callers skip + warn. - GAP-006: skip team members whose identity resolves to the configured default. - GAP-007: deleted the architecturally-impossible CLI `@us1-write-idempotency` scenario (CLI has no package-fs access — Principle VI). - GAP-008/009: OTel in-memory exporter tests (`ExportMetricsTests`, per-test-scoped `MeterProvider`). **Compliance:** net481 guard remediation (FR-018/020 — removed non-compliant interface + DI-hiding `#if` guards); observability gate applied; doc-sync (`.agents/30-context`, operator-guide, configuration-reference). ## Reviewer notes / known divergences (tracked in `specs/038-close-dsl-gaps/discrepancies.md`) - **ATDD form**: scenarios are covered by **code-first DSL + unit + live SystemTests** (this repo's evolved convention), not new Reqnroll bindings. The stale identity feature file rewrite is the remaining cleanup. - **`ImportAsync` tool-param** retained as a deliberate **DI-cycle-avoidance** seam (tool→orchestrator dependency); full lifecycle consolidation is a focused follow-up. - Metric names are `platform.workitems.export.*` (the spec's `migration.*` names were illustrative) — D-007. - Stale "TFS is source-only" comments across pre-existing files spawned as a separate cleanup task — D-006. ## Validation - `dotnet build --no-incremental` — 0 errors (net10 + net481). - `dotnet test` — 1,411 passed / 2 skipped / 0 failed across 10 assemblies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved identity resolution with UPN and display-name matching plus connector-specific adapters. * New agent-context extension to refresh the managed Spec Kit section in agent context files. * **Improvements** * Identity translation seam with configurable fallbacks and clearer skip-and-warn behaviours for teams and nodes. * Nodes import now skips when replication is disabled. * Deterministic per-test OpenTelemetry metrics support. * **Documentation** * Expanded spec, plan, quickstart and operator guides for DSL migration gap closure. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent acc9627 commit 9cd8a1c

119 files changed

Lines changed: 5289 additions & 551 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/20-guardrails/workflow/testing-rules.md

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ MSTest conventions, test naming, and organisation. See also: [coding-standards.m
1010
| --- | --- | --- | --- | --- |
1111
| 1 (highest) | Unit Tests | `[TestCategory("UnitTests")]` | < 50 ms | All logic, branching, transforms. No I/O, no DI. Single class/method in isolation. |
1212
| 2 | Domain Tests (Internal DSL + MSTest) | `[TestCategory("DomainTests")]` | < 500 ms | Business behaviour across collaborating domain objects via the internal DSL. |
13-
| 3 | Simulated System Tests | `[TestCategory("SystemTests_Simulated")]` | < 10 s | End-to-end with `Simulated` connector. No network. |
14-
| 3a | Smoke System Tests | `[TestCategory("SystemTests_Smoke")]` | < 30 s | Critical-path subset of system tests run on every PR. |
15-
| 4 (lowest) | Live System Tests | `[TestCategory("SystemTests")]`/`[TestCategory("SystemTests_Live")]` | < 60 s | Requires live ADO/TFS. Environment-gated. |
13+
| 3 | Simulated System Tests | `[TestCategory("SystemTest_Simulated")]` | < 10 s | End-to-end with `Simulated` connector. No network. |
14+
| 3a | Smoke System Tests | `[TestCategory("SystemTest_Smoke")]` | < 30 s | Critical-path subset of system tests run on every PR. |
15+
| 4 (lowest) | Live System Tests | `[TestCategory("SystemTest")]`/`[TestCategory("SystemTest_Live")]` | < 60 s | Requires live ADO/TFS. Environment-gated. |
1616

1717
### Distinguishing UnitTests from DomainTests
1818

@@ -44,17 +44,17 @@ Every time a test file is **created, edited, moved, or touched in any way**, eve
4444
| --- | --- |
4545
| Test uses `DevOpsMigrationPlatform.Testing` DSL | `[TestCategory("DomainTests")]` |
4646
| Test is isolated unit test (no DSL, no I/O) | `[TestCategory("UnitTests")]` |
47-
| Test uses `Simulated` connector end-to-end | `[TestCategory("SystemTests_Simulated")]` |
48-
| Test is a critical-path smoke subset | `[TestCategory("SystemTests_Smoke")]` |
49-
| Test targets live ADO/TFS | `[TestCategory("SystemTests")]` or `[TestCategory("SystemTests_Live")]` |
47+
| Test uses `Simulated` connector end-to-end | `[TestCategory("SystemTest_Simulated")]` |
48+
| Test is a critical-path smoke subset | `[TestCategory("SystemTest_Smoke")]` |
49+
| Test targets live ADO/TFS | `[TestCategory("SystemTest")]` or `[TestCategory("SystemTest_Live")]` |
5050

5151
**Enforcement rules — all are blocking, none are optional:**
5252

5353
1. **Missing tag on touch:** If any `[TestMethod]` or `[TestClass]` in a touched file lacks `[TestCategory]`, add the correct tag in the same edit. This applies to every method in the file, not just the method being modified.
5454
2. **Wrong tag on touch:** If a tag is incorrect (wrong category, old name), correct it in the same edit.
5555
3. **Delegation does not exempt:** If a sub-agent or delegated run added or modified a test, the calling agent is responsible for verifying tags before closing the task. "The delegated run didn't add it" is not a valid completion state.
5656
4. **No partial compliance:** Applying the tag to the new method while leaving existing uncategorised methods in the same file is non-compliant. Fix the whole file.
57-
5. **Category names are canonical:** Only the exact strings `UnitTests`, `DomainTests`, `SystemTests_Simulated`, `SystemTests_Smoke`, `SystemTests`, `SystemTests_Live` are valid. Any other value is non-compliant and must be corrected on contact.
57+
5. **Category names are canonical:** Only the exact strings `UnitTests`, `DomainTests`, `SystemTest_Simulated`, `SystemTest_Smoke`, `SystemTest`, `SystemTest_Live` are valid. Any other value is non-compliant and must be corrected on contact.
5858
6. The `nkda-testdsl-*` skills must apply `[TestCategory("DomainTests")]` to all converted tests.
5959
7. The `nkda-testdsl-refactor` skill must verify and correct all category tags in any file it touches.
6060

@@ -66,11 +66,60 @@ Every time a test file is **created, edited, moved, or touched in any way**, eve
6666

6767
---
6868

69+
## Mandatory DSL Migration (Touch = Convert)
70+
71+
> **HARD GATE — no exceptions, no deferrals, no delegation excuses.**
72+
> Legacy Reqnroll is a **migration debt**, not an editable test style. The moment you need to
73+
> *change the behaviour* of a legacy `.feature` file or its `*Steps.cs`, that feature family
74+
> MUST be migrated to the internal DSL — you do not edit the old style in place.
75+
76+
**Trigger.** Any change to the *behaviour or scenarios* of a legacy Reqnroll `.feature` file, or
77+
to its `[Binding]`/`[Given]`/`[When]`/`[Then]` step definitions, obligates migration of that
78+
**whole feature family** to the code-first internal DSL before the task is complete. This sits
79+
alongside Touch = Tag: a touched legacy test must be both migrated **and** correctly categorised.
80+
81+
**How.** Migration is performed *only* via the DSL orchestration skill — do not hand-roll it:
82+
83+
```text
84+
nkda-testdsl-autonomous {feature}
85+
```
86+
87+
`{feature}` is the touched `.feature` path, step-file path, family folder, or named family. The
88+
skill runs the full loop (assess → DSL design → extraction → conversion → refactor → verification)
89+
and produces code-first `[TestCategory("DomainTests")]` tests under
90+
`tests/<Project>.Tests/<Area>/<Behaviour>Tests.cs` using the
91+
`tests/DevOpsMigrationPlatform.Testing` DSL library.
92+
93+
**Terminal state.** After migration the legacy `.feature` and `*Steps.cs` for that family are
94+
**removed** (their behaviour now lives in the DSL tests). A family is "migrated" only when no
95+
`.feature`/`[Binding]` artefacts remain for it and the converted tests pass.
96+
97+
**Narrow carve-outs (do NOT require migration):**
98+
99+
1. **Retirement***deleting* an obsolete or architecturally-impossible scenario/family outright
100+
(no replacement behaviour) is a removal, not a change-in-place; record the rationale.
101+
2. **Non-behavioural edits** — fixing a typo/comment with no scenario or step change.
102+
3. **Orphaned feature files** — a `.feature` with **no matching `[Binding]`/`Steps.cs`** that
103+
generates **no executable tests** is stale *documentation*, not a legacy test. It cannot be
104+
"migrated" (there is no behaviour to convert). Editing it does not trigger this gate; the
105+
correct end state is to **delete** the orphan once its intent is captured elsewhere (docs or
106+
DSL tests), not to keep dead Gherkin.
107+
108+
If you find yourself editing legacy scenario steps to assert new behaviour, **stop and run
109+
`nkda-testdsl-autonomous`** instead.
110+
111+
**Enforcement:** blocking. A task that edits legacy `.feature`/`Steps` behaviour without running
112+
the DSL migration is incomplete, regardless of whether tests pass.
113+
114+
---
115+
69116
## Framework
70117

71118
- Unit runner: MSTest only.
72119
- Code-first behavioural tests: use `tests/DevOpsMigrationPlatform.Testing` internal DSL.
73-
- Legacy BDD: Reqnroll remains only for feature families not yet migrated.
120+
- Legacy BDD: Reqnroll is a migration debt. It remains **only** for families not yet migrated, and
121+
**any behavioural touch triggers migration** (see *Touch = Convert* above) — it is not an
122+
editable style.
74123
- New feature behaviour must not be added as `.feature` files unless explicitly approved.
75124
- Do not add new `[Binding]`, `[Given]`, `[When]`, or `[Then]` classes for migrated areas.
76125

@@ -110,7 +159,7 @@ Legacy-only (`Reqnroll` / `[Binding]`) guidance:
110159
- `Mock<T>` (Moq) or hand-written fakes for infrastructure interfaces.
111160
- Never use real `FileSystemArtefactStore` in unit tests.
112161
- Never use live Azure DevOps in unit tests.
113-
- Real filesystem → `[TestCategory("SystemTests_Simulated")]`.
162+
- Real filesystem → `[TestCategory("SystemTest_Simulated")]`.
114163

115164
---
116165

@@ -193,7 +242,7 @@ The contributor-facing debugging workflow lives in [docs/testing-guide.md](../..
193242

194243
## CLI Feature → System Test Requirement
195244

196-
Every CLI command MUST have `[TestCategory("SystemTests")]` test that:
245+
Every CLI command MUST have `[TestCategory("SystemTest")]` test that:
197246

198247
1. Guards on env vars (calls `Assert.Fail` with a clear message if absent — see Assert.Inconclusive Is Banned above).
199248
2. Exercises the feature against real/simulated system.

.agents/30-context/domains/connector-model.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,17 @@ Target connector coverage evolves by capability; do not treat current TFS target
2222

2323
## Client Integration Boundary
2424

25-
Azure DevOps connectors obtain clients from `IAzureDevOpsClientFactory`. This is the only permitted way to create API clients. No direct `VssConnection` instantiation.
25+
Azure DevOps connectors obtain clients from `IAzureDevOpsClientFactory`. This is the only permitted way to create API clients. No direct `VssConnection` instantiation. (The identity adapter uses `IAzureDevOpsClientFactory.CreateIdentityClientAsync``IdentityHttpClient`.)
26+
27+
## Identity Adapter (`IIdentityAdapter`)
28+
29+
`IIdentityAdapter` is the connector abstraction for querying the **target** tenant by UPN/display name during the Prepare phase (distinct from `IIdentitySource`, which enumerates the source at export time). Each connector lives at its own project boundary — **no `#if` guards**:
30+
31+
- `SimulatedIdentityAdapter` (Infrastructure.Simulated, net10) — deterministic in-memory target.
32+
- `AzureDevOpsIdentityAdapter` (Infrastructure.AzureDevOps, net10) — `IdentityHttpClient.ReadIdentitiesAsync`.
33+
- `TfsIdentityAdapter` (Infrastructure.TfsObjectModel, net481) — reduced capability: returns empty + structured Warning (the TFS Identity Service does not expose UPN/display-name search). Modeled explicitly in the contract result, not a stub.
34+
35+
Registered via `AddIdentityAdapter<T>("<connectorType>")` and dispatched by `CompositeIdentityAdapter` on `ITargetEndpointInfo.ConnectorType`.
2636

2737
## Key Rules
2838

.agents/30-context/domains/identity-and-mapping.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,22 @@ During `PrepareAsync`, the `IdentitiesModule`:
6060

6161
### "Identity is a Cross-Cutting Service" Rule
6262

63-
No module should implement its own identity resolution. All modules consume the `IIdentityMappingService` injected at construction. The `IdentitiesModule` must complete export before any other module begins import.
63+
No module should implement its own identity resolution. All modules consume the cross-cutting identity seam injected at construction. The `IdentitiesModule` must complete export before any other module begins import.
64+
65+
### Identity Resolution Architecture (spec 038)
66+
67+
The resolution pipeline is composed of four collaborating abstractions:
68+
69+
| Abstraction | Role | Lives in |
70+
|---|---|---|
71+
| `IIdentityTranslationTool` | **Tool seam** consumed by all modules (`TeamImportOrchestrator`, `RevisionFolderProcessor`, `WorkItemsModule`, `IdentitiesModule`). `Translate(source)` is **synchronous** and reads cached Prepare-phase results only. Exposes `IsEnabled` and `DefaultIdentity`. Configured under `MigrationPlatform:Tools:IdentityTranslation`. (Renamed from the former `IIdentityLookupTool`; `Resolve()``Translate()`.) | `Abstractions.Agent/Tools` |
72+
| `IIdentitiesOrchestrator` | Owns the phase lifecycle (`PrepareAsync`/`ExportAsync`/`ImportAsync`/`ValidateAsync`). `PrepareAsync` performs the live matching, caches results (`source → target descriptor`), and writes `prepare-report.json`. `ResolvePrepared(source)` is the synchronous cache read the tool delegates to. | `Abstractions.Agent/Modules` |
73+
| `IIdentityAdapter` | **Connector-specific** query of the live **target** tenant (`FindByUpnAsync`, `FindByDisplayNameAsync`). Called by `PrepareAsync` only. Implementations: `SimulatedIdentityAdapter`, `AzureDevOpsIdentityAdapter` (SDK `IdentityHttpClient`), `TfsIdentityAdapter` (reduced capability — empty + Warning). Dispatched by `CompositeIdentityAdapter` on the target connector type. | `Abstractions.Agent/Identity` (+ connector projects) |
74+
| `IIdentityMatchingStrategy` | **Pure** ordered match step. `UpnIdentityMatchingStrategy` (step 2) then `DisplayNameIdentityMatchingStrategy` (step 3, Unicode-NFC, case-insensitive). Returns `IdentityMatch(Descriptor, MatchCount)`; ambiguity (`MatchCount > 1`) is logged by the orchestrator. | `Abstractions.Agent/Identity` |
75+
76+
**Four-step order** (executed in PrepareAsync for steps 2–3; in the tool for 1 & 4): (1) explicit `mapping.json` override → (2) UPN match → (3) display-name match → (4) configured `DefaultIdentity`, else source pass-through. Ambiguous matches and adapter failures are **not** treated as resolved: PrepareAsync logs a structured warning, records them as **unresolved** in `prepare-report.json`, and they remain subject to the import-blocking rule above (line 59). The step-4 default/pass-through applies only at translation time for identities the operator has not subsequently mapped.
77+
78+
`IIdentityMappingService` remains the override/default resolver the orchestrator consults internally; modules do not call it directly — they go through `IIdentityTranslationTool`.
6479

6580

6681

.agents/agents.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,5 @@ If anything conflicts, guardrails win.
4444

4545
<!-- SPECKIT START -->
4646
For additional context about technologies to be used, project structure,
47-
shell commands, and other important information, read:
48-
`specs/037-workitem-resolution-arch/plan.md`
47+
shell commands, and other important information, read the current plan
4948
<!-- SPECKIT END -->
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
description: Refresh the managed Spec Kit section in the coding agent context file
3+
---
4+
5+
6+
<!-- Extension: agent-context -->
7+
<!-- Config: .specify/extensions/agent-context/ -->
8+
# Update Coding Agent Context
9+
10+
Refresh the managed Spec Kit section inside the active coding agent's context/instruction file (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`).
11+
12+
## Behavior
13+
14+
The script reads the agent-context extension config at
15+
`.specify/extensions/agent-context/agent-context-config.yml` to discover:
16+
17+
- `context_file` — the path of the coding agent context file to manage.
18+
- `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing.
19+
20+
It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/<feature>/plan.md`).
21+
22+
If `context_file` is empty or the file cannot be located, the command reports nothing to do and exits successfully.
23+
24+
## Execution
25+
26+
- **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]`
27+
- **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]`
28+
29+
When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
description: Refresh the managed Spec Kit section in the coding agent context file
3+
---
4+
5+
6+
<!-- Extension: agent-context -->
7+
<!-- Config: .specify/extensions/agent-context/ -->
8+
# Update Coding Agent Context
9+
10+
Refresh the managed Spec Kit section inside the active coding agent's context/instruction file (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`).
11+
12+
## Behavior
13+
14+
The script reads the agent-context extension config at
15+
`.specify/extensions/agent-context/agent-context-config.yml` to discover:
16+
17+
- `context_file` — the path of the coding agent context file to manage.
18+
- `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing.
19+
20+
It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/<feature>/plan.md`).
21+
22+
If `context_file` is empty or the file cannot be located, the command reports nothing to do and exits successfully.
23+
24+
## Execution
25+
26+
- **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]`
27+
- **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]`
28+
29+
When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`.

.agents/configure.ps1

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@ param()
2323
$ErrorActionPreference = 'Stop'
2424

2525
$agentsDir = $PSScriptRoot
26-
$repoRoot = Split-Path $agentsDir -Parent
26+
$repoRoot = Split-Path $agentsDir -Parent
2727

2828
# ---------------------------------------------------------------------------
2929
# Helpers
3030
# ---------------------------------------------------------------------------
3131

3232
function Write-Status {
3333
param(
34-
[ValidateSet('OK','CREATED','WARN','ERROR')]
34+
[ValidateSet('OK', 'CREATED', 'WARN', 'ERROR')]
3535
[string]$Status,
3636
[string]$Message
3737
)
38-
$colors = @{ OK='Green'; CREATED='Cyan'; WARN='Yellow'; ERROR='Red' }
38+
$colors = @{ OK = 'Green'; CREATED = 'Cyan'; WARN = 'Yellow'; ERROR = 'Red' }
3939
Write-Host (" [{0,-7}] {1}" -f $Status, $Message) -ForegroundColor $colors[$Status]
4040
}
4141

@@ -51,7 +51,7 @@ function Ensure-HardLink {
5151
[string]$TargetPath
5252
)
5353

54-
$link = Join-Path $repoRoot $LinkPath
54+
$link = Join-Path $repoRoot $LinkPath
5555
$target = Join-Path $repoRoot $TargetPath
5656

5757
if (-not (Test-Path $target)) {
@@ -62,14 +62,15 @@ function Ensure-HardLink {
6262
if (Test-Path $link) {
6363
$item = Get-Item -LiteralPath $link -Force
6464
if ($item.LinkType -eq 'HardLink') {
65-
$linkHash = (Get-FileHash $link -Algorithm SHA256).Hash
65+
$linkHash = (Get-FileHash $link -Algorithm SHA256).Hash
6666
$targetHash = (Get-FileHash $target -Algorithm SHA256).Hash
6767
if ($linkHash -eq $targetHash) {
6868
Write-Status OK "$LinkPath (hardlink, content matches)"
6969
return
7070
}
7171
Write-Status WARN "$LinkPath is a hardlink but content differs — recreating"
72-
} else {
72+
}
73+
else {
7374
Write-Status WARN "$LinkPath exists as $($item.LinkType ?? 'file') — recreating as hardlink"
7475
}
7576
if ($PSCmdlet.ShouldProcess($link, 'Remove')) { Remove-Item -LiteralPath $link -Force }
@@ -95,8 +96,8 @@ function Ensure-Symlink {
9596
[string]$TargetRelative
9697
)
9798

98-
$link = Join-Path $repoRoot $LinkPath
99-
$linkParent = Split-Path $link -Parent
99+
$link = Join-Path $repoRoot $LinkPath
100+
$linkParent = Split-Path $link -Parent
100101
$targetAbsolute = [System.IO.Path]::GetFullPath((Join-Path $linkParent $TargetRelative))
101102

102103
if (-not (Test-Path $targetAbsolute)) {
@@ -113,7 +114,8 @@ function Ensure-Symlink {
113114
return
114115
}
115116
Write-Status WARN "$LinkPath symlink points elsewhere — recreating"
116-
} else {
117+
}
118+
else {
117119
Write-Status WARN "$LinkPath exists as directory/file — replacing with symlink"
118120
}
119121
if ($PSCmdlet.ShouldProcess($link, 'Remove')) { Remove-Item -LiteralPath $link -Recurse -Force }
@@ -130,8 +132,8 @@ function Ensure-Symlink {
130132
# ---------------------------------------------------------------------------
131133

132134
# Check symlink privileges (required for all symlink operations)
133-
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
134-
$devMode = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' -ErrorAction SilentlyContinue).AllowDevelopmentWithoutDevLicense -eq 1
135+
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
136+
$devMode = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' -ErrorAction SilentlyContinue).AllowDevelopmentWithoutDevLicense -eq 1
135137
$canSymlink = $isAdmin -or $devMode
136138

137139
if (-not $canSymlink -and -not $WhatIfPreference) {
@@ -159,6 +161,7 @@ Write-Host ".claude/ symlinks:" -ForegroundColor White
159161
Ensure-Symlink '.claude\skills' '..\.agents\skills'
160162
Ensure-Symlink '.claude\agents' '..\.agents\agents'
161163
Ensure-Symlink '.claude\commands' '..\.agents\commands'
164+
Ensure-Symlink '.claude\prompts' '..\.agents\prompts'
162165

163166
# -- .github symlinks --------------------------------------------------------
164167
Write-Host ""
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
agent: speckit.agent-context.update
3+
---

0 commit comments

Comments
 (0)