aid-installer 1.1.1 → 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
@@ -2403,7 +2412,7 @@ function script:Invoke-AidRepairSettingsEraA {
2403
2412
  $sfDir = Split-Path $SettingsFile -Parent
2404
2413
  $tmp = Join-Path $sfDir ("settings.yml.aid-tmp." + [System.IO.Path]::GetRandomFileName())
2405
2414
  try {
2406
- 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))
2407
2416
  Move-Item -LiteralPath $tmp -Destination $SettingsFile -Force -ErrorAction Stop
2408
2417
  } catch {
2409
2418
  Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
@@ -2774,6 +2783,7 @@ $_AidFromBundle = ''
2774
2783
  $_AidForce = $false
2775
2784
  $_AidRemoveForce = $false
2776
2785
  $_AidTarget = ''
2786
+ $_AidDryRun = $false
2777
2787
  $_AidPosTools = [System.Collections.Generic.List[string]]::new()
2778
2788
 
2779
2789
  $remIdx = 0
@@ -2800,8 +2810,9 @@ while ($remIdx -lt $script:_RemArgs.Count) {
2800
2810
  $_AidTarget = $script:_RemArgs[$remIdx]
2801
2811
  break
2802
2812
  }
2803
- '^(-NoPath|--no-path)$' { break <# bootstrap-only; silently ignore here #> }
2804
- '^(-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 }
2805
2816
  '^-' {
2806
2817
  script:Fail-Aid "unknown flag: $a" 2
2807
2818
  }
@@ -2825,6 +2836,16 @@ if (-not $_AidForce -and ($env:AID_FORCE -eq '1' -or $env:AID_FORCE -eq 'true'))
2825
2836
  }
2826
2837
  if ($script:_AidVerbose) { $env:AID_VERBOSE = '1' }
2827
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
+
2828
2849
  if (-not $_AidTarget) { $_AidTarget = '.' }
2829
2850
 
2830
2851
  # Validate target directory.
@@ -2833,14 +2854,31 @@ if (-not (Test-Path $_AidTarget -PathType Container)) {
2833
2854
  }
2834
2855
  $_AidTarget = (Resolve-Path -LiteralPath $_AidTarget).Path
2835
2856
 
2836
- # ---- C-table pre-check for 'update [tool]': non-project -> offer + exit 0 ----
2837
- # Must run BEFORE resolve-tools so we never reach exit-6 when no .aid/ exists.
2838
- # '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.
2839
2860
  # Test-AidIsProjectDir excludes the CLI state home from "is project" classification.
2840
2861
  if ($SUBCMD -eq 'update') {
2841
2862
  if (-not (script:Test-AidIsProjectDir -Dir $_AidTarget)) {
2842
- script:Invoke-AidCwdNoAidOffer -Target $_AidTarget
2843
- # 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
2844
2882
  }
2845
2883
  }
2846
2884
 
@@ -2852,8 +2890,8 @@ if ($SUBCMD -eq 'update' -and (script:Test-AidIsProjectDir -Dir $_AidTarget)) {
2852
2890
  }
2853
2891
 
2854
2892
  # ---- Self-update-if-needed preamble (FF-3 / CLI-2 / task-079) --------------
2855
- # For 'update [<tool>]' only (not 'add', not 'update self'). Ensures the CLI
2856
- # 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.
2857
2895
  if ($SUBCMD -eq 'update') {
2858
2896
  script:Invoke-AidUpdateSelfIfStale -FromBundle $_AidFromBundle
2859
2897
  }
@@ -3022,8 +3060,6 @@ function script:Prepare-AidToolStaging {
3022
3060
  # Dispatch to engine.
3023
3061
  # ---------------------------------------------------------------------------
3024
3062
  try {
3025
- $overallBlocked = $false
3026
-
3027
3063
  switch ($SUBCMD) {
3028
3064
  { $_ -in @('add', 'update') } {
3029
3065
  # B-table (for 'add'): writability pre-check BEFORE any .aid/ is created.
@@ -3039,33 +3075,142 @@ try {
3039
3075
  }
3040
3076
  }
3041
3077
 
3042
- # 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.
3043
3139
  # The missing-.aid/ case was already intercepted above (pre-resolve-tools).
3044
3140
  if ($SUBCMD -eq 'update') {
3045
3141
  script:Invoke-AidCwdClassify -Target $_AidTarget
3046
3142
  }
3047
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
+
3048
3153
  foreach ($t in $_AidTools) {
3049
- Write-Host ""
3050
3154
  script:Prepare-AidToolStaging -Tool $t -Version $_AidVersionArg -Bundle $_AidFromBundle
3051
- Write-Host "Installing $t v$($script:_DispResolvedVersion) -> $_AidTarget"
3052
- $rc = Install-AidTool -StagingDir $script:_DispStagingDir -Tool $t -Target $_AidTarget `
3053
- -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) `
3054
3203
  -AidVerbose $script:_AidVerbose
3055
- if ($rc -eq 5) {
3056
- $overallBlocked = $true
3057
- } 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.")
3058
3208
  script:Exit-Aid $rc
3059
3209
  }
3060
3210
  }
3061
3211
 
3062
3212
  Write-Host ""
3063
- if ($overallBlocked) {
3064
- Write-Host "Install complete with warnings: one or more root agent files were not overwritten."
3065
- Write-Host "Review the *.aid-new file(s) and merge, or re-run with -Force to overwrite."
3066
- script:Exit-Aid 5
3067
- }
3068
- Write-Host "Done. AID $($script:_DispResolvedVersion) installed into: $_AidTarget"
3213
+ Write-Host "Done. AID $stageVersion installed into: $_AidTarget"
3069
3214
 
3070
3215
  # B-table (for 'add'): tier-aware registration after successful install.
3071
3216
  # Decision #3 (unwritable) already handled above with error+abort.
@@ -3074,7 +3219,7 @@ try {
3074
3219
  $_btabTier = script:Resolve-AidTier -CanonPath $_AidTarget
3075
3220
  script:Registry-Register -Repo $_AidTarget -Tier $_btabTier
3076
3221
  } else {
3077
- # 'update [tool]': C-table register-on-encounter already ran above.
3222
+ # 'update': C-table register-on-encounter already ran above.
3078
3223
  # The post-install register is idempotent; route via user tier.
3079
3224
  script:Registry-Register -Repo $_AidTarget -Tier 'user'
3080
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);