Skip to content

Commit fdafafa

Browse files
feat(instructions)\!: migrate disclaimer SSOT to disclaimer-language.instructions.md
BREAKING CHANGE: .github/config/disclaimers.yml removed; consumers must reference disclaimer-language.instructions.md.
1 parent 2bdcb8d commit fdafafa

7 files changed

Lines changed: 310 additions & 198 deletions

File tree

.github/config/disclaimers.yml

Lines changed: 0 additions & 52 deletions
This file was deleted.

.github/instructions/security/backlog-handoff.instructions.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Generate formatted work items from security model mitigations and standards gaps
1818

1919
## ADO Work Item Template
2020

21+
> **Note:** This work item was generated by the Security Planner AI agent. Review for accuracy and adjust before assigning to a person.
22+
23+
- [ ] Reviewed by a human security professional before execution
24+
2125
Assign sequential IDs within the security plan using the format `WI-SEC-{NNN}` (for example, WI-SEC-001, WI-SEC-002). Order work items by type hierarchy: Epic, Feature, User Story, Task, Bug.
2226

2327
Required fields per work item:
@@ -60,6 +64,10 @@ Five MCP tool categories support ADO operations: work item creation, work item u
6064

6165
## GitHub Issue Template
6266

67+
> **Note:** This work item was generated by the Security Planner AI agent. Review for accuracy and adjust before assigning to a person.
68+
69+
- [ ] Reviewed by a human security professional before execution
70+
6371
Assign temporary IDs using the format `{{SEC-TEMP-N}}`, replaced with real issue numbers on creation. Order operations by type: create, update, comment, label, close.
6472

6573
Required fields per issue:

.github/instructions/security/identity.instructions.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ Core responsibilities:
1616

1717
Voice: clear, methodical, and security-focused. Communicate with professional authority while keeping guidance accessible and actionable.
1818

19+
## Disclaimer and Attribution Protocol
20+
21+
### Session Start Display
22+
23+
On the first turn of any Security Planner session, display the canonical Security Planning disclaimer block defined in [.github/instructions/shared/disclaimer-language.instructions.md](../shared/disclaimer-language.instructions.md) verbatim. Record the display by setting `state.disclaimerShownAt` to an ISO 8601 timestamp. Do not advance to any phase work before the disclaimer is shown for the session.
24+
25+
### Exit Point Reminder
26+
27+
At each of the following exit points, re-surface a brief one-line professional-review reminder. Use the canonical wording in [.github/instructions/shared/disclaimer-language.instructions.md](../shared/disclaimer-language.instructions.md) (Security Planning section) for the reminder text.
28+
29+
1. **Phase 6 completion (handoff success path)** — Display the reminder immediately before presenting the final handoff summary.
30+
2. **Compact handoff** — Display the reminder when the orchestrator hands off to ADO or GitHub backlog workflows.
31+
3. **Error exit** — Display the reminder on any unrecoverable error path before terminating the session.
32+
4. **User-initiated exit** — Display the reminder when the user explicitly stops the session or switches agents.
33+
34+
Each reminder must state that the generated plan is AI-assisted and requires professional security review before execution.
35+
1936
## Six-Phase Definitions
2037

2138
Each phase has entry criteria, activities, exit criteria, artifacts produced, and a defined transition.

.github/instructions/shared/disclaimer-language.instructions.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ applyTo: '**/.copilot-tracking/rai-plans/**, **/.copilot-tracking/security-plans
77

88
Planning agents that generate assessments requiring professional review display a CAUTION block during startup. Each section contains the verbatim disclaimer for the corresponding planner. Prompt files and agents reference the appropriate section via `#file:` to ensure consistent presentation across all entry points.
99

10+
<!--
11+
Authoring contract (parsed by scripts/linting/Validate-PlannerArtifacts.ps1):
12+
13+
- Each planner gets exactly one H2 section. The first whitespace-delimited word of the heading, lowercased, is the slug. Examples: "RAI Planning" -> "rai"; "Security Planning" -> "security"; "SSSC Planning" -> "sssc".
14+
- The slug derives three downstream identifiers used by ai-artifact footer validation:
15+
- planner key: `{slug}-planner`
16+
- disclaimer id: `{slug}-full-disclaimer`
17+
- disclaimer label: `{heading} Disclaimer` (full heading, not slug)
18+
- Each H2 section must contain exactly one `> [!CAUTION]` blockquote. Only the first CAUTION block in a section is extracted; additional CAUTION blocks within the same H2 are ignored.
19+
- The CAUTION block's prose should begin with `**Disclaimer:**` (the trailing colon is optional). This prefix is stripped before the text is matched against artifact footers; prose without it is retained verbatim.
20+
- Multi-line blockquote prose is joined with single spaces. Keep wrapping natural for source readability.
21+
-->
22+
23+
1024
## RAI Planning
1125

1226
> [!CAUTION]

scripts/linting/Validate-PlannerArtifacts.ps1

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
Validates AI artifact footer and disclaimer presence in instruction templates.
1010
1111
.DESCRIPTION
12-
Reads footer-with-review.yml and disclaimers.yml config files as the single source
13-
of truth, then scans instruction files for required footer text based on artifact
12+
Reads footer-with-review.yml for footer text and artifact-classification rules, and
13+
parses shared/disclaimer-language.instructions.md as the canonical disclaimer source.
14+
Scans instruction files for required footer and disclaimer text based on artifact
1415
classification rules. Outputs results as JSON and sets CI environment variables on
1516
failure.
1617
@@ -23,8 +24,9 @@
2324
.PARAMETER FooterConfigPath
2425
Path to the footer-with-review.yml config file.
2526
26-
.PARAMETER DisclaimerConfigPath
27-
Path to the disclaimers.yml config file.
27+
.PARAMETER DisclaimerSourcePath
28+
Path to the shared disclaimer-language instructions markdown file. The validator
29+
parses H2 sections and their CAUTION blockquote bodies to derive disclaimer text.
2830
2931
.PARAMETER FailOnMissing
3032
When specified, treats missing footers and disclaimers as validation failures.
@@ -51,7 +53,7 @@ param(
5153
[string]$FooterConfigPath = '.github/config/footer-with-review.yml',
5254

5355
[Parameter(Mandatory = $false)]
54-
[string]$DisclaimerConfigPath = '.github/config/disclaimers.yml',
56+
[string]$DisclaimerSourcePath = '.github/instructions/shared/disclaimer-language.instructions.md',
5557

5658
[Parameter(Mandatory = $false)]
5759
[switch]$FailOnMissing,
@@ -107,40 +109,78 @@ function Import-FooterConfig {
107109
return $config
108110
}
109111

110-
function Import-DisclaimerConfig {
112+
function Import-DisclaimerSource {
111113
<#
112114
.SYNOPSIS
113-
Loads and validates disclaimers.yml.
115+
Parses the shared disclaimer-language instructions markdown as the canonical disclaimer source.
114116
115-
.PARAMETER ConfigPath
116-
Absolute path to the disclaimer config YAML file.
117+
.DESCRIPTION
118+
Reads the markdown file, splits on H2 headings to identify planner sections,
119+
and extracts the verbatim disclaimer prose from each section's CAUTION blockquote.
120+
The first word of each heading (lowercased) maps to the planner key and disclaimer id
121+
convention: 'RAI Planning' -> 'rai-planner' / 'rai-full-disclaimer'.
122+
123+
.PARAMETER SourcePath
124+
Absolute path to the disclaimer-language.instructions.md markdown file.
117125
118126
.OUTPUTS
119-
[hashtable] Parsed disclaimer config.
127+
[hashtable] Parsed config shaped as @{ version; source; disclaimers = @{ key = @{ id; label; text } } }.
120128
#>
121129
[CmdletBinding()]
122130
[OutputType([hashtable])]
123131
param(
124132
[Parameter(Mandatory = $true)]
125133
[ValidateNotNullOrEmpty()]
126-
[string]$ConfigPath
134+
[string]$SourcePath
127135
)
128136

129-
if (-not (Test-Path $ConfigPath)) {
130-
throw "Disclaimer config not found: $ConfigPath"
137+
if (-not (Test-Path $SourcePath)) {
138+
throw "Disclaimer source not found: $SourcePath"
131139
}
132140

133-
$content = Get-Content -Path $ConfigPath -Raw -Encoding utf8
134-
$config = ConvertFrom-Yaml -Yaml $content
141+
$raw = Get-Content -Path $SourcePath -Raw -Encoding utf8
142+
# Strip YAML frontmatter
143+
$body = $raw -replace '(?s)\A---.*?\r?\n---\s*\r?\n', ''
135144

136-
if (-not $config.version) {
137-
throw "Disclaimer config missing 'version' field: $ConfigPath"
145+
$disclaimers = @{}
146+
$sectionRegex = [regex]'(?ms)^##[ \t]+(?<heading>[^\r\n]+?)[ \t]*\r?\n(?<body>.*?)(?=^##[ \t]|\z)'
147+
$cautionRegex = [regex]'(?m)^>[ \t]*\[!CAUTION\][ \t]*\r?\n(?<block>(?:^>.*\r?\n?)+)'
148+
$prefixRegex = [regex]'^\*\*Disclaimer:?\*\*[\s:\-\u2014]*'
149+
150+
foreach ($match in $sectionRegex.Matches($body)) {
151+
$heading = $match.Groups['heading'].Value.Trim()
152+
$sectionBody = $match.Groups['body'].Value
153+
154+
$cautionMatch = $cautionRegex.Match($sectionBody)
155+
if (-not $cautionMatch.Success) { continue }
156+
157+
$blockLines = $cautionMatch.Groups['block'].Value -split '\r?\n'
158+
$proseParts = foreach ($line in $blockLines) {
159+
$stripped = $line -replace '^>[ \t]?', ''
160+
if ($stripped.Trim().Length -gt 0) { $stripped.Trim() }
161+
}
162+
$prose = ($proseParts -join ' ').Trim()
163+
$prose = $prefixRegex.Replace($prose, '', 1)
164+
if ([string]::IsNullOrWhiteSpace($prose)) { continue }
165+
166+
$slug = ($heading -split '\s+' | Select-Object -First 1).ToLowerInvariant()
167+
$key = "$slug-planner"
168+
$disclaimers[$key] = @{
169+
id = "$slug-full-disclaimer"
170+
label = "$heading Disclaimer"
171+
text = $prose
172+
}
138173
}
139-
if (-not $config.disclaimers) {
140-
throw "Disclaimer config missing 'disclaimers' section: $ConfigPath"
174+
175+
if ($disclaimers.Count -eq 0) {
176+
throw "No disclaimer sections found in source: $SourcePath"
141177
}
142178

143-
return $config
179+
return @{
180+
version = 'markdown-source'
181+
source = $SourcePath
182+
disclaimers = $disclaimers
183+
}
144184
}
145185

146186
function Get-FooterSearchText {
@@ -318,7 +358,7 @@ function Test-AIArtifactCompliance {
318358
Parsed footer-with-review.yml config.
319359
320360
.PARAMETER DisclaimerConfig
321-
Parsed disclaimers.yml config.
361+
Parsed disclaimer source (see Import-DisclaimerSource).
322362
323363
.PARAMETER RepoRoot
324364
Repository root for relative path display.
@@ -422,8 +462,8 @@ function Test-AIArtifactValidation {
422462
.PARAMETER FooterConfigPath
423463
Path to footer-with-review.yml relative to repo root.
424464
425-
.PARAMETER DisclaimerConfigPath
426-
Path to disclaimers.yml relative to repo root.
465+
.PARAMETER DisclaimerSourcePath
466+
Path to the shared disclaimer-language instructions markdown file, relative to repo root.
427467
428468
.PARAMETER FailOnMissing
429469
When set, missing footers cause a non-zero exit code.
@@ -447,7 +487,7 @@ function Test-AIArtifactValidation {
447487
[string]$FooterConfigPath,
448488

449489
[Parameter(Mandatory = $true)]
450-
[string]$DisclaimerConfigPath,
490+
[string]$DisclaimerSourcePath,
451491

452492
[Parameter(Mandatory = $false)]
453493
[switch]$FailOnMissing,
@@ -464,7 +504,7 @@ function Test-AIArtifactValidation {
464504

465505
# Load configs
466506
$footerConfig = Import-FooterConfig -ConfigPath (Join-Path $repoRoot $FooterConfigPath)
467-
$disclaimerConfig = Import-DisclaimerConfig -ConfigPath (Join-Path $repoRoot $DisclaimerConfigPath)
507+
$disclaimerConfig = Import-DisclaimerSource -SourcePath (Join-Path $repoRoot $DisclaimerSourcePath)
468508

469509
# Collect instruction files
470510
$allFiles = @()
@@ -616,7 +656,7 @@ if ($MyInvocation.InvocationName -ne '.') {
616656
-Paths $Paths `
617657
-ExcludePaths $ExcludePaths `
618658
-FooterConfigPath $FooterConfigPath `
619-
-DisclaimerConfigPath $DisclaimerConfigPath `
659+
-DisclaimerSourcePath $DisclaimerSourcePath `
620660
-FailOnMissing:$FailOnMissing `
621661
-OutputPath $OutputPath
622662

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#Requires -Modules Pester
2+
# Copyright (c) Microsoft Corporation.
3+
# SPDX-License-Identifier: MIT
4+
<#
5+
.SYNOPSIS
6+
Asserts disclaimer Note + reviewer checkbox patterns are present in both backlog
7+
instruction files, and the `### Exit Point Reminder` heading with all four named
8+
exit points exists in both planner identity files.
9+
#>
10+
11+
BeforeDiscovery {
12+
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path
13+
14+
$script:noteLiteral = '> **Note:** This work item was generated by the Security Planner AI agent. Review for accuracy and adjust before assigning to a person.'
15+
$script:checkboxLiteral = '- [ ] Reviewed by a human security professional before execution'
16+
17+
$script:backlogFiles = @(
18+
(Join-Path $script:repoRoot '.github/instructions/security/backlog-handoff.instructions.md'),
19+
(Join-Path $script:repoRoot '.github/instructions/security/sssc-backlog.instructions.md')
20+
)
21+
$script:identityFiles = @(
22+
(Join-Path $script:repoRoot '.github/instructions/security/identity.instructions.md'),
23+
(Join-Path $script:repoRoot '.github/instructions/security/sssc-identity.instructions.md')
24+
)
25+
26+
$script:exitPoints = @(
27+
'Phase 6 completion',
28+
'Compact handoff',
29+
'Error exit',
30+
'User-initiated exit'
31+
)
32+
}
33+
34+
Describe 'Backlog instruction files include disclaimer Note and reviewer checkbox in every WI template variant' {
35+
It 'Backlog file <_> contains the Note literal at least twice' -ForEach $script:backlogFiles {
36+
Test-Path $_ | Should -BeTrue
37+
$content = Get-Content -Path $_ -Raw
38+
$count = ([regex]::Matches($content, [regex]::Escape($script:noteLiteral))).Count
39+
$count | Should -BeGreaterOrEqual 2 -Because "Note literal must appear in both ADO and GitHub WI variants in $_"
40+
}
41+
42+
It 'Backlog file <_> contains the reviewer checkbox literal at least twice' -ForEach $script:backlogFiles {
43+
$content = Get-Content -Path $_ -Raw
44+
$count = ([regex]::Matches($content, [regex]::Escape($script:checkboxLiteral))).Count
45+
$count | Should -BeGreaterOrEqual 2 -Because "reviewer checkbox must appear in both ADO and GitHub WI variants in $_"
46+
}
47+
}
48+
49+
Describe 'Identity files include Exit Point Reminder section with four named exit points' {
50+
It 'Identity file <_> contains ### Exit Point Reminder heading' -ForEach $script:identityFiles {
51+
Test-Path $_ | Should -BeTrue
52+
$content = Get-Content -Path $_ -Raw
53+
$content | Should -Match '(?m)^###\s+Exit Point Reminder\s*$'
54+
}
55+
56+
It 'Identity file <_> mentions all four exit-point labels' -ForEach $script:identityFiles {
57+
$content = Get-Content -Path $_ -Raw
58+
foreach ($label in $script:exitPoints) {
59+
$content | Should -BeLike "*$label*" -Because "exit point '$label' must be listed in $_"
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)