aid-installer 1.1.1 → 2.0.1

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
  }
@@ -130,6 +135,9 @@ function script:Test-AidIsProjectDir {
130
135
  # The current .aid/ layout version. Bumped ONLY on a breaking layout change,
131
136
  # never on every CLI release. Defined exactly once; all comparisons read this.
132
137
  # Integer must equal the bash AID_SUPPORTED_FORMAT in bin/aid.
138
+ # NOTE (work-007 C6): the install-time settings seed also stamps this value. On a
139
+ # bump, update ALL carriers together: this line, bin/aid AID_SUPPORTED_FORMAT, and
140
+ # lib/AidInstallCore.psm1 $script:_AidSupportedFormat (the PS seed's source).
133
141
  # ---------------------------------------------------------------------------
134
142
  Set-Variable -Name AidSupportedFormat -Value 1 -Option Constant -Scope Script
135
143
 
@@ -191,15 +199,19 @@ function script:Show-AidUsage {
191
199
  Write-Host ' -DryRun: print the exact command(s) it would run, then exit (no changes).'
192
200
  }
193
201
  'update' {
194
- Write-Host 'aid update [<tool>...] [-Version <v>] [-FromBundle <path>] [-Force] [-Target <dir>]'
202
+ Write-Host 'aid update [-Version <v>] [-FromBundle <path>] [-Force] [-DryRun] [-Target <dir>]'
195
203
  Write-Host 'aid update self [-FromBundle <path>] [-DryRun]'
196
- Write-Host ' Update to latest. No args: update all installed tools.'
204
+ Write-Host ' Update to latest.'
205
+ Write-Host ' Outside an AID repo: updates the CLI only (no-op if already latest).'
206
+ Write-Host ' Inside an AID repo: updates the CLI first, then ALL installed tools to one version.'
207
+ Write-Host ' No per-tool selection -- any tool positional is an error (use "self" only).'
197
208
  Write-Host ' self: COMPLETELY update the aid CLI, channel-aware:'
198
209
  Write-Host ' npm -> npm i -g | pypi -> pipx upgrade | curl -> re-bootstrap install.ps1.'
199
210
  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'
211
+ Write-Host ' -Version <v>: pin ALL tools (and CLI) to version v.'
212
+ Write-Host ' -FromBundle <path>: install from a local artifact instead of @latest'
201
213
  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).'
214
+ Write-Host ' -DryRun: print the full plan (tools updated, files copied, paths pruned) and exit.'
203
215
  }
204
216
  'version' {
205
217
  Write-Host 'aid version'
@@ -241,7 +253,7 @@ function script:Show-AidUsage {
241
253
  Write-Host ' aid version Print the CLI version'
242
254
  Write-Host ' aid status Show AID state of the current project'
243
255
  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'
256
+ Write-Host ' aid update [self] Update to latest; inside repo = all tools'
245
257
  Write-Host ' aid remove [<tool>... | self] Remove; no arg = ALL AID from project'
246
258
  Write-Host ' aid dashboard start|stop ... Start/stop the local dashboard'
247
259
  Write-Host ' aid projects [list|add|remove] List/register/unregister AID projects'
@@ -1448,7 +1460,7 @@ function script:Get-AidProjectState {
1448
1460
  # Mirror of bash _aid_project_tools.
1449
1461
  function script:Get-AidProjectTools {
1450
1462
  param([string]$Path)
1451
- $manifest = Join-Path $Path '.aid' '.aid-manifest.json'
1463
+ $manifest = Join-Path (Join-Path $Path '.aid') '.aid-manifest.json'
1452
1464
  if (-not (Test-Path $manifest -PathType Leaf)) { return '' }
1453
1465
  $lines = Get-Content -LiteralPath $manifest -Encoding utf8 -ErrorAction SilentlyContinue
1454
1466
  if (-not $lines) { return '' }
@@ -1482,7 +1494,7 @@ function script:Get-AidProjectTools {
1482
1494
  function script:Get-WhichTierHolds {
1483
1495
  param([string]$Path)
1484
1496
  $primaryReg = Join-Path $script:_AidStateHome 'registry.yml'
1485
- $userReg = Join-Path $HOME '.aid' 'registry.yml'
1497
+ $userReg = Join-Path (Join-Path $HOME '.aid') 'registry.yml'
1486
1498
 
1487
1499
  $primaryNorm = [System.IO.Path]::GetFullPath($script:_AidStateHome)
1488
1500
  $userNorm = [System.IO.Path]::GetFullPath((Join-Path $HOME '.aid'))
@@ -1539,7 +1551,7 @@ function script:Invoke-AidProjectsList {
1539
1551
  ([System.IO.Path]::GetFullPath($script:_AidStateHome) -ne [System.IO.Path]::GetFullPath((Join-Path $HOME '.aid')))) {
1540
1552
  Join-Path $script:_AidStateHome 'registry.yml'
1541
1553
  } else {
1542
- Join-Path $HOME '.aid' 'registry.yml'
1554
+ Join-Path (Join-Path $HOME '.aid') 'registry.yml'
1543
1555
  }
1544
1556
  Write-Host (' registry: {0}' -f $regSrc)
1545
1557
  }
@@ -1599,7 +1611,7 @@ function script:Invoke-AidProjectsAdd {
1599
1611
  $regFile = if ($tier -eq 'shared' -and $primaryNorm -ne $userNorm) {
1600
1612
  Join-Path $script:_AidStateHome 'registry.yml'
1601
1613
  } else {
1602
- Join-Path $HOME '.aid' 'registry.yml'
1614
+ Join-Path (Join-Path $HOME '.aid') 'registry.yml'
1603
1615
  }
1604
1616
  Write-Host ("aid projects: registry file: $regFile")
1605
1617
  }
@@ -1624,7 +1636,7 @@ function script:Invoke-AidProjectsRemove {
1624
1636
 
1625
1637
  # Check if registered before unregistering (for idempotency message).
1626
1638
  $primaryReg = Join-Path $script:_AidStateHome 'registry.yml'
1627
- $userReg = Join-Path $HOME '.aid' 'registry.yml'
1639
+ $userReg = Join-Path (Join-Path $HOME '.aid') 'registry.yml'
1628
1640
  $primaryNorm = [System.IO.Path]::GetFullPath($script:_AidStateHome)
1629
1641
  $userNorm = [System.IO.Path]::GetFullPath((Join-Path $HOME '.aid'))
1630
1642
  $found = $false
@@ -1791,7 +1803,7 @@ function script:Registry-Register {
1791
1803
  $lns.Add("schema: 1")
1792
1804
  $lns.Add("projects:")
1793
1805
  foreach ($p in ($repos | Where-Object { $_ } | Sort-Object -Unique)) { $lns.Add(" - $p") }
1794
- Set-Content -LiteralPath $tmp -Value $lns.ToArray() -Encoding utf8NoBOM -ErrorAction Stop
1806
+ [System.IO.File]::WriteAllText($tmp, (($lns.ToArray()) -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
1795
1807
  Move-Item -LiteralPath $tmp -Destination $regPath -Force -ErrorAction Stop
1796
1808
  return $true
1797
1809
  } catch {
@@ -1904,7 +1916,7 @@ function script:Registry-Unregister {
1904
1916
  $lns.Add("schema: 1")
1905
1917
  $lns.Add("projects:")
1906
1918
  if ($remaining) { foreach ($p in $remaining) { $lns.Add(" - $p") } }
1907
- Set-Content -LiteralPath $tmp -Value $lns.ToArray() -Encoding utf8NoBOM -ErrorAction Stop
1919
+ [System.IO.File]::WriteAllText($tmp, (($lns.ToArray()) -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
1908
1920
  Move-Item -LiteralPath $tmp -Destination $regPath -Force -ErrorAction Stop
1909
1921
  return $true
1910
1922
  } catch {
@@ -2006,7 +2018,7 @@ if ($script:_RawArgs.Count -eq 0) {
2006
2018
  if (Test-Path $verFile -PathType Leaf) {
2007
2019
  $cliVersion = (Get-Content -LiteralPath $verFile -Raw).Trim()
2008
2020
  }
2009
- Write-Host "AID v$cliVersion - Agentic Iterative Development"
2021
+ Write-Host "AID v$cliVersion - AI Integrated Development"
2010
2022
  Write-Host "Install, update, and manage AID across your projects."
2011
2023
 
2012
2024
  # C6': format gate for cwd repo (.aid/ is guaranteed present here -- the
@@ -2403,7 +2415,7 @@ function script:Invoke-AidRepairSettingsEraA {
2403
2415
  $sfDir = Split-Path $SettingsFile -Parent
2404
2416
  $tmp = Join-Path $sfDir ("settings.yml.aid-tmp." + [System.IO.Path]::GetRandomFileName())
2405
2417
  try {
2406
- Set-Content -LiteralPath $tmp -Value $lines.ToArray() -Encoding utf8NoBOM -ErrorAction Stop
2418
+ [System.IO.File]::WriteAllText($tmp, (($lines.ToArray()) -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
2407
2419
  Move-Item -LiteralPath $tmp -Destination $SettingsFile -Force -ErrorAction Stop
2408
2420
  } catch {
2409
2421
  Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
@@ -2774,6 +2786,7 @@ $_AidFromBundle = ''
2774
2786
  $_AidForce = $false
2775
2787
  $_AidRemoveForce = $false
2776
2788
  $_AidTarget = ''
2789
+ $_AidDryRun = $false
2777
2790
  $_AidPosTools = [System.Collections.Generic.List[string]]::new()
2778
2791
 
2779
2792
  $remIdx = 0
@@ -2800,8 +2813,9 @@ while ($remIdx -lt $script:_RemArgs.Count) {
2800
2813
  $_AidTarget = $script:_RemArgs[$remIdx]
2801
2814
  break
2802
2815
  }
2803
- '^(-NoPath|--no-path)$' { break <# bootstrap-only; silently ignore here #> }
2804
- '^(-h|--help|-Help)$' { script:Show-AidUsage $SUBCMD; script:Exit-Aid 0 }
2816
+ '^(-NoPath|--no-path)$' { break <# bootstrap-only; silently ignore here #> }
2817
+ '^(-DryRun|--dry-run)$' { $_AidDryRun = $true; break }
2818
+ '^(-h|--help|-Help)$' { script:Show-AidUsage $SUBCMD; script:Exit-Aid 0 }
2805
2819
  '^-' {
2806
2820
  script:Fail-Aid "unknown flag: $a" 2
2807
2821
  }
@@ -2825,6 +2839,16 @@ if (-not $_AidForce -and ($env:AID_FORCE -eq '1' -or $env:AID_FORCE -eq 'true'))
2825
2839
  }
2826
2840
  if ($script:_AidVerbose) { $env:AID_VERBOSE = '1' }
2827
2841
 
2842
+ # FR10: 'update' no longer accepts a per-tool positional (other than 'self' which
2843
+ # was already consumed above). Any non-flag positional on 'aid update' is a usage error.
2844
+ if ($SUBCMD -eq 'update' -and $_AidToolArg) {
2845
+ [Console]::Error.WriteLine("ERROR: aid update: unexpected argument: '$_AidToolArg'")
2846
+ [Console]::Error.WriteLine(" 'aid update' updates all installed tools -- no per-tool selection.")
2847
+ [Console]::Error.WriteLine(" Use 'aid update self' to update the CLI only.")
2848
+ [Console]::Error.WriteLine(" See 'aid update -h' for usage.")
2849
+ script:Exit-Aid 2
2850
+ }
2851
+
2828
2852
  if (-not $_AidTarget) { $_AidTarget = '.' }
2829
2853
 
2830
2854
  # Validate target directory.
@@ -2833,14 +2857,31 @@ if (-not (Test-Path $_AidTarget -PathType Container)) {
2833
2857
  }
2834
2858
  $_AidTarget = (Resolve-Path -LiteralPath $_AidTarget).Path
2835
2859
 
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.
2860
+ # ---- FR10: 'update' outside an AID repo -> update the CLI only (not offer-and-exit) ----
2861
+ # Outside a repo: delegates to the CLI-only update path; no tool loop.
2862
+ # Inside a repo: fall through to the full tool-update pass below.
2839
2863
  # Test-AidIsProjectDir excludes the CLI state home from "is project" classification.
2840
2864
  if ($SUBCMD -eq 'update') {
2841
2865
  if (-not (script:Test-AidIsProjectDir -Dir $_AidTarget)) {
2842
- script:Invoke-AidCwdNoAidOffer -Target $_AidTarget
2843
- # Invoke-AidCwdNoAidOffer always calls Exit-Aid 0.
2866
+ # FR10 outside-repo: update the CLI only; no tool loop.
2867
+ $updCliVer = ''
2868
+ $updVerFile = Join-Path $script:_AidCodeHome 'VERSION'
2869
+ if (Test-Path $updVerFile -PathType Leaf) {
2870
+ $updCliVer = (Get-Content -LiteralPath $updVerFile -Raw -ErrorAction SilentlyContinue).Trim()
2871
+ }
2872
+ # Check if already latest using cached update-check result (no network call).
2873
+ $updCacheFile = Join-Path $HOME (Join-Path '.aid' '.update-check')
2874
+ $updCachedLatest = ''
2875
+ if (Test-Path $updCacheFile -PathType Leaf) {
2876
+ $updLines = Get-Content -LiteralPath $updCacheFile -ErrorAction SilentlyContinue
2877
+ if ($updLines -and $updLines.Count -ge 2) { $updCachedLatest = $updLines[1].Trim() }
2878
+ }
2879
+ if ($updCliVer -and $updCachedLatest -and ($updCliVer -eq $updCachedLatest)) {
2880
+ Write-Host "CLI is current (v$updCliVer)"
2881
+ script:Exit-Aid 0
2882
+ }
2883
+ script:Invoke-AidUpdateSelfIfStale -FromBundle $_AidFromBundle
2884
+ script:Exit-Aid 0
2844
2885
  }
2845
2886
  }
2846
2887
 
@@ -2852,8 +2893,8 @@ if ($SUBCMD -eq 'update' -and (script:Test-AidIsProjectDir -Dir $_AidTarget)) {
2852
2893
  }
2853
2894
 
2854
2895
  # ---- 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.
2896
+ # For 'update' inside an AID repo only (not 'add', not 'update self').
2897
+ # Ensures the CLI is current before the per-repo tool-update runs. WARN-not-fail.
2857
2898
  if ($SUBCMD -eq 'update') {
2858
2899
  script:Invoke-AidUpdateSelfIfStale -FromBundle $_AidFromBundle
2859
2900
  }
@@ -3022,8 +3063,6 @@ function script:Prepare-AidToolStaging {
3022
3063
  # Dispatch to engine.
3023
3064
  # ---------------------------------------------------------------------------
3024
3065
  try {
3025
- $overallBlocked = $false
3026
-
3027
3066
  switch ($SUBCMD) {
3028
3067
  { $_ -in @('add', 'update') } {
3029
3068
  # B-table (for 'add'): writability pre-check BEFORE any .aid/ is created.
@@ -3039,33 +3078,142 @@ try {
3039
3078
  }
3040
3079
  }
3041
3080
 
3042
- # C-table (for 'update [tool]'): register-on-encounter.
3081
+ # ---------------------------------------------------------------------------
3082
+ # FR11: aid add version selection (same-version invariant).
3083
+ # First-tool (no existing tools in manifest): install at the CLI version.
3084
+ # Additional-tool (manifest already has >=1 tool): install at the EXISTING
3085
+ # tools' version to keep the repo uniform. add does NOT force a repo-wide
3086
+ # update. -Version on add must apply to ALL tools or error (mixed-version
3087
+ # repo would result if the requested version differs from the existing one).
3088
+ # ---------------------------------------------------------------------------
3089
+ if ($SUBCMD -eq 'add') {
3090
+ $_fr11CliVer = ''
3091
+ $fr11VerFile = Join-Path $script:_AidCodeHome 'VERSION'
3092
+ if (Test-Path $fr11VerFile -PathType Leaf) {
3093
+ $_fr11CliVer = (Get-Content -LiteralPath $fr11VerFile -Raw -ErrorAction SilentlyContinue).Trim()
3094
+ }
3095
+ $_fr11ExistingVer = ''
3096
+ if (Test-Path $_AidManifest -PathType Leaf) {
3097
+ $fr11FirstTool = (Get-ManifestToolList -ManifestPath $_AidManifest | Select-Object -First 1)
3098
+ if ($fr11FirstTool) {
3099
+ $_fr11ExistingVer = Read-ManifestToolVersion -ManifestPath $_AidManifest -Tool $fr11FirstTool.Id
3100
+ }
3101
+ }
3102
+
3103
+ if ($_AidVersionArg) {
3104
+ # -Version on add: validate it won't create a mixed-version repo.
3105
+ if ($_fr11ExistingVer -and $_AidVersionArg -ne $_fr11ExistingVer) {
3106
+ [Console]::Error.WriteLine("ERROR: aid add: -Version $_AidVersionArg would create a mixed-version repo.")
3107
+ [Console]::Error.WriteLine(" Existing tools are at v$_fr11ExistingVer. Either:")
3108
+ [Console]::Error.WriteLine(" - Omit -Version to install at the repo version (v$_fr11ExistingVer), or")
3109
+ [Console]::Error.WriteLine(" - Run 'aid update -Version $_AidVersionArg' first to advance the whole repo.")
3110
+ script:Exit-Aid 2
3111
+ }
3112
+ # -Version provided and no conflict: apply to all tools (passed through to staging).
3113
+ } elseif ($_fr11ExistingVer) {
3114
+ # Additional-tool: pin staging to the existing repo version (not the CLI version).
3115
+ $_AidVersionArg = $_fr11ExistingVer
3116
+ # Skew notice when CLI is ahead of the repo version.
3117
+ if ($_fr11CliVer) {
3118
+ $fr11PartsA = $_fr11ExistingVer -split '\.'
3119
+ $fr11PartsB = $_fr11CliVer -split '\.'
3120
+ $fr11IsLt = $false
3121
+ for ($fr11i = 0; $fr11i -lt 3; $fr11i++) {
3122
+ $fr11rA = if ($fr11i -lt $fr11PartsA.Count) { $fr11PartsA[$fr11i] } else { '0' }
3123
+ $fr11rB = if ($fr11i -lt $fr11PartsB.Count) { $fr11PartsB[$fr11i] } else { '0' }
3124
+ if ($fr11rA -match '^(\d+)') { $fr11vA = [int]$Matches[1] } else { $fr11vA = 0 }
3125
+ if ($fr11rB -match '^(\d+)') { $fr11vB = [int]$Matches[1] } else { $fr11vB = 0 }
3126
+ if ($fr11vA -lt $fr11vB) { $fr11IsLt = $true; break }
3127
+ if ($fr11vA -gt $fr11vB) { break }
3128
+ }
3129
+ if ($fr11IsLt) {
3130
+ 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."
3131
+ }
3132
+ }
3133
+ } else {
3134
+ # First-tool: pin to CLI version (bundle supplies its own version; skip if so).
3135
+ if (-not $_AidFromBundle -and $_fr11CliVer) {
3136
+ $_AidVersionArg = $_fr11CliVer
3137
+ }
3138
+ }
3139
+ }
3140
+
3141
+ # C-table (for 'update'): register-on-encounter.
3043
3142
  # The missing-.aid/ case was already intercepted above (pre-resolve-tools).
3044
3143
  if ($SUBCMD -eq 'update') {
3045
3144
  script:Invoke-AidCwdClassify -Target $_AidTarget
3046
3145
  }
3047
3146
 
3147
+ # ---------------------------------------------------------------------------
3148
+ # FR10 Stage-all-first atomicity (task-009):
3149
+ # PHASE 1: Stage ALL tools (resolve version, fetch, checksum-verify, extract
3150
+ # to temp) BEFORE any destination write. A failure here aborts with
3151
+ # zero destination mutation.
3152
+ # ---------------------------------------------------------------------------
3153
+ $stageMap = [System.Collections.Generic.Dictionary[string,string]]::new()
3154
+ $stageVersion = ''
3155
+
3048
3156
  foreach ($t in $_AidTools) {
3049
- Write-Host ""
3050
3157
  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) `
3158
+ $stageMap[$t] = $script:_DispStagingDir
3159
+ if (-not $stageVersion) { $stageVersion = $script:_DispResolvedVersion }
3160
+ }
3161
+
3162
+ # ---------------------------------------------------------------------------
3163
+ # FR10 -DryRun: print the plan and exit with no writes.
3164
+ # ---------------------------------------------------------------------------
3165
+ if ($_AidDryRun) {
3166
+ Write-Host "--- aid $SUBCMD -DryRun plan (no writes) ---"
3167
+ Write-Host "Target: $_AidTarget"
3168
+ Write-Host "Version: $(if ($stageVersion) { $stageVersion } else { '<current>' })"
3169
+ foreach ($t in $_AidTools) {
3170
+ Write-Host ""
3171
+ Write-Host "Tool: $t"
3172
+ $dryStaging = $stageMap[$t]
3173
+ $dryFiles = @(Get-ChildItem -LiteralPath $dryStaging -Recurse -File -ErrorAction SilentlyContinue |
3174
+ Sort-Object FullName)
3175
+ foreach ($df in $dryFiles) {
3176
+ $rel = $df.FullName.Substring($dryStaging.Length).TrimStart([char]'\', [char]'/')
3177
+ Write-Host " copy: $rel -> $_AidTarget"
3178
+ }
3179
+ # List files that would be MOVED TO TRASH by the retired-root migration sweep
3180
+ # (marker 1: aid-* prefix; marker 2: inside an aid\ subtree).
3181
+ # Uses ListOnly=$true mode of Invoke-MigrateRetiredLayout (no writes).
3182
+ # The function emits paths via Write-Output; capture then display.
3183
+ $dryRemovePaths = @(Invoke-MigrateRetiredLayout -Target $_AidTarget -Tool $t -ListOnly $true)
3184
+ if ($dryRemovePaths.Count -gt 0) {
3185
+ Write-Host " Would MOVE TO TRASH (retired-layout migration):"
3186
+ foreach ($rp in $dryRemovePaths) {
3187
+ Write-Host " move to trash: $rp"
3188
+ }
3189
+ }
3190
+ }
3191
+ Write-Host ""
3192
+ Write-Host "--- end dry-run plan ---"
3193
+ script:Exit-Aid 0
3194
+ }
3195
+
3196
+ # ---------------------------------------------------------------------------
3197
+ # PHASE 2: Commit all staged tools.
3198
+ # If any commit fails, exit non-zero with a re-run-to-heal message.
3199
+ # aid update is idempotent: re-running drives every tool to the target version.
3200
+ # ---------------------------------------------------------------------------
3201
+ foreach ($t in $_AidTools) {
3202
+ Write-Host ""
3203
+ Write-Host "Installing $t v$stageVersion -> $_AidTarget"
3204
+ $rc = Install-AidTool -StagingDir $stageMap[$t] -Tool $t -Target $_AidTarget `
3205
+ -Version $stageVersion -Force ([bool]$_AidForce) `
3054
3206
  -AidVerbose $script:_AidVerbose
3055
- if ($rc -eq 5) {
3056
- $overallBlocked = $true
3057
- } elseif ($rc -ne 0) {
3207
+ if ($rc -ne 0) {
3208
+ Write-Host ""
3209
+ [Console]::Error.WriteLine("ERROR: aid $SUBCMD failed mid-commit for tool '$t' (rc=$rc).")
3210
+ [Console]::Error.WriteLine(" The repo may be at mixed versions. Re-run 'aid update' to heal.")
3058
3211
  script:Exit-Aid $rc
3059
3212
  }
3060
3213
  }
3061
3214
 
3062
3215
  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"
3216
+ Write-Host "Done. AID $stageVersion installed into: $_AidTarget"
3069
3217
 
3070
3218
  # B-table (for 'add'): tier-aware registration after successful install.
3071
3219
  # Decision #3 (unwritable) already handled above with error+abort.
@@ -3074,7 +3222,7 @@ try {
3074
3222
  $_btabTier = script:Resolve-AidTier -CanonPath $_AidTarget
3075
3223
  script:Registry-Register -Repo $_AidTarget -Tier $_btabTier
3076
3224
  } else {
3077
- # 'update [tool]': C-table register-on-encounter already ran above.
3225
+ # 'update': C-table register-on-encounter already ran above.
3078
3226
  # The post-install register is idempotent; route via user tier.
3079
3227
  script:Registry-Register -Repo $_AidTarget -Tier 'user'
3080
3228
  }
@@ -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);