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.
@@ -81,6 +81,19 @@ _root_agent_file() {
81
81
  esac
82
82
  }
83
83
 
84
+ # _root_dir <tool> - print the tool's install-tree root dir name (relative to the
85
+ # project root). The AID-own subtree lives at <root>/aid/ (e.g. .claude/aid/).
86
+ # Mirrors the per-tool dispatch in install_tool.
87
+ _root_dir() {
88
+ case "$1" in
89
+ claude-code) echo ".claude" ;;
90
+ codex) echo ".codex" ;;
91
+ cursor) echo ".cursor" ;;
92
+ copilot-cli) echo ".github" ;;
93
+ antigravity) echo ".agent" ;;
94
+ esac
95
+ }
96
+
84
97
  # ---------------------------------------------------------------------------
85
98
  # Utility
86
99
  # ---------------------------------------------------------------------------
@@ -315,7 +328,11 @@ copy_file() {
315
328
  mkdir -p "$dst_dir"
316
329
 
317
330
  if [[ ! -e "$dst" ]]; then
318
- cp "$src" "$dst"
331
+ if ! cp "$src" "$dst"; then
332
+ echo "ERROR: aid-install-core: copy failed: ${dst}" >&2
333
+ _COPY_COUNT_FAILED=$((_COPY_COUNT_FAILED + 1))
334
+ return 0
335
+ fi
319
336
  _COPY_COUNT_COPIED=$((_COPY_COUNT_COPIED + 1))
320
337
  [[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Copied: ${dst}"
321
338
  return 0
@@ -327,15 +344,21 @@ copy_file() {
327
344
  return 0
328
345
  fi
329
346
 
330
- # File exists and differs.
331
- if [[ "$force" -eq 1 ]]; then
332
- cp "$src" "$dst"
333
- _COPY_COUNT_UPDATED=$((_COPY_COUNT_UPDATED + 1))
334
- [[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Updated: ${dst}"
335
- else
336
- _COPY_COUNT_SKIPPED=$((_COPY_COUNT_SKIPPED + 1))
337
- [[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Skipped (differs; use --force): ${dst}"
347
+ # File exists and differs -> always overwrite (work-007).
348
+ # AID-owned files track the bundle, which is the source of truth, so an
349
+ # add/update must bring them current. `force` is retained in the signature
350
+ # for back-compat and bash<->PS parity but no longer gates this overwrite:
351
+ # skip-on-diff silently left stale files behind on in-place upgrades (e.g. an
352
+ # old flat-path skill surviving next to relocated .aid/ scripts). User-owned
353
+ # root agent files (CLAUDE.md/AGENTS.md) never reach copy_file -- they are
354
+ # handled by _copy_root_agent_file, which keeps its own force gating.
355
+ if ! cp "$src" "$dst"; then
356
+ echo "ERROR: aid-install-core: copy failed: ${dst}" >&2
357
+ _COPY_COUNT_FAILED=$((_COPY_COUNT_FAILED + 1))
358
+ return 0
338
359
  fi
360
+ _COPY_COUNT_UPDATED=$((_COPY_COUNT_UPDATED + 1))
361
+ [[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Updated: ${dst}"
339
362
  }
340
363
 
341
364
  # copy_dir <src_dir> <dst_dir> [force]
@@ -428,7 +451,11 @@ _copy_root_agent_file() {
428
451
  # then lines after the closing marker.
429
452
  local tmp
430
453
  tmp="$(mktemp "${dst_dir}/.aid-root-agent.XXXXXX")"
431
- awk -v region="$src_region" '
454
+ # awk -v applies C-escape processing to the value; double every backslash
455
+ # so a region containing "\" (or "\n"-like text) passes through literally
456
+ # (work-007 C7). No-op for the current backslash-free region.
457
+ local src_region_esc="${src_region//\\/\\\\}"
458
+ awk -v region="$src_region_esc" '
432
459
  BEGIN { in_aid=0; printed_region=0 }
433
460
  /^<!-- AID:BEGIN -->/ {
434
461
  if (!printed_region) {
@@ -471,11 +498,13 @@ _copy_root_agent_file() {
471
498
  # stem match and re-insert as a marked region.
472
499
  #
473
500
  # AID section stems to excise (exact heading stem; tolerate trailing
474
- # parenthetical like " (global)" or " (IMPERATIVE)"):
501
+ # parenthetical like " (global)" or " (IMPERATIVE)") -- MUST match every
502
+ # "## " heading in the region (see is_aid_heading):
503
+ # ## Tracking discipline
475
504
  # ## Knowledge Base
505
+ # ## Workflow
476
506
  # ## Review output format
477
507
  # ## Permissions
478
- # ## Tracking discipline
479
508
  #
480
509
  # A section runs from its "## Stem..." heading line until the next "## "
481
510
  # heading (exclusive) or end-of-file.
@@ -487,27 +516,34 @@ _copy_root_agent_file() {
487
516
  # Extract the new marked region from source.
488
517
  local new_region
489
518
  new_region="$(awk '/^<!-- AID:BEGIN -->/{found=1} found{print} /^<!-- AID:END -->/{if(found){exit}}' "$src")"
519
+ # awk -v applies C-escape processing; double backslashes so the region passes
520
+ # through literally (work-007 C7). No-op for the current backslash-free region.
521
+ local new_region_esc="${new_region//\\/\\\\}"
490
522
 
491
523
  local tmp
492
524
  tmp="$(mktemp "${dst_dir}/.aid-root-agent.XXXXXX")"
493
525
 
494
- # Use awk to perform the excise-and-reinsert in one pass.
526
+ # Use awk to perform the excise-and-reinsert in one pass (region passed
527
+ # backslash-doubled via new_region_esc -- see C7 note above).
495
528
  # The awk script:
496
529
  # - Identifies AID-managed section headings by stem prefix match.
497
530
  # - Suppresses those sections (and their body lines).
498
531
  # - At the position of the first suppressed section, emits the new region.
499
532
  # - All other lines are emitted verbatim.
500
- awk -v new_region="$new_region" '
533
+ awk -v new_region="$new_region_esc" '
501
534
  function is_aid_heading(line, stem) {
502
535
  # Match "## StemText" optionally followed by " (anything)".
503
- # Stems: Knowledge Base, Review output format, Permissions,
504
- # Tracking discipline.
536
+ # Stems must cover EVERY "## " heading inside the shipped AID:BEGIN/END
537
+ # region (Tracking discipline, Knowledge Base, Workflow, Review output
538
+ # format, Permissions) -- a stem missing here causes a duplicate section
539
+ # on the C2 (no-marker) migration path (work-007: Workflow was omitted).
505
540
  if (line !~ /^## /) return 0
506
541
  stem = line
507
542
  gsub(/^## /, "", stem)
508
543
  # Strip trailing parenthetical suffix: " (..." -> ""
509
544
  gsub(/ \([^)]*\)$/, "", stem)
510
545
  if (stem == "Knowledge Base") return 1
546
+ if (stem == "Workflow") return 1
511
547
  if (stem == "Review output format") return 1
512
548
  if (stem == "Permissions") return 1
513
549
  if (stem == "Tracking discipline") return 1
@@ -1695,6 +1731,8 @@ _prune_tool_dirs() {
1695
1731
 
1696
1732
  # ------------------------------------------------------------------
1697
1733
  # Per-tool scoping: which native dirs + which aid/ root to walk.
1734
+ # New layout (work-005/delivery-001): codex is unified under .codex/;
1735
+ # cursor and antigravity no longer ship rules/ dirs.
1698
1736
  # ------------------------------------------------------------------
1699
1737
  case "$tool" in
1700
1738
  claude-code)
@@ -1703,15 +1741,15 @@ _prune_tool_dirs() {
1703
1741
  _prune_aid_subtree "${target}/.claude/aid"
1704
1742
  ;;
1705
1743
  codex)
1706
- # .codex ships only agents/; .agents ships skills/ + aid/ subtree.
1744
+ # New unified layout: everything under .codex/ (agents, skills, aid).
1707
1745
  _prune_native_dir "${target}/.codex/agents"
1708
- _prune_native_dir "${target}/.agents/skills"
1709
- _prune_aid_subtree "${target}/.agents/aid"
1746
+ _prune_native_dir "${target}/.codex/skills"
1747
+ _prune_aid_subtree "${target}/.codex/aid"
1710
1748
  ;;
1711
1749
  cursor)
1750
+ # rules/ dir removed from new layout; agents/skills/aid remain.
1712
1751
  _prune_native_dir "${target}/.cursor/agents"
1713
1752
  _prune_native_dir "${target}/.cursor/skills"
1714
- _prune_native_dir "${target}/.cursor/rules"
1715
1753
  _prune_aid_subtree "${target}/.cursor/aid"
1716
1754
  ;;
1717
1755
  copilot-cli)
@@ -1721,7 +1759,8 @@ _prune_tool_dirs() {
1721
1759
  _prune_aid_subtree "${target}/.github/aid"
1722
1760
  ;;
1723
1761
  antigravity)
1724
- _prune_native_dir "${target}/.agent/rules"
1762
+ # rules/ dir removed from new layout; agents/skills/aid remain.
1763
+ _prune_native_dir "${target}/.agent/agents"
1725
1764
  _prune_native_dir "${target}/.agent/skills"
1726
1765
  _prune_aid_subtree "${target}/.agent/aid"
1727
1766
  ;;
@@ -1735,6 +1774,138 @@ _prune_tool_dirs() {
1735
1774
  unset -f _in_set _prune_native_dir _prune_aid_subtree
1736
1775
  }
1737
1776
 
1777
+ # ---------------------------------------------------------------------------
1778
+ # Retired-root migration sweep (FR7/FR7a)
1779
+ # ---------------------------------------------------------------------------
1780
+
1781
+ # _migrate_retired_layout <target> <tool>
1782
+ #
1783
+ # Complete-replacement migration: removes AID-owned content from the static
1784
+ # list of retired AID roots that no longer exist in the new bundle layout.
1785
+ # Called from install_tool BEFORE _prune_tool_dirs (same aid update pass).
1786
+ #
1787
+ # Retired roots swept per tool:
1788
+ # codex: .agents/ (split layout retired)
1789
+ # cursor: .cursor/rules/ (rules dir retired)
1790
+ # antigravity: .agent/rules/ (rules dir retired)
1791
+ # all tools: .cursor/rules/, .agent/rules/ are cursor/antigravity only
1792
+ #
1793
+ # Ownership markers applied (content-isolation.md rules 1+2):
1794
+ # Marker 1: filename starts with "aid-" (tool-native dir files)
1795
+ # Marker 2: lives inside an "aid/" subtree
1796
+ #
1797
+ # Marker 3 (AID:BEGIN/END region in root files) is NOT touched here;
1798
+ # that is handled by _copy_root_agent_file exclusively.
1799
+ #
1800
+ # User content (no marker) is NEVER removed.
1801
+ # Idempotent: a no-op when the retired path is already absent.
1802
+ # Returns count of retired items removed (written to _MIGRATE_RETIRED_COUNT).
1803
+ _migrate_retired_layout() {
1804
+ local target="$1"
1805
+ local tool="$2"
1806
+ local list_only="${3:-0}" # 1 = dry-run enumeration only (no removals)
1807
+ _MIGRATE_RETIRED_COUNT=0
1808
+
1809
+ # Return 1 if a file is AID-owned (marker 1 or 2); 0 if user content.
1810
+ _is_aid_owned_file() {
1811
+ local f="$1"
1812
+ local base
1813
+ base="$(basename "$f")"
1814
+ # Marker 1: aid- prefix.
1815
+ if [[ "$base" == aid-* ]]; then return 0; fi
1816
+ # Marker 2: lives inside an aid/ folder (any path component named "aid").
1817
+ local dir
1818
+ dir="$(dirname "$f")"
1819
+ while [[ "$dir" != "$target" && "$dir" != "/" && "$dir" != "." ]]; do
1820
+ if [[ "$(basename "$dir")" == "aid" ]]; then return 0; fi
1821
+ dir="$(dirname "$dir")"
1822
+ done
1823
+ # No marker -- user content.
1824
+ return 1
1825
+ }
1826
+
1827
+ # Move one AID-owned file (marker 1 or 2) to <target>/.aid/.trash/<rel-path>.
1828
+ # In list_only mode: print the path instead of moving it (no writes).
1829
+ _retire_file() {
1830
+ local f="$1"
1831
+ [[ -f "$f" ]] || return 0
1832
+ if _is_aid_owned_file "$f"; then
1833
+ if [[ "$list_only" -eq 1 ]]; then
1834
+ echo " move to trash: ${f}"
1835
+ _MIGRATE_RETIRED_COUNT=$((_MIGRATE_RETIRED_COUNT + 1))
1836
+ else
1837
+ # Compute path relative to $target and move to the trash dir.
1838
+ local rel dest
1839
+ rel="${f#${target}/}"
1840
+ dest="${target}/.aid/.trash/${rel}"
1841
+ mkdir -p "$(dirname "$dest")"
1842
+ mv -f "$f" "$dest"
1843
+ _MIGRATE_RETIRED_COUNT=$((_MIGRATE_RETIRED_COUNT + 1))
1844
+ if [[ "${AID_VERBOSE:-0}" -eq 1 ]]; then echo "Trashed: ${f} -> ${dest}"; fi
1845
+ fi
1846
+ fi
1847
+ return 0
1848
+ }
1849
+
1850
+ # Sweep one retired root directory: remove AID-owned files, then prune
1851
+ # now-empty subdirs, then remove the root dir itself if now empty.
1852
+ # In list_only mode: enumerate would-be-removed files, make no changes.
1853
+ _sweep_retired_root() {
1854
+ local rdir="$1"
1855
+ [[ -d "$rdir" ]] || return 0
1856
+ # Walk all files under the retired root.
1857
+ local fpath
1858
+ while IFS= read -r -d '' fpath; do
1859
+ _retire_file "$fpath"
1860
+ done < <(find "$rdir" -type f -print0 2>/dev/null | sort -z)
1861
+ if [[ "$list_only" -eq 1 ]]; then return 0; fi
1862
+ # Prune now-empty subdirs (deepest first).
1863
+ local dpath
1864
+ while IFS= read -r dpath; do
1865
+ if [[ -d "$dpath" ]]; then
1866
+ local rem
1867
+ rem="$(find "$dpath" -mindepth 1 2>/dev/null | head -1)"
1868
+ if [[ -z "$rem" ]]; then
1869
+ rmdir "$dpath" 2>/dev/null || true
1870
+ if [[ "${AID_VERBOSE:-0}" -eq 1 ]]; then echo "Retired dir: ${dpath}"; fi
1871
+ fi
1872
+ fi
1873
+ done < <(find "$rdir" -mindepth 1 -type d 2>/dev/null | sort -r)
1874
+ # Remove the retired root itself if now empty.
1875
+ if [[ -d "$rdir" ]]; then
1876
+ local rem
1877
+ rem="$(find "$rdir" -mindepth 1 2>/dev/null | head -1)"
1878
+ if [[ -z "$rem" ]]; then
1879
+ rmdir "$rdir" 2>/dev/null || true
1880
+ if [[ "${AID_VERBOSE:-0}" -eq 1 ]]; then echo "Retired root dir: ${rdir}"; fi
1881
+ fi
1882
+ fi
1883
+ return 0
1884
+ }
1885
+
1886
+ case "$tool" in
1887
+ codex)
1888
+ # Retired root: .agents/ (old split layout -- skills/ + aid/ lived here).
1889
+ _sweep_retired_root "${target}/.agents"
1890
+ ;;
1891
+ cursor)
1892
+ # Retired root: .cursor/rules/ (rules dir no longer in new layout).
1893
+ _sweep_retired_root "${target}/.cursor/rules"
1894
+ ;;
1895
+ antigravity)
1896
+ # Retired root: .agent/rules/ (rules dir no longer in new layout).
1897
+ _sweep_retired_root "${target}/.agent/rules"
1898
+ ;;
1899
+ # claude-code and copilot-cli have no retired roots in this migration.
1900
+ esac
1901
+
1902
+ if [[ "$list_only" -eq 0 && "$_MIGRATE_RETIRED_COUNT" -gt 0 ]]; then
1903
+ echo " ${_MIGRATE_RETIRED_COUNT} retired AID file(s) moved to .aid/.trash/"
1904
+ fi
1905
+
1906
+ unset -f _is_aid_owned_file _retire_file _sweep_retired_root
1907
+ }
1908
+
1738
1909
  # ---------------------------------------------------------------------------
1739
1910
  # Version marker
1740
1911
  # ---------------------------------------------------------------------------
@@ -1746,6 +1917,133 @@ write_version_marker() {
1746
1917
  printf '%s\n' "$version" > "${target}/.aid/.aid-version"
1747
1918
  }
1748
1919
 
1920
+ # ---------------------------------------------------------------------------
1921
+ # Project-level provisioning (work-007): required runtime file + VCS hygiene.
1922
+ # Both are idempotent and safe to run once per tool during a multi-tool add.
1923
+ # ---------------------------------------------------------------------------
1924
+
1925
+ # seed_settings_yml <target> <tool>
1926
+ # Seed <target>/.aid/settings.yml from the tool's just-installed template when it
1927
+ # does not already exist. NEVER overwrites an existing settings.yml -- it holds
1928
+ # user config (project identity, grades). Not recorded in the manifest: it is
1929
+ # user data and must survive `aid remove`/prune and never flow through the copy
1930
+ # engine. Sets _CORE_SEEDED_SETTINGS=1 when a file was created, else 0.
1931
+ seed_settings_yml() {
1932
+ local target="$1" tool="$2"
1933
+ local root dst tmpl
1934
+ _CORE_SEEDED_SETTINGS=0
1935
+ root="$(_root_dir "$tool")"
1936
+ [[ -z "$root" ]] && return 0
1937
+ dst="${target}/.aid/settings.yml"
1938
+ tmpl="${target}/${root}/aid/templates/settings.yml"
1939
+ [[ -f "$dst" ]] && return 0 # never clobber user config
1940
+ # Template absent -> a tool bundle shipped without aid/templates/settings.yml.
1941
+ # Surface it (work-007 C5): a silent skip re-triggers the "no settings.yml"
1942
+ # class this seed exists to fix, and the format gate would then warn forever.
1943
+ if [[ ! -f "$tmpl" ]]; then
1944
+ echo "WARN: aid-install-core: settings template missing (${tmpl}); .aid/settings.yml not seeded for '${tool}'." >&2
1945
+ return 0
1946
+ fi
1947
+ mkdir -p "${target}/.aid"
1948
+ # Seed from the template, but STAMP the format_version as the first line so
1949
+ # the settings-format gate does not warn on every subsequent command (the raw
1950
+ # template ships unstamped; era-b synthesis stamps -- we match it here). The
1951
+ # value mirrors bin/aid's AID_SUPPORTED_FORMAT (fallback 1 for install.sh).
1952
+ local _fmt _seed_tmp
1953
+ _fmt="${AID_SUPPORTED_FORMAT:-1}"
1954
+ _seed_tmp="$(mktemp "${dst}.aid-tmp.XXXXXX")" || {
1955
+ echo "WARN: aid-install-core: could not create temp file to seed .aid/settings.yml for '${tool}'." >&2
1956
+ return 0
1957
+ }
1958
+ if grep -q '^format_version:' "$tmpl" 2>/dev/null; then
1959
+ cp "$tmpl" "$_seed_tmp"
1960
+ else
1961
+ { printf 'format_version: %s\n' "$_fmt"; cat "$tmpl"; } > "$_seed_tmp"
1962
+ fi
1963
+ if ! mv -f "$_seed_tmp" "$dst"; then
1964
+ rm -f "$_seed_tmp" 2>/dev/null
1965
+ echo "WARN: aid-install-core: could not write .aid/settings.yml for '${tool}'." >&2
1966
+ return 0
1967
+ fi
1968
+ _CORE_SEEDED_SETTINGS=1
1969
+ return 0
1970
+ }
1971
+
1972
+ # AID-managed .gitignore region markers. The lines BETWEEN the markers are owned
1973
+ # by the installer and rewritten on every add/update; everything OUTSIDE the
1974
+ # markers is preserved verbatim (mirrors the root-agent region-update pattern).
1975
+ _AID_GI_BEGIN="# >>> AID managed -- do not edit (aid add/update maintains this block) >>>"
1976
+ _AID_GI_END="# <<< AID managed <<<"
1977
+
1978
+ # _aid_gitignore_block - print the current AID-managed .gitignore block (markers
1979
+ # inclusive). The exclusion set is the single source of truth for transient
1980
+ # .aid/ artifacts that must never be committed.
1981
+ _aid_gitignore_block() {
1982
+ printf '%s\n' "$_AID_GI_BEGIN"
1983
+ printf '%s\n' \
1984
+ ".aid/.temp/" \
1985
+ ".aid/.trash/" \
1986
+ ".aid/.heartbeat/" \
1987
+ ".aid/generated/" \
1988
+ ".aid/knowledge/.cache/" \
1989
+ ".aid/knowledge/.manual-checklist.json" \
1990
+ ".aid/knowledge/.spot-check-facts.txt"
1991
+ printf '%s' "$_AID_GI_END"
1992
+ }
1993
+
1994
+ # update_gitignore <target>
1995
+ # Ensure <target>/.gitignore carries the AID-managed region with the current
1996
+ # transient .aid/ exclusions. Creates the file if absent; otherwise strips any
1997
+ # existing AID region, preserves all user content, and appends a fresh region
1998
+ # (with a single blank-line separator). Idempotent: a second run produces a
1999
+ # byte-identical file. Sets _CORE_GITIGNORE_ACTION to created|updated|unchanged.
2000
+ update_gitignore() {
2001
+ local target="$1"
2002
+ local gi="${target}/.gitignore"
2003
+ local block tmp
2004
+ _CORE_GITIGNORE_ACTION="unchanged"
2005
+ block="$(_aid_gitignore_block)"
2006
+
2007
+ if [[ ! -f "$gi" ]]; then
2008
+ printf '%s\n' "$block" > "$gi"
2009
+ _CORE_GITIGNORE_ACTION="created"
2010
+ return 0
2011
+ fi
2012
+
2013
+ # Normalize the existing file to LF for BOTH processing and comparison, so a
2014
+ # line-ending-only difference (a CRLF .gitignore) does NOT force a rewrite
2015
+ # every run. Parity with PS Update-AidGitignore, which compares CRLF->LF-
2016
+ # normalized content (work-007 C3). If unchanged, the original file is left
2017
+ # byte-untouched (CRLF preserved); only a real region change triggers a write.
2018
+ local norm
2019
+ norm="$(mktemp "${target}/.gitignore.norm.XXXXXX")"
2020
+ tr -d '\r' < "$gi" > "$norm"
2021
+
2022
+ tmp="$(mktemp "${target}/.gitignore.aid.XXXXXX")"
2023
+ awk -v b="$_AID_GI_BEGIN" -v e="$_AID_GI_END" -v blk="$block" '
2024
+ $0==b {inblk=1; next}
2025
+ inblk && $0==e {inblk=0; next}
2026
+ inblk {next}
2027
+ {lines[n++]=$0}
2028
+ END {
2029
+ while (n>0 && lines[n-1] ~ /^[ \t]*$/) n--
2030
+ for (i=0;i<n;i++) print lines[i]
2031
+ if (n>0) print ""
2032
+ print blk
2033
+ }
2034
+ ' "$norm" > "$tmp"
2035
+
2036
+ if cmp -s "$tmp" "$norm"; then
2037
+ rm -f "$tmp" "$norm"
2038
+ _CORE_GITIGNORE_ACTION="unchanged"
2039
+ else
2040
+ rm -f "$norm"
2041
+ mv -f "$tmp" "$gi"
2042
+ _CORE_GITIGNORE_ACTION="updated"
2043
+ fi
2044
+ return 0
2045
+ }
2046
+
1749
2047
  # ---------------------------------------------------------------------------
1750
2048
  # High-level install_tool
1751
2049
  # ---------------------------------------------------------------------------
@@ -1768,6 +2066,7 @@ install_tool() {
1768
2066
  _COPY_COUNT_UPTODATE=0
1769
2067
  _COPY_COUNT_UPDATED=0
1770
2068
  _COPY_COUNT_SKIPPED=0
2069
+ _COPY_COUNT_FAILED=0
1771
2070
 
1772
2071
  local root_agent
1773
2072
  root_agent="$(_root_agent_file "$tool")"
@@ -1786,7 +2085,8 @@ install_tool() {
1786
2085
  fi
1787
2086
  ;;
1788
2087
  codex)
1789
- # .codex/ + .agents/ + AGENTS.md
2088
+ # New unified layout: .codex/ only (agents, skills, aid all under .codex/).
2089
+ # .agents/ is the RETIRED split layout -- handled by _migrate_retired_layout.
1790
2090
  if [[ -d "${staging}/.codex" ]]; then
1791
2091
  copy_dir "${staging}/.codex" "${target}/.codex" "$force"
1792
2092
  while IFS= read -r -d '' f; do
@@ -1794,13 +2094,6 @@ install_tool() {
1794
2094
  install_paths+=("$rel")
1795
2095
  done < <(find "${staging}/.codex" -type f -print0 2>/dev/null | sort -z)
1796
2096
  fi
1797
- if [[ -d "${staging}/.agents" ]]; then
1798
- copy_dir "${staging}/.agents" "${target}/.agents" "$force"
1799
- while IFS= read -r -d '' f; do
1800
- local rel="${f#${staging}/}"
1801
- install_paths+=("$rel")
1802
- done < <(find "${staging}/.agents" -type f -print0 2>/dev/null | sort -z)
1803
- fi
1804
2097
  ;;
1805
2098
  cursor)
1806
2099
  # .cursor/ + AGENTS.md
@@ -1834,6 +2127,13 @@ install_tool() {
1834
2127
  ;;
1835
2128
  esac
1836
2129
 
2130
+ # Abort loudly if any AID-owned file failed to copy -- do NOT proceed to
2131
+ # write a manifest that records a partial install as success (work-007 C4).
2132
+ if [[ "${_COPY_COUNT_FAILED:-0}" -gt 0 ]]; then
2133
+ echo "ERROR: aid-install-core: ${_COPY_COUNT_FAILED} file(s) failed to copy for '${tool}'. Install aborted (incomplete)." >&2
2134
+ return 1
2135
+ fi
2136
+
1837
2137
  # Handle root agent file via in-place region update (Pillar 3).
1838
2138
  local root_src="${staging}/${root_agent}"
1839
2139
  local root_dst="${target}/${root_agent}"
@@ -1852,8 +2152,41 @@ install_tool() {
1852
2152
  fi
1853
2153
  fi
1854
2154
 
1855
- # Write manifest (merge).
1856
- manifest_write "$manifest" "$tool" "$version" "install_paths" "root_entries"
2155
+ # Manifest-seam entry gate (PLAN risk #3, delivery-001->002 seam).
2156
+ # Runs BEFORE manifest_write so a contaminated bundle never writes to disk.
2157
+ # Assert that the new bundle's path set does NOT contain any retired roots.
2158
+ # If a retired path leaked into the new manifest, fail loudly -- do not prune
2159
+ # against a contaminated manifest (content-isolation cornerstone).
2160
+ local _retired_roots=(".agents/" ".cursor/rules/" ".agent/rules/")
2161
+ local _rr _leaked
2162
+ for _rr in "${_retired_roots[@]}"; do
2163
+ for _p in "${install_paths[@]+"${install_paths[@]}"}"; do
2164
+ if [[ "$_p" == "${_rr}"* ]]; then
2165
+ _leaked="$_p"
2166
+ break
2167
+ fi
2168
+ done
2169
+ if [[ -n "${_leaked:-}" ]]; then
2170
+ echo "ERROR: aid-install-core: manifest-seam violation: retired root '${_rr}' leaked into the new bundle manifest (path: ${_leaked}). Aborting install to protect user content." >&2
2171
+ return 1
2172
+ fi
2173
+ done
2174
+ unset _rr _leaked
2175
+
2176
+ # Write manifest (merge). A failure here MUST abort loudly (parity with PS
2177
+ # Write-AidManifest, which throws) -- otherwise a failed manifest write is a
2178
+ # silent success and later `aid status`/`aid remove`/prune won't see the tool.
2179
+ if ! manifest_write "$manifest" "$tool" "$version" "install_paths" "root_entries"; then
2180
+ echo "ERROR: aid-install-core: manifest write failed for ${manifest}. Install aborted (files copied but not recorded)." >&2
2181
+ return 1
2182
+ fi
2183
+
2184
+ # Retired-root migration sweep (FR7/FR7a).
2185
+ # Remove AID-owned content from retired layout dirs BEFORE the normal prune,
2186
+ # so that old .agents/, .cursor/rules/, .agent/rules/ trees are cleaned up.
2187
+ # A non-zero rc from install_tool is treated as a mid-commit failure (caller
2188
+ # prints the "re-run to heal" message); this function returns 0 (WARN-not-fail).
2189
+ _migrate_retired_layout "$target" "$tool"
1857
2190
 
1858
2191
  # Prune stale AID-owned files (Pillar 2, R7).
1859
2192
  # Build an associative array from the new manifest path set for O(1) lookup.
@@ -1868,6 +2201,11 @@ install_tool() {
1868
2201
  # Write version marker.
1869
2202
  write_version_marker "$target" "$version"
1870
2203
 
2204
+ # Project-level provisioning (work-007): seed the required settings.yml and
2205
+ # maintain the .gitignore AID region. Both idempotent; safe per-tool.
2206
+ seed_settings_yml "$target" "$tool"
2207
+ update_gitignore "$target"
2208
+
1871
2209
  # Print concise summary (always shown; per-file lines only when AID_VERBOSE=1).
1872
2210
  local _total_files=$((_COPY_COUNT_COPIED + _COPY_COUNT_UPTODATE + _COPY_COUNT_UPDATED + _COPY_COUNT_SKIPPED))
1873
2211
  if [[ "$_total_files" -gt 0 ]]; then
@@ -1890,6 +2228,13 @@ install_tool() {
1890
2228
  fi
1891
2229
  fi
1892
2230
 
2231
+ # Report project-level provisioning actions (work-007; always shown).
2232
+ [[ "${_CORE_SEEDED_SETTINGS:-0}" -eq 1 ]] && echo " Created .aid/settings.yml from template"
2233
+ case "${_CORE_GITIGNORE_ACTION:-unchanged}" in
2234
+ created) echo " Created .gitignore with AID exclusions" ;;
2235
+ updated) echo " Updated .gitignore AID exclusions" ;;
2236
+ esac
2237
+
1893
2238
  return 0
1894
2239
  }
1895
2240
 
@@ -1986,14 +2331,17 @@ uninstall_tool() {
1986
2331
  # Remove this tool from manifest.
1987
2332
  manifest_remove_tool "$manifest" "$tool"
1988
2333
 
1989
- # If no manifest remains, remove the .aid version marker too.
2334
+ # If no manifest remains (last tool removed), remove the AID-provisioned
2335
+ # project files so a full uninstall leaves .aid/ clean (work-007).
1990
2336
  if [[ ! -f "$manifest" ]]; then
1991
- local version_marker
1992
- version_marker="$(dirname "$manifest")/.aid-version"
1993
- rm -f "$version_marker"
1994
- # Remove .aid dir if empty.
1995
2337
  local aid_meta_dir
1996
2338
  aid_meta_dir="$(dirname "$manifest")"
2339
+ rm -f "${aid_meta_dir}/.aid-version"
2340
+ # Remove the install-time-seeded settings.yml (symmetric with seed_settings_yml).
2341
+ # Only fires when NO tools remain -- a partial uninstall keeps settings.yml
2342
+ # for the remaining tools.
2343
+ rm -f "${aid_meta_dir}/settings.yml"
2344
+ # Remove .aid dir if empty.
1997
2345
  if [[ -d "$aid_meta_dir" ]]; then
1998
2346
  local rem
1999
2347
  rem="$(find "$aid_meta_dir" -type f 2>/dev/null | head -1)"
package/package.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "name": "aid-installer",
3
- "version": "1.1.1",
4
- "description": "AID CLI installer - puts the persistent aid command on PATH via npm",
3
+ "version": "2.0.1",
4
+ "description": "AID (AI Integrated Development) CLI installer - puts the persistent aid command on PATH via npm",
5
+ "author": "Andre Vianna",
6
+ "homepage": "https://aid.casuloailabs.com",
5
7
  "bin": {
6
8
  "aid": "bin/aid.js"
7
9
  },
@@ -18,18 +20,19 @@
18
20
  "bin/",
19
21
  "lib/",
20
22
  "dashboard/",
21
- "scripts/",
23
+ "scripts/postinstall.js",
22
24
  "VERSION",
23
25
  "README.md",
24
26
  "LICENSE"
25
27
  ],
26
28
  "keywords": [
27
29
  "aid",
28
- "agentic",
29
- "iterative",
30
+ "AI",
31
+ "integrated",
30
32
  "development",
31
33
  "cli",
32
- "installer"
34
+ "installer",
35
+ "methodology"
33
36
  ],
34
37
  "license": "MIT",
35
38
  "repository": {