aid-installer 1.1.0 → 2.0.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/bin/aid.ps1 CHANGED
@@ -13,7 +13,7 @@
13
13
  # aid version Print the CLI version
14
14
  # aid status Show AID state of the current project
15
15
  # aid add <tool>[,...] Add tool(s) to the current project
16
- # aid update [<tool>... | self] Update to latest; no arg = all tools; 'self' = the aid CLI
16
+ # aid update [self] Update to latest; inside repo = CLI + all tools; 'self' = CLI only
17
17
  # aid remove [<tool>... | self] Remove; no arg = ALL AID from project; 'self' = the aid CLI
18
18
  # aid <command> -h | --help Per-command help
19
19
  #
@@ -29,6 +29,11 @@
29
29
  # Bootstrap URL - single place to update when the branch merges to master.
30
30
  # Override with $env:AID_INSTALL_URL for tests.
31
31
  # ---------------------------------------------------------------------------
32
+
33
+ # Enable TLS 1.2 for HTTPS. Windows PowerShell 5.1 (.NET Framework) can default to
34
+ # SSL3/TLS1.0, which GitHub/npm/pypi reject -> downloads fail. Harmless on PS7/.NET Core.
35
+ try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {}
36
+
32
37
  $script:_AidInstallUrl = if ($env:AID_INSTALL_URL) { $env:AID_INSTALL_URL } else {
33
38
  'https://raw.githubusercontent.com/AndreVianna/aid-methodology/master/install.ps1'
34
39
  }
@@ -191,15 +196,19 @@ function script:Show-AidUsage {
191
196
  Write-Host ' -DryRun: print the exact command(s) it would run, then exit (no changes).'
192
197
  }
193
198
  'update' {
194
- Write-Host 'aid update [<tool>...] [-Version <v>] [-FromBundle <path>] [-Force] [-Target <dir>]'
199
+ Write-Host 'aid update [-Version <v>] [-FromBundle <path>] [-Force] [-DryRun] [-Target <dir>]'
195
200
  Write-Host 'aid update self [-FromBundle <path>] [-DryRun]'
196
- Write-Host ' Update to latest. No args: update all installed tools.'
201
+ Write-Host ' Update to latest.'
202
+ Write-Host ' Outside an AID repo: updates the CLI only (no-op if already latest).'
203
+ Write-Host ' Inside an AID repo: updates the CLI first, then ALL installed tools to one version.'
204
+ Write-Host ' No per-tool selection -- any tool positional is an error (use "self" only).'
197
205
  Write-Host ' self: COMPLETELY update the aid CLI, channel-aware:'
198
206
  Write-Host ' npm -> npm i -g | pypi -> pipx upgrade | curl -> re-bootstrap install.ps1.'
199
207
  Write-Host ' On Windows, elevation is the caller''s responsibility (no sudo).'
200
- Write-Host ' -FromBundle <path>: install the CLI from a local artifact instead of @latest'
208
+ Write-Host ' -Version <v>: pin ALL tools (and CLI) to version v.'
209
+ Write-Host ' -FromBundle <path>: install from a local artifact instead of @latest'
201
210
  Write-Host ' (npm .tgz | pypi .whl | curl release-staging dir with install.ps1).'
202
- Write-Host ' -DryRun: print the exact command(s) it would run, then exit (no changes).'
211
+ Write-Host ' -DryRun: print the full plan (tools updated, files copied, paths pruned) and exit.'
203
212
  }
204
213
  'version' {
205
214
  Write-Host 'aid version'
@@ -241,7 +250,7 @@ function script:Show-AidUsage {
241
250
  Write-Host ' aid version Print the CLI version'
242
251
  Write-Host ' aid status Show AID state of the current project'
243
252
  Write-Host ' aid add <tool>[,...] Add tool(s) to the current project'
244
- Write-Host ' aid update [<tool>... | self] Update to latest; no arg = all tools'
253
+ Write-Host ' aid update [self] Update to latest; inside repo = all tools'
245
254
  Write-Host ' aid remove [<tool>... | self] Remove; no arg = ALL AID from project'
246
255
  Write-Host ' aid dashboard start|stop ... Start/stop the local dashboard'
247
256
  Write-Host ' aid projects [list|add|remove] List/register/unregister AID projects'
@@ -1448,7 +1457,7 @@ function script:Get-AidProjectState {
1448
1457
  # Mirror of bash _aid_project_tools.
1449
1458
  function script:Get-AidProjectTools {
1450
1459
  param([string]$Path)
1451
- $manifest = Join-Path $Path '.aid' '.aid-manifest.json'
1460
+ $manifest = Join-Path (Join-Path $Path '.aid') '.aid-manifest.json'
1452
1461
  if (-not (Test-Path $manifest -PathType Leaf)) { return '' }
1453
1462
  $lines = Get-Content -LiteralPath $manifest -Encoding utf8 -ErrorAction SilentlyContinue
1454
1463
  if (-not $lines) { return '' }
@@ -1482,7 +1491,7 @@ function script:Get-AidProjectTools {
1482
1491
  function script:Get-WhichTierHolds {
1483
1492
  param([string]$Path)
1484
1493
  $primaryReg = Join-Path $script:_AidStateHome 'registry.yml'
1485
- $userReg = Join-Path $HOME '.aid' 'registry.yml'
1494
+ $userReg = Join-Path (Join-Path $HOME '.aid') 'registry.yml'
1486
1495
 
1487
1496
  $primaryNorm = [System.IO.Path]::GetFullPath($script:_AidStateHome)
1488
1497
  $userNorm = [System.IO.Path]::GetFullPath((Join-Path $HOME '.aid'))
@@ -1539,7 +1548,7 @@ function script:Invoke-AidProjectsList {
1539
1548
  ([System.IO.Path]::GetFullPath($script:_AidStateHome) -ne [System.IO.Path]::GetFullPath((Join-Path $HOME '.aid')))) {
1540
1549
  Join-Path $script:_AidStateHome 'registry.yml'
1541
1550
  } else {
1542
- Join-Path $HOME '.aid' 'registry.yml'
1551
+ Join-Path (Join-Path $HOME '.aid') 'registry.yml'
1543
1552
  }
1544
1553
  Write-Host (' registry: {0}' -f $regSrc)
1545
1554
  }
@@ -1599,7 +1608,7 @@ function script:Invoke-AidProjectsAdd {
1599
1608
  $regFile = if ($tier -eq 'shared' -and $primaryNorm -ne $userNorm) {
1600
1609
  Join-Path $script:_AidStateHome 'registry.yml'
1601
1610
  } else {
1602
- Join-Path $HOME '.aid' 'registry.yml'
1611
+ Join-Path (Join-Path $HOME '.aid') 'registry.yml'
1603
1612
  }
1604
1613
  Write-Host ("aid projects: registry file: $regFile")
1605
1614
  }
@@ -1624,7 +1633,7 @@ function script:Invoke-AidProjectsRemove {
1624
1633
 
1625
1634
  # Check if registered before unregistering (for idempotency message).
1626
1635
  $primaryReg = Join-Path $script:_AidStateHome 'registry.yml'
1627
- $userReg = Join-Path $HOME '.aid' 'registry.yml'
1636
+ $userReg = Join-Path (Join-Path $HOME '.aid') 'registry.yml'
1628
1637
  $primaryNorm = [System.IO.Path]::GetFullPath($script:_AidStateHome)
1629
1638
  $userNorm = [System.IO.Path]::GetFullPath((Join-Path $HOME '.aid'))
1630
1639
  $found = $false
@@ -1791,7 +1800,7 @@ function script:Registry-Register {
1791
1800
  $lns.Add("schema: 1")
1792
1801
  $lns.Add("projects:")
1793
1802
  foreach ($p in ($repos | Where-Object { $_ } | Sort-Object -Unique)) { $lns.Add(" - $p") }
1794
- Set-Content -LiteralPath $tmp -Value $lns.ToArray() -Encoding utf8NoBOM -ErrorAction Stop
1803
+ [System.IO.File]::WriteAllText($tmp, (($lns.ToArray()) -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
1795
1804
  Move-Item -LiteralPath $tmp -Destination $regPath -Force -ErrorAction Stop
1796
1805
  return $true
1797
1806
  } catch {
@@ -1904,7 +1913,7 @@ function script:Registry-Unregister {
1904
1913
  $lns.Add("schema: 1")
1905
1914
  $lns.Add("projects:")
1906
1915
  if ($remaining) { foreach ($p in $remaining) { $lns.Add(" - $p") } }
1907
- Set-Content -LiteralPath $tmp -Value $lns.ToArray() -Encoding utf8NoBOM -ErrorAction Stop
1916
+ [System.IO.File]::WriteAllText($tmp, (($lns.ToArray()) -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
1908
1917
  Move-Item -LiteralPath $tmp -Destination $regPath -Force -ErrorAction Stop
1909
1918
  return $true
1910
1919
  } catch {
@@ -2006,7 +2015,7 @@ if ($script:_RawArgs.Count -eq 0) {
2006
2015
  if (Test-Path $verFile -PathType Leaf) {
2007
2016
  $cliVersion = (Get-Content -LiteralPath $verFile -Raw).Trim()
2008
2017
  }
2009
- Write-Host "AID v$cliVersion - Agentic Iterative Development"
2018
+ Write-Host "AID v$cliVersion - AI Integrated Development"
2010
2019
  Write-Host "Install, update, and manage AID across your projects."
2011
2020
 
2012
2021
  # C6': format gate for cwd repo (.aid/ is guaranteed present here -- the
@@ -2136,10 +2145,15 @@ function script:Invoke-AidMigrateRepo {
2136
2145
  $era = ''
2137
2146
  if (Test-Path $settingsPath -PathType Leaf) {
2138
2147
  $era = 'a'
2139
- } elseif ((Test-Path $dsA -PathType Leaf) -or (Test-Path $dsB -PathType Leaf) -or (Test-Path $dsC -PathType Leaf)) {
2148
+ } elseif ((Test-Path $dsA -PathType Leaf) -or (Test-Path $dsB -PathType Leaf) -or (Test-Path $dsC -PathType Leaf) -or (Test-Path (Join-Path $aidDir '.aid-manifest.json') -PathType Leaf)) {
2149
+ # Era-b: KB-state present, OR a tracked repo (manifest present) with no
2150
+ # settings.yml yet (the `aid add`-only state). Synthesize a fresh stamped
2151
+ # settings.yml so the format gate stops warning and the repo is brought
2152
+ # current. Mirrors bin/aid. Without the manifest clause such repos warn
2153
+ # forever and are never stamped.
2140
2154
  $era = 'b'
2141
2155
  } else {
2142
- return 0 # bare .aid/ -- not a candidate
2156
+ return 0 # bare .aid/ (no settings.yml, no KB state, no manifest) -- not a candidate
2143
2157
  }
2144
2158
 
2145
2159
  $repoName = Split-Path $Repo -Leaf
@@ -2398,7 +2412,7 @@ function script:Invoke-AidRepairSettingsEraA {
2398
2412
  $sfDir = Split-Path $SettingsFile -Parent
2399
2413
  $tmp = Join-Path $sfDir ("settings.yml.aid-tmp." + [System.IO.Path]::GetRandomFileName())
2400
2414
  try {
2401
- Set-Content -LiteralPath $tmp -Value $lines.ToArray() -Encoding utf8NoBOM -ErrorAction Stop
2415
+ [System.IO.File]::WriteAllText($tmp, (($lines.ToArray()) -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
2402
2416
  Move-Item -LiteralPath $tmp -Destination $SettingsFile -Force -ErrorAction Stop
2403
2417
  } catch {
2404
2418
  Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
@@ -2769,6 +2783,7 @@ $_AidFromBundle = ''
2769
2783
  $_AidForce = $false
2770
2784
  $_AidRemoveForce = $false
2771
2785
  $_AidTarget = ''
2786
+ $_AidDryRun = $false
2772
2787
  $_AidPosTools = [System.Collections.Generic.List[string]]::new()
2773
2788
 
2774
2789
  $remIdx = 0
@@ -2795,8 +2810,9 @@ while ($remIdx -lt $script:_RemArgs.Count) {
2795
2810
  $_AidTarget = $script:_RemArgs[$remIdx]
2796
2811
  break
2797
2812
  }
2798
- '^(-NoPath|--no-path)$' { break <# bootstrap-only; silently ignore here #> }
2799
- '^(-h|--help|-Help)$' { script:Show-AidUsage $SUBCMD; script:Exit-Aid 0 }
2813
+ '^(-NoPath|--no-path)$' { break <# bootstrap-only; silently ignore here #> }
2814
+ '^(-DryRun|--dry-run)$' { $_AidDryRun = $true; break }
2815
+ '^(-h|--help|-Help)$' { script:Show-AidUsage $SUBCMD; script:Exit-Aid 0 }
2800
2816
  '^-' {
2801
2817
  script:Fail-Aid "unknown flag: $a" 2
2802
2818
  }
@@ -2820,6 +2836,16 @@ if (-not $_AidForce -and ($env:AID_FORCE -eq '1' -or $env:AID_FORCE -eq 'true'))
2820
2836
  }
2821
2837
  if ($script:_AidVerbose) { $env:AID_VERBOSE = '1' }
2822
2838
 
2839
+ # FR10: 'update' no longer accepts a per-tool positional (other than 'self' which
2840
+ # was already consumed above). Any non-flag positional on 'aid update' is a usage error.
2841
+ if ($SUBCMD -eq 'update' -and $_AidToolArg) {
2842
+ [Console]::Error.WriteLine("ERROR: aid update: unexpected argument: '$_AidToolArg'")
2843
+ [Console]::Error.WriteLine(" 'aid update' updates all installed tools -- no per-tool selection.")
2844
+ [Console]::Error.WriteLine(" Use 'aid update self' to update the CLI only.")
2845
+ [Console]::Error.WriteLine(" See 'aid update -h' for usage.")
2846
+ script:Exit-Aid 2
2847
+ }
2848
+
2823
2849
  if (-not $_AidTarget) { $_AidTarget = '.' }
2824
2850
 
2825
2851
  # Validate target directory.
@@ -2828,14 +2854,31 @@ if (-not (Test-Path $_AidTarget -PathType Container)) {
2828
2854
  }
2829
2855
  $_AidTarget = (Resolve-Path -LiteralPath $_AidTarget).Path
2830
2856
 
2831
- # ---- C-table pre-check for 'update [tool]': non-project -> offer + exit 0 ----
2832
- # Must run BEFORE resolve-tools so we never reach exit-6 when no .aid/ exists.
2833
- # 'add' uses the B-table (checked inside the dispatch case); 'remove' is not in C-table.
2857
+ # ---- FR10: 'update' outside an AID repo -> update the CLI only (not offer-and-exit) ----
2858
+ # Outside a repo: delegates to the CLI-only update path; no tool loop.
2859
+ # Inside a repo: fall through to the full tool-update pass below.
2834
2860
  # Test-AidIsProjectDir excludes the CLI state home from "is project" classification.
2835
2861
  if ($SUBCMD -eq 'update') {
2836
2862
  if (-not (script:Test-AidIsProjectDir -Dir $_AidTarget)) {
2837
- script:Invoke-AidCwdNoAidOffer -Target $_AidTarget
2838
- # Invoke-AidCwdNoAidOffer always calls Exit-Aid 0.
2863
+ # FR10 outside-repo: update the CLI only; no tool loop.
2864
+ $updCliVer = ''
2865
+ $updVerFile = Join-Path $script:_AidCodeHome 'VERSION'
2866
+ if (Test-Path $updVerFile -PathType Leaf) {
2867
+ $updCliVer = (Get-Content -LiteralPath $updVerFile -Raw -ErrorAction SilentlyContinue).Trim()
2868
+ }
2869
+ # Check if already latest using cached update-check result (no network call).
2870
+ $updCacheFile = Join-Path $HOME (Join-Path '.aid' '.update-check')
2871
+ $updCachedLatest = ''
2872
+ if (Test-Path $updCacheFile -PathType Leaf) {
2873
+ $updLines = Get-Content -LiteralPath $updCacheFile -ErrorAction SilentlyContinue
2874
+ if ($updLines -and $updLines.Count -ge 2) { $updCachedLatest = $updLines[1].Trim() }
2875
+ }
2876
+ if ($updCliVer -and $updCachedLatest -and ($updCliVer -eq $updCachedLatest)) {
2877
+ Write-Host "CLI is current (v$updCliVer)"
2878
+ script:Exit-Aid 0
2879
+ }
2880
+ script:Invoke-AidUpdateSelfIfStale -FromBundle $_AidFromBundle
2881
+ script:Exit-Aid 0
2839
2882
  }
2840
2883
  }
2841
2884
 
@@ -2847,8 +2890,8 @@ if ($SUBCMD -eq 'update' -and (script:Test-AidIsProjectDir -Dir $_AidTarget)) {
2847
2890
  }
2848
2891
 
2849
2892
  # ---- Self-update-if-needed preamble (FF-3 / CLI-2 / task-079) --------------
2850
- # For 'update [<tool>]' only (not 'add', not 'update self'). Ensures the CLI
2851
- # is current before the per-repo migration runs (FR38 / OQ-6). WARN-not-fail.
2893
+ # For 'update' inside an AID repo only (not 'add', not 'update self').
2894
+ # Ensures the CLI is current before the per-repo tool-update runs. WARN-not-fail.
2852
2895
  if ($SUBCMD -eq 'update') {
2853
2896
  script:Invoke-AidUpdateSelfIfStale -FromBundle $_AidFromBundle
2854
2897
  }
@@ -3017,8 +3060,6 @@ function script:Prepare-AidToolStaging {
3017
3060
  # Dispatch to engine.
3018
3061
  # ---------------------------------------------------------------------------
3019
3062
  try {
3020
- $overallBlocked = $false
3021
-
3022
3063
  switch ($SUBCMD) {
3023
3064
  { $_ -in @('add', 'update') } {
3024
3065
  # B-table (for 'add'): writability pre-check BEFORE any .aid/ is created.
@@ -3034,33 +3075,142 @@ try {
3034
3075
  }
3035
3076
  }
3036
3077
 
3037
- # C-table (for 'update [tool]'): register-on-encounter.
3078
+ # ---------------------------------------------------------------------------
3079
+ # FR11: aid add version selection (same-version invariant).
3080
+ # First-tool (no existing tools in manifest): install at the CLI version.
3081
+ # Additional-tool (manifest already has >=1 tool): install at the EXISTING
3082
+ # tools' version to keep the repo uniform. add does NOT force a repo-wide
3083
+ # update. -Version on add must apply to ALL tools or error (mixed-version
3084
+ # repo would result if the requested version differs from the existing one).
3085
+ # ---------------------------------------------------------------------------
3086
+ if ($SUBCMD -eq 'add') {
3087
+ $_fr11CliVer = ''
3088
+ $fr11VerFile = Join-Path $script:_AidCodeHome 'VERSION'
3089
+ if (Test-Path $fr11VerFile -PathType Leaf) {
3090
+ $_fr11CliVer = (Get-Content -LiteralPath $fr11VerFile -Raw -ErrorAction SilentlyContinue).Trim()
3091
+ }
3092
+ $_fr11ExistingVer = ''
3093
+ if (Test-Path $_AidManifest -PathType Leaf) {
3094
+ $fr11FirstTool = (Get-ManifestToolList -ManifestPath $_AidManifest | Select-Object -First 1)
3095
+ if ($fr11FirstTool) {
3096
+ $_fr11ExistingVer = Read-ManifestToolVersion -ManifestPath $_AidManifest -Tool $fr11FirstTool.Id
3097
+ }
3098
+ }
3099
+
3100
+ if ($_AidVersionArg) {
3101
+ # -Version on add: validate it won't create a mixed-version repo.
3102
+ if ($_fr11ExistingVer -and $_AidVersionArg -ne $_fr11ExistingVer) {
3103
+ [Console]::Error.WriteLine("ERROR: aid add: -Version $_AidVersionArg would create a mixed-version repo.")
3104
+ [Console]::Error.WriteLine(" Existing tools are at v$_fr11ExistingVer. Either:")
3105
+ [Console]::Error.WriteLine(" - Omit -Version to install at the repo version (v$_fr11ExistingVer), or")
3106
+ [Console]::Error.WriteLine(" - Run 'aid update -Version $_AidVersionArg' first to advance the whole repo.")
3107
+ script:Exit-Aid 2
3108
+ }
3109
+ # -Version provided and no conflict: apply to all tools (passed through to staging).
3110
+ } elseif ($_fr11ExistingVer) {
3111
+ # Additional-tool: pin staging to the existing repo version (not the CLI version).
3112
+ $_AidVersionArg = $_fr11ExistingVer
3113
+ # Skew notice when CLI is ahead of the repo version.
3114
+ if ($_fr11CliVer) {
3115
+ $fr11PartsA = $_fr11ExistingVer -split '\.'
3116
+ $fr11PartsB = $_fr11CliVer -split '\.'
3117
+ $fr11IsLt = $false
3118
+ for ($fr11i = 0; $fr11i -lt 3; $fr11i++) {
3119
+ $fr11rA = if ($fr11i -lt $fr11PartsA.Count) { $fr11PartsA[$fr11i] } else { '0' }
3120
+ $fr11rB = if ($fr11i -lt $fr11PartsB.Count) { $fr11PartsB[$fr11i] } else { '0' }
3121
+ if ($fr11rA -match '^(\d+)') { $fr11vA = [int]$Matches[1] } else { $fr11vA = 0 }
3122
+ if ($fr11rB -match '^(\d+)') { $fr11vB = [int]$Matches[1] } else { $fr11vB = 0 }
3123
+ if ($fr11vA -lt $fr11vB) { $fr11IsLt = $true; break }
3124
+ if ($fr11vA -gt $fr11vB) { break }
3125
+ }
3126
+ if ($fr11IsLt) {
3127
+ Write-Host "repo is at v$_fr11ExistingVer; new tool(s) installed at v$_fr11ExistingVer to keep the repo uniform. Run 'aid update' to advance all tools to v$_fr11CliVer."
3128
+ }
3129
+ }
3130
+ } else {
3131
+ # First-tool: pin to CLI version (bundle supplies its own version; skip if so).
3132
+ if (-not $_AidFromBundle -and $_fr11CliVer) {
3133
+ $_AidVersionArg = $_fr11CliVer
3134
+ }
3135
+ }
3136
+ }
3137
+
3138
+ # C-table (for 'update'): register-on-encounter.
3038
3139
  # The missing-.aid/ case was already intercepted above (pre-resolve-tools).
3039
3140
  if ($SUBCMD -eq 'update') {
3040
3141
  script:Invoke-AidCwdClassify -Target $_AidTarget
3041
3142
  }
3042
3143
 
3144
+ # ---------------------------------------------------------------------------
3145
+ # FR10 Stage-all-first atomicity (task-009):
3146
+ # PHASE 1: Stage ALL tools (resolve version, fetch, checksum-verify, extract
3147
+ # to temp) BEFORE any destination write. A failure here aborts with
3148
+ # zero destination mutation.
3149
+ # ---------------------------------------------------------------------------
3150
+ $stageMap = [System.Collections.Generic.Dictionary[string,string]]::new()
3151
+ $stageVersion = ''
3152
+
3043
3153
  foreach ($t in $_AidTools) {
3044
- Write-Host ""
3045
3154
  script:Prepare-AidToolStaging -Tool $t -Version $_AidVersionArg -Bundle $_AidFromBundle
3046
- Write-Host "Installing $t v$($script:_DispResolvedVersion) -> $_AidTarget"
3047
- $rc = Install-AidTool -StagingDir $script:_DispStagingDir -Tool $t -Target $_AidTarget `
3048
- -Version $script:_DispResolvedVersion -Force ([bool]$_AidForce) `
3155
+ $stageMap[$t] = $script:_DispStagingDir
3156
+ if (-not $stageVersion) { $stageVersion = $script:_DispResolvedVersion }
3157
+ }
3158
+
3159
+ # ---------------------------------------------------------------------------
3160
+ # FR10 -DryRun: print the plan and exit with no writes.
3161
+ # ---------------------------------------------------------------------------
3162
+ if ($_AidDryRun) {
3163
+ Write-Host "--- aid $SUBCMD -DryRun plan (no writes) ---"
3164
+ Write-Host "Target: $_AidTarget"
3165
+ Write-Host "Version: $(if ($stageVersion) { $stageVersion } else { '<current>' })"
3166
+ foreach ($t in $_AidTools) {
3167
+ Write-Host ""
3168
+ Write-Host "Tool: $t"
3169
+ $dryStaging = $stageMap[$t]
3170
+ $dryFiles = @(Get-ChildItem -LiteralPath $dryStaging -Recurse -File -ErrorAction SilentlyContinue |
3171
+ Sort-Object FullName)
3172
+ foreach ($df in $dryFiles) {
3173
+ $rel = $df.FullName.Substring($dryStaging.Length).TrimStart([char]'\', [char]'/')
3174
+ Write-Host " copy: $rel -> $_AidTarget"
3175
+ }
3176
+ # List files that would be MOVED TO TRASH by the retired-root migration sweep
3177
+ # (marker 1: aid-* prefix; marker 2: inside an aid\ subtree).
3178
+ # Uses ListOnly=$true mode of Invoke-MigrateRetiredLayout (no writes).
3179
+ # The function emits paths via Write-Output; capture then display.
3180
+ $dryRemovePaths = @(Invoke-MigrateRetiredLayout -Target $_AidTarget -Tool $t -ListOnly $true)
3181
+ if ($dryRemovePaths.Count -gt 0) {
3182
+ Write-Host " Would MOVE TO TRASH (retired-layout migration):"
3183
+ foreach ($rp in $dryRemovePaths) {
3184
+ Write-Host " move to trash: $rp"
3185
+ }
3186
+ }
3187
+ }
3188
+ Write-Host ""
3189
+ Write-Host "--- end dry-run plan ---"
3190
+ script:Exit-Aid 0
3191
+ }
3192
+
3193
+ # ---------------------------------------------------------------------------
3194
+ # PHASE 2: Commit all staged tools.
3195
+ # If any commit fails, exit non-zero with a re-run-to-heal message.
3196
+ # aid update is idempotent: re-running drives every tool to the target version.
3197
+ # ---------------------------------------------------------------------------
3198
+ foreach ($t in $_AidTools) {
3199
+ Write-Host ""
3200
+ Write-Host "Installing $t v$stageVersion -> $_AidTarget"
3201
+ $rc = Install-AidTool -StagingDir $stageMap[$t] -Tool $t -Target $_AidTarget `
3202
+ -Version $stageVersion -Force ([bool]$_AidForce) `
3049
3203
  -AidVerbose $script:_AidVerbose
3050
- if ($rc -eq 5) {
3051
- $overallBlocked = $true
3052
- } elseif ($rc -ne 0) {
3204
+ if ($rc -ne 0) {
3205
+ Write-Host ""
3206
+ [Console]::Error.WriteLine("ERROR: aid $SUBCMD failed mid-commit for tool '$t' (rc=$rc).")
3207
+ [Console]::Error.WriteLine(" The repo may be at mixed versions. Re-run 'aid update' to heal.")
3053
3208
  script:Exit-Aid $rc
3054
3209
  }
3055
3210
  }
3056
3211
 
3057
3212
  Write-Host ""
3058
- if ($overallBlocked) {
3059
- Write-Host "Install complete with warnings: one or more root agent files were not overwritten."
3060
- Write-Host "Review the *.aid-new file(s) and merge, or re-run with -Force to overwrite."
3061
- script:Exit-Aid 5
3062
- }
3063
- Write-Host "Done. AID $($script:_DispResolvedVersion) installed into: $_AidTarget"
3213
+ Write-Host "Done. AID $stageVersion installed into: $_AidTarget"
3064
3214
 
3065
3215
  # B-table (for 'add'): tier-aware registration after successful install.
3066
3216
  # Decision #3 (unwritable) already handled above with error+abort.
@@ -3069,7 +3219,7 @@ try {
3069
3219
  $_btabTier = script:Resolve-AidTier -CanonPath $_AidTarget
3070
3220
  script:Registry-Register -Repo $_AidTarget -Tier $_btabTier
3071
3221
  } else {
3072
- # 'update [tool]': C-table register-on-encounter already ran above.
3222
+ # 'update': C-table register-on-encounter already ran above.
3073
3223
  # The post-install register is idempotent; route via user tier.
3074
3224
  script:Registry-Register -Repo $_AidTarget -Tier 'user'
3075
3225
  }
@@ -1581,11 +1581,13 @@ footer {
1581
1581
  }
1582
1582
 
1583
1583
  // Build the KB summary card — 5-state (UI-A, feature-007, task-065)
1584
- // Reads kb_state.status LITERALLY (never re-derives client-side).
1584
+ // Reads kb_state.status / doc_freshness / suspect_count LITERALLY (never re-derives client-side).
1585
1585
  // States: pending | generating | preparing | approved | outdated
1586
1586
  // An unknown/missing status degrades to the pending (dim) treatment (DM-A2).
1587
1587
  // Only approved/outdated are clickable (FR32); others render a dead (non-link) card.
1588
1588
  // Clickable href is location-relative ./kb.html (LC-A3).
1589
+ // Freshness signal (f007 task-043): per-doc suspect marker appended after state block when
1590
+ // kb_state.suspect_count > 0; reads doc_freshness[] literally (absence-tolerant).
1589
1591
  function _renderKbCard(kbState) {
1590
1592
  // Resolve status: read literally; null kbState or missing/unknown status -> 'pending'
1591
1593
  var KB_STATUSES = ['pending', 'generating', 'preparing', 'approved', 'outdated'];
@@ -1723,14 +1725,49 @@ footer {
1723
1725
  meta.style.marginTop = '0.5rem';
1724
1726
  meta.textContent = metaText;
1725
1727
  card.appendChild(meta);
1726
- // Outdated refresh prompt (FR18-style, FR32)
1728
+ // Outdated refresh prompt (FR18-style, FR32) -- per-doc language (f007/task-043)
1727
1729
  var prompt = document.createElement('p');
1728
1730
  prompt.className = 'meta';
1729
1731
  prompt.style.marginTop = '0.5rem';
1730
- prompt.textContent = 'The branch has advanced past the KB baseline. 1. Run /aid-housekeep to reconcile the KB and refresh the summary. 2. Verify: this card returns to Ready on the next refresh.';
1732
+ prompt.textContent = 'One or more docs are suspect or the KB baseline has been exceeded. Run /aid-housekeep to reconcile the affected docs. Verify: this card returns to Ready on the next refresh.';
1731
1733
  card.appendChild(prompt);
1732
1734
  }
1733
1735
 
1736
+ // Per-doc suspect marker (f007/task-043): appended after state block, absent-tolerant.
1737
+ // Reads kb_state.doc_freshness / suspect_count LITERALLY (no client-side re-derivation).
1738
+ // Shown on any coarse status when suspect_count > 0; omitted (no element added) when 0 or absent.
1739
+ var suspectCount = (kbState && typeof kbState.suspect_count === 'number') ? kbState.suspect_count : 0;
1740
+ if (suspectCount > 0) {
1741
+ var suspectBadge = document.createElement('span');
1742
+ suspectBadge.className = 'badge badge-warn kb-suspect-badge';
1743
+ suspectBadge.style.display = 'inline-block';
1744
+ suspectBadge.style.marginTop = '0.75rem';
1745
+ suspectBadge.style.marginBottom = '0.25rem';
1746
+ suspectBadge.textContent = suspectCount + ' doc' + (suspectCount === 1 ? '' : 's') + ' suspect';
1747
+ card.appendChild(suspectBadge);
1748
+
1749
+ // List each suspect doc with its drifted source(s) where present
1750
+ var freshness = (kbState && Array.isArray(kbState.doc_freshness)) ? kbState.doc_freshness : [];
1751
+ var suspectDocs = freshness.filter(function(d) { return d && d.verdict === 'suspect'; });
1752
+ if (suspectDocs.length > 0) {
1753
+ var suspectList = document.createElement('ul');
1754
+ suspectList.className = 'meta kb-suspect-list';
1755
+ suspectList.style.marginTop = '0.25rem';
1756
+ suspectList.style.paddingLeft = '1.25rem';
1757
+ for (var si = 0; si < suspectDocs.length; si++) {
1758
+ var sd = suspectDocs[si];
1759
+ var li = document.createElement('li');
1760
+ var docLabel = sd.doc || '(unknown)';
1761
+ if (sd.suspect_sources && sd.suspect_sources.length > 0) {
1762
+ docLabel += ' — source: ' + sd.suspect_sources.join(', ');
1763
+ }
1764
+ li.textContent = docLabel;
1765
+ suspectList.appendChild(li);
1766
+ }
1767
+ card.appendChild(suspectList);
1768
+ }
1769
+ }
1770
+
1734
1771
  return card;
1735
1772
  }
1736
1773
 
@@ -1764,7 +1801,7 @@ footer {
1764
1801
  var li1 = document.createElement('li');
1765
1802
  li1.appendChild(document.createTextNode('In this repo, run: '));
1766
1803
  var codeEl = document.createElement('code');
1767
- codeEl.textContent = '/aid-interview';
1804
+ codeEl.textContent = '/aid-describe';
1768
1805
  li1.appendChild(codeEl);
1769
1806
  li1.appendChild(document.createTextNode(' — begins a new work (creates .aid/work-NNN-<name>/ + its STATE.md).'));
1770
1807
  ol.appendChild(li1);