@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.7 → 0.1.9
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/adl +2 -2
- package/bin/git +89 -8
- package/bin/lean-ctx-observe-strip +76 -3
- package/bin/reconcile +175 -5
- package/bin/validate-manifest +19 -1
- package/package.json +1 -1
- package/packages/agent-hooks/src/deploy/deploy.ts +98 -18
- package/packages/agent-hooks/src/deploy/doctor.ts +61 -11
- package/packages/agent-hooks/src/deploy/json-merge.ts +430 -18
- package/packages/agent-hooks/src/deploy/lock.ts +36 -2
- package/packages/agent-hooks/src/lean-ctx/env.ts +17 -0
- package/packages/agent-hooks/src/lean-ctx/mcp.ts +4 -2
- package/packages/agent-settings/assets/lean-ctx/config.toml +3 -0
- package/packages/agent-settings/assets/lean-ctx/gitignore +4 -0
- package/packages/agent-settings/assets/lean-ctx/layout.toml +2 -0
- package/packages/agent-settings/src/deploy/agents-deploy.ts +10 -5
- package/packages/agent-settings/src/deploy/doctor.ts +39 -3
- package/packages/agent-settings/src/deploy/lean-ctx-deploy.ts +104 -12
- package/packages/agent-settings/src/deploy/lean-ctx-merge.ts +580 -0
- package/packages/agent-settings/src/deploy/lock.ts +4 -1
- package/packages/agent-settings/src/deploy/mcp-deploy.ts +9 -6
- package/packages/agent-settings/src/deploy/settings.ts +6 -3
- package/packages/agent-skills/dist/deploy.d.ts +2 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +15 -8
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/dist/manifest.d.ts +1 -0
- package/packages/agent-skills/dist/manifest.d.ts.map +1 -1
- package/packages/agent-skills/dist/manifest.js +6 -2
- package/packages/agent-skills/dist/manifest.js.map +1 -1
- package/packages/agent-skills/src/deploy.ts +15 -8
- package/packages/agent-skills/src/manifest.ts +5 -2
- package/packages/azdo/src/azdo-repo-doctor.ts +412 -95
- package/packages/azdo/src/lib/graph-identity.ts +190 -3
- package/packages/fleet/jobs/git-maintain/src/main.ts +305 -45
- package/packages/fleet/manifest.json +2 -0
- package/packages/fleet/manifest.schema.json +9 -0
- package/packages/repo-controls/README.md +13 -6
- package/packages/repo-controls/assets/adl-config.default.toml +1 -0
- package/packages/repo-controls/assets/version-bump.sh +6 -1
- package/packages/repo-controls/src/cli.ts +6 -1
- package/packages/repo-controls/src/install.ts +88 -6
package/bin/adl
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/bin/adl
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 82920928 bytes
|
|
4
|
+
sha256: 74d2d0af04a268afff30746e0d68d9d49d8e92c347e913330393c90362c99aff
|
package/bin/git
CHANGED
|
@@ -621,8 +621,76 @@ case "$command_name" in
|
|
|
621
621
|
*) exec "$real_git" ${args[@]+"${args[@]}"} ;;
|
|
622
622
|
esac
|
|
623
623
|
|
|
624
|
+
# Whether option ARG, for network subcommand SUB, consumes the NEXT argv
|
|
625
|
+
# element as its value. Incident 2026-09-30 (Azure DevOps push 16505 recorded
|
|
626
|
+
# as the HUMAN "Tarun Rana" for an agent session's `git push -u origin
|
|
627
|
+
# feat/adl-install`): one shared value-option list treated `-u` as taking a
|
|
628
|
+
# value for EVERY subcommand. It only does for ls-remote/clone
|
|
629
|
+
# (`--upload-pack <exec>`); for push it is `--set-upstream` and for fetch/pull
|
|
630
|
+
# `--update-head-ok`, both flags. `origin` was swallowed as the "value",
|
|
631
|
+
# `feat/adl-install` became the "remote", `remote get-url` failed, and the
|
|
632
|
+
# push went to plain git and the human's credential helper. The table is
|
|
633
|
+
# therefore PER SUBCOMMAND (verified against git-push/fetch/pull/ls-remote/
|
|
634
|
+
# clone(1)). Options whose argument is optional (`--force-with-lease`,
|
|
635
|
+
# `--recurse-submodules` on fetch/pull/clone, `-S`, `-r`) only take it
|
|
636
|
+
# attached (`--opt=value`) and so never consume the next element.
|
|
637
|
+
network_option_takes_value() {
|
|
638
|
+
local sub="$1" arg="$2" short_values long_values chars i ch
|
|
639
|
+
local fetch_long=' depth deepen shallow-since shallow-exclude negotiation-tip negotiation-restrict negotiation-include filter refmap jobs server-option upload-pack exec submodule-prefix recurse-submodules-default bundle-uri '
|
|
640
|
+
case "$sub" in
|
|
641
|
+
push) short_values='o'; long_values=' push-option receive-pack exec repo recurse-submodules ' ;;
|
|
642
|
+
fetch) short_values='jo'; long_values="$fetch_long" ;;
|
|
643
|
+
pull) short_values='josX'; long_values="$fetch_long strategy strategy-option cleanup into-name " ;;
|
|
644
|
+
ls-remote) short_values='ou'; long_values=' upload-pack exec server-option sort ' ;;
|
|
645
|
+
clone) short_values='obucj'; long_values=' origin branch upload-pack template config separate-git-dir ref-format jobs bundle-uri depth shallow-since shallow-exclude reference reference-if-able server-option filter revision object-format ' ;;
|
|
646
|
+
*) return 1 ;;
|
|
647
|
+
esac
|
|
648
|
+
case "$arg" in
|
|
649
|
+
--|--*=*) return 1 ;;
|
|
650
|
+
--*) [[ "$long_values" == *" ${arg#--} "* ]] ;;
|
|
651
|
+
-?) [[ "$short_values" == *"${arg#-}"* ]] ;;
|
|
652
|
+
-*)
|
|
653
|
+
# Clustered short options (`-uf`, `-fo x`): the first value-taking
|
|
654
|
+
# letter ends the cluster; it consumes the next element only when it is
|
|
655
|
+
# the LAST letter (otherwise the rest of the cluster is its value).
|
|
656
|
+
chars="${arg#-}"
|
|
657
|
+
for ((i = 0; i < ${#chars}; i += 1)); do
|
|
658
|
+
ch="${chars:i:1}"
|
|
659
|
+
if [[ "$short_values" == *"$ch"* ]]; then
|
|
660
|
+
((i == ${#chars} - 1)) && return 0
|
|
661
|
+
return 1
|
|
662
|
+
fi
|
|
663
|
+
done
|
|
664
|
+
return 1
|
|
665
|
+
;;
|
|
666
|
+
*) return 1 ;;
|
|
667
|
+
esac
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
# True when OPERAND (an unresolved would-be remote name) is really a local
|
|
671
|
+
# path or a URL, which plain git handles without any named remote: an
|
|
672
|
+
# absolute/relative/home path, an existing path (relative to any leading
|
|
673
|
+
# `-C <dir>`), a `scheme://` URL, or scp-like `host:path`.
|
|
674
|
+
is_path_or_url_operand() {
|
|
675
|
+
local operand="$1" base='' i
|
|
676
|
+
case "$operand" in
|
|
677
|
+
/*|./*|../*|~*|.|..|*://*) return 0 ;;
|
|
678
|
+
esac
|
|
679
|
+
is_scp_like_url "$operand" && return 0
|
|
680
|
+
for ((i = 0; i < command_index; i += 1)); do
|
|
681
|
+
if [[ "${args[i]}" == -C && $((i + 1)) -lt command_index ]]; then
|
|
682
|
+
case "${args[i + 1]}" in
|
|
683
|
+
/*) base="${args[i + 1]}" ;;
|
|
684
|
+
*) base="${base:+$base/}${args[i + 1]}" ;;
|
|
685
|
+
esac
|
|
686
|
+
fi
|
|
687
|
+
done
|
|
688
|
+
[[ -e "${base:+$base/}$operand" ]]
|
|
689
|
+
}
|
|
690
|
+
|
|
624
691
|
remote_url=''
|
|
625
692
|
remote_name='origin'
|
|
693
|
+
remote_explicit=0
|
|
626
694
|
|
|
627
695
|
# A direct URL is authoritative for every network command. Scan it before
|
|
628
696
|
# resolving a remote name so clone, ls-remote, and explicit fetch/push/pull URLs never
|
|
@@ -635,24 +703,27 @@ for ((index = command_index + 1; index < ${#args[@]}; index += 1)); do
|
|
|
635
703
|
done
|
|
636
704
|
|
|
637
705
|
if [[ -z "$remote_url" ]]; then
|
|
638
|
-
# The first positional argument to fetch, pull, push, and ls-remote is the
|
|
639
|
-
# remote. Skip the
|
|
640
|
-
#
|
|
706
|
+
# The first positional argument to fetch, pull, push, and ls-remote is the
|
|
707
|
+
# selected remote. Skip options (and the value of those that take one FOR
|
|
708
|
+
# THIS SUBCOMMAND, see network_option_takes_value); other options are
|
|
709
|
+
# skipped rather than treated as a remote name.
|
|
641
710
|
for ((index = command_index + 1; index < ${#args[@]}; index += 1)); do
|
|
642
711
|
argument="${args[index]}"
|
|
643
712
|
case "$argument" in
|
|
644
713
|
--)
|
|
645
714
|
((index += 1))
|
|
646
|
-
((index < ${#args[@]}))
|
|
715
|
+
if ((index < ${#args[@]})); then
|
|
716
|
+
remote_name="${args[index]}"
|
|
717
|
+
remote_explicit=1
|
|
718
|
+
fi
|
|
647
719
|
break
|
|
648
720
|
;;
|
|
649
|
-
|
|
650
|
-
((index += 1))
|
|
651
|
-
;;
|
|
652
|
-
-?*|--*=*)
|
|
721
|
+
-?*)
|
|
722
|
+
network_option_takes_value "$command_name" "$argument" && ((index += 1))
|
|
653
723
|
;;
|
|
654
724
|
*)
|
|
655
725
|
remote_name="$argument"
|
|
726
|
+
remote_explicit=1
|
|
656
727
|
break
|
|
657
728
|
;;
|
|
658
729
|
esac
|
|
@@ -661,6 +732,16 @@ fi
|
|
|
661
732
|
|
|
662
733
|
if [[ -z "$remote_url" ]]; then
|
|
663
734
|
if ! remote_url="$("$real_git" "${args[@]:0:command_index}" remote get-url "$remote_name" 2>/dev/null)"; then
|
|
735
|
+
# FAIL CLOSED (incident 2026-09-30, above): an EXPLICITLY named remote
|
|
736
|
+
# that does not resolve, and is not a local path or a URL, must never be
|
|
737
|
+
# handed to plain git -- it would authenticate as the human via the
|
|
738
|
+
# keychain/credential helper if the name was really an Azure DevOps
|
|
739
|
+
# remote we mis-scanned. clone has no remote name (its operand is the
|
|
740
|
+
# source), and an unnamed default remote keeps today's pass-through.
|
|
741
|
+
if [[ "$command_name" != clone && "$remote_explicit" == 1 ]] && ! is_path_or_url_operand "$remote_name"; then
|
|
742
|
+
printf "git: cannot resolve remote '%s' for %s; refusing to pass an unresolved remote to plain git (it may authenticate as the human)\n" "$remote_name" "$command_name" >&2
|
|
743
|
+
exit 1
|
|
744
|
+
fi
|
|
664
745
|
exec "$real_git" ${args[@]+"${args[@]}"}
|
|
665
746
|
fi
|
|
666
747
|
fi
|
|
@@ -10,7 +10,7 @@ export PATH="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/us
|
|
|
10
10
|
# the per-repo carrier (`adl hook lean-ctx pre-tool-use|post-tool-use`,
|
|
11
11
|
# written by `adl hooks deploy`) already provides rewrite/redirect/
|
|
12
12
|
# read-dedup with the repo's own config, and `observe` has no per-repo
|
|
13
|
-
# carrier at all and is dropped outright.
|
|
13
|
+
# carrier at all and is dropped outright. Six carriers, all written by
|
|
14
14
|
# `lean-ctx setup`, are covered ("fix all the lean-ctx issues" -- Tarun,
|
|
15
15
|
# 2026-09-29 -- widened this from an initial two-of-five scope):
|
|
16
16
|
# 1. Raw, unwrapped `lean-ctx hook <subcommand>` entries -- observe,
|
|
@@ -33,6 +33,20 @@ export PATH="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/us
|
|
|
33
33
|
# at $HOME/.claude/skills/lean-ctx, .codex/skills/lean-ctx,
|
|
34
34
|
# .cursor/skills/lean-ctx, .config/opencode/skills/lean-ctx,
|
|
35
35
|
# .agents/skills/lean-ctx.
|
|
36
|
+
# 6. Global lean-ctx hook SCRIPTS: every entry named `lean-ctx-*` directly
|
|
37
|
+
# under $HOME/.claude/hooks (lean-ctx-rewrite.sh, lean-ctx-redirect.sh,
|
|
38
|
+
# their -native counterparts, and any .bak copies). Added 2026-09-30
|
|
39
|
+
# because of lean-ctx's own startup refresh gate (proven from v3.10.5
|
|
40
|
+
# source, rust/src/hooks/mod.rs): refresh_installed_hooks() re-writes
|
|
41
|
+
# the global Claude hooks whenever hooks_installed_for("claude", home)
|
|
42
|
+
# is true, i.e. <claude_dir>/hooks/lean-ctx-rewrite.sh EXISTS or
|
|
43
|
+
# <claude_dir>/settings.json contains the substring `lean-ctx`.
|
|
44
|
+
# Stripping settings.json (shape 1) alone leaves the scripts behind and
|
|
45
|
+
# the gate armed, so the carrier rewrites the global hooks on its next
|
|
46
|
+
# start. Only names starting with `lean-ctx-` are touched; every other
|
|
47
|
+
# file there (enact-*, code-simplifier.mjs, lib/, symlinks to other
|
|
48
|
+
# repos) is left alone. Scope is $HOME/.claude only -- CLAUDE_CONFIG_DIR
|
|
49
|
+
# is deliberately not consulted (the tool takes --home explicitly).
|
|
36
50
|
# `adl hook lean-ctx ...` (the per-repo carrier) and Orca's own hooks are
|
|
37
51
|
# never touched -- see bin/lib/lean-ctx-observe-hooks.sh's own header for
|
|
38
52
|
# exactly why the match can never catch either, and for the full "which
|
|
@@ -42,7 +56,7 @@ export PATH="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/us
|
|
|
42
56
|
# that same header, not stripped. Run by bin/reconcile on every
|
|
43
57
|
# reconciliation (plan AND apply, exactly like bin/kimi-hooks /
|
|
44
58
|
# bin/opencode-hooks) so a hand-edited or Orca-reinstalled per-user config
|
|
45
|
-
# never lets any of the
|
|
59
|
+
# never lets any of the six carriers creep back in unnoticed.
|
|
46
60
|
#
|
|
47
61
|
# Usage: lean-ctx-observe-strip --home ABSOLUTE_PATH --mode plan|apply
|
|
48
62
|
#
|
|
@@ -66,7 +80,11 @@ export PATH="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/us
|
|
|
66
80
|
# not exist, is reported and left alone. A skill directory is
|
|
67
81
|
# moved (never deleted) only when its SKILL.md frontmatter says
|
|
68
82
|
# `name: lean-ctx`; any other content there is left alone and
|
|
69
|
-
# reported.
|
|
83
|
+
# reported. The `lean-ctx-*` hook scripts under ~/.claude/hooks are
|
|
84
|
+
# moved (never deleted) into a sibling
|
|
85
|
+
# `hooks.pre-lean-ctx-observe-strip-<UTC ts>/` directory OUTSIDE
|
|
86
|
+
# the scanned hooks/ directory, exactly like a skill directory's
|
|
87
|
+
# backup, and reported one line per file. Refuses outright (before touching anything) when run
|
|
70
88
|
# as literal root (EUID 0) against a --home it does not own --
|
|
71
89
|
# bin/reconcile's apply path never does this itself; see
|
|
72
90
|
# lean_ctx_observe_strip_guard_euid in the shared lib.
|
|
@@ -348,6 +366,60 @@ strip_skill_dir_one() {
|
|
|
348
366
|
return 0
|
|
349
367
|
}
|
|
350
368
|
|
|
369
|
+
# strip_hook_scripts_one <hooks-dir> -- shape 6 (see the header): moves every
|
|
370
|
+
# entry named `lean-ctx-*` directly under <hooks-dir> into a sibling
|
|
371
|
+
# `<hooks-dir>.pre-lean-ctx-observe-strip-<UTC ts>/` directory (OUTSIDE the
|
|
372
|
+
# scanned hooks/ directory, same reasoning as strip_skill_dir_one), so
|
|
373
|
+
# lean-ctx's refresh gate (`hooks/lean-ctx-rewrite.sh` exists) is disarmed.
|
|
374
|
+
# Matches by name prefix only, never by content; a symlink named
|
|
375
|
+
# `lean-ctx-*` is moved as the link itself (mv never follows it). Nothing
|
|
376
|
+
# else in <hooks-dir> is read or touched. Each file is reported as
|
|
377
|
+
# "backed up previous hook script <original> to <backup>" (two explicit
|
|
378
|
+
# paths, like the skill-directory note -- the backup path does not end in
|
|
379
|
+
# `.pre-<label>-<timestamp>`, so bin/reconcile cannot recover the original
|
|
380
|
+
# by suffix-stripping).
|
|
381
|
+
strip_hook_scripts_one() {
|
|
382
|
+
local hooks_dir="$1" entry
|
|
383
|
+
local found=()
|
|
384
|
+
if [[ -d "$hooks_dir" ]]; then
|
|
385
|
+
for entry in "$hooks_dir"/lean-ctx-*; do
|
|
386
|
+
if [[ -e "$entry" || -L "$entry" ]]; then
|
|
387
|
+
found+=("$entry")
|
|
388
|
+
fi
|
|
389
|
+
done
|
|
390
|
+
fi
|
|
391
|
+
if [[ "${#found[@]}" -eq 0 ]]; then
|
|
392
|
+
echo "lean-ctx-observe-strip: $hooks_dir/lean-ctx-*: not present -- nothing to strip"
|
|
393
|
+
return 0
|
|
394
|
+
fi
|
|
395
|
+
for entry in "${found[@]}"; do
|
|
396
|
+
echo "lean-ctx-observe-strip: $entry: found lean-ctx hook script"
|
|
397
|
+
if [[ "$mode" != apply ]]; then
|
|
398
|
+
echo "lean-ctx-observe-strip: would move $entry to a timestamped backup location outside $hooks_dir"
|
|
399
|
+
fi
|
|
400
|
+
done
|
|
401
|
+
[[ "$mode" == apply ]] || return 0
|
|
402
|
+
|
|
403
|
+
local backup_root
|
|
404
|
+
backup_root="${hooks_dir}.pre-lean-ctx-observe-strip-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
405
|
+
if ! mkdir "$backup_root" 2>/dev/null; then
|
|
406
|
+
echo "lean-ctx-observe-strip: $hooks_dir: FAILED to create backup location $backup_root -- not modified" >&2
|
|
407
|
+
return 1
|
|
408
|
+
fi
|
|
409
|
+
local backup rc=0
|
|
410
|
+
for entry in "${found[@]}"; do
|
|
411
|
+
backup="$backup_root/$(basename "$entry")"
|
|
412
|
+
if ! mv "$entry" "$backup"; then
|
|
413
|
+
echo "lean-ctx-observe-strip: $entry: FAILED to move to $backup -- not modified" >&2
|
|
414
|
+
rc=1
|
|
415
|
+
continue
|
|
416
|
+
fi
|
|
417
|
+
echo "lean-ctx-observe-strip: backed up previous hook script $entry to $backup"
|
|
418
|
+
echo "lean-ctx-observe-strip: moved lean-ctx hook script $entry to $backup"
|
|
419
|
+
done
|
|
420
|
+
return "$rc"
|
|
421
|
+
}
|
|
422
|
+
|
|
351
423
|
status=0
|
|
352
424
|
for target in "${targets[@]}"; do
|
|
353
425
|
strip_one "$target" || status=1
|
|
@@ -358,4 +430,5 @@ done
|
|
|
358
430
|
for target in "${skill_dir_targets[@]}"; do
|
|
359
431
|
strip_skill_dir_one "$target" || status=1
|
|
360
432
|
done
|
|
433
|
+
strip_hook_scripts_one "$home/.claude/hooks" || status=1
|
|
361
434
|
exit "$status"
|
package/bin/reconcile
CHANGED
|
@@ -180,6 +180,12 @@ collect_lean_ctx_observe_strip_backups() {
|
|
|
180
180
|
original="${BASH_REMATCH[1]}"
|
|
181
181
|
backup="${BASH_REMATCH[2]}"
|
|
182
182
|
record_backup "$original" "$backup"
|
|
183
|
+
elif [[ "$line" =~ ^lean-ctx-observe-strip:\ backed\ up\ previous\ hook\ script\ (.+)\ to\ (.+)$ ]]; then
|
|
184
|
+
# ~/.claude/hooks/lean-ctx-* scripts: same two-path shape as a skill
|
|
185
|
+
# directory (sibling "hooks.pre-lean-ctx-observe-strip-<ts>/" backup).
|
|
186
|
+
original="${BASH_REMATCH[1]}"
|
|
187
|
+
backup="${BASH_REMATCH[2]}"
|
|
188
|
+
record_backup "$original" "$backup"
|
|
183
189
|
fi
|
|
184
190
|
done <<<"$text"
|
|
185
191
|
}
|
|
@@ -1979,6 +1985,37 @@ run_surface_update() {
|
|
|
1979
1985
|
sudo -u "$administrator" -H env PATH="$surface_cli_search_path" "$resolved" "$@" </dev/null
|
|
1980
1986
|
}
|
|
1981
1987
|
|
|
1988
|
+
# Extracts an update command's own reported failure reason from its
|
|
1989
|
+
# captured output, or prints nothing (empty) if none is found. Confirmed
|
|
1990
|
+
# live for kimi on strato and hostinger-vps-1 (2026-09-30): `kimi upgrade`
|
|
1991
|
+
# printed "error: failed to check for updates: CDN /latest returned HTTP
|
|
1992
|
+
# 521" and still exited 0. Broadened by independent review the same day:
|
|
1993
|
+
# optional leading whitespace before the marker, ANSI color codes stripped
|
|
1994
|
+
# first (real CLIs colorize their own output), and any of
|
|
1995
|
+
# "error:"/"Error:"/"ERROR:"/"✖ error:" -- case-insensitive by
|
|
1996
|
+
# construction (POSIX bracket character classes, not a sed `-I`/`I` flag,
|
|
1997
|
+
# which BSD sed does not support). Applied identically to every surface's
|
|
1998
|
+
# update path, native/npm and Homebrew-cask alike, rather than one
|
|
1999
|
+
# per-surface parser.
|
|
2000
|
+
#
|
|
2001
|
+
# Never `cmd1 | cmd2` piped straight into the caller's assignment: see the
|
|
2002
|
+
# inner `if var=$(grep ...); then` below (not `grep | sed`) for why -- under
|
|
2003
|
+
# this file's `set -euo pipefail`, grep's ordinary "no match" exit would
|
|
2004
|
+
# otherwise abort the run on every surface that DIDN'T report an error. An
|
|
2005
|
+
# `if`-tested command is exempt from that abort by design; the function's
|
|
2006
|
+
# OWN exit status is therefore never meaningful (an `if` with no `else`
|
|
2007
|
+
# always returns 0), so callers check the printed string's emptiness, never
|
|
2008
|
+
# this function's exit status.
|
|
2009
|
+
surface_update_error_reason() {
|
|
2010
|
+
local output="$1" plain line
|
|
2011
|
+
# LC_ALL=C: the strip is bytewise. Under en_US.UTF-8 glibc resolves [@-~]
|
|
2012
|
+
# by collation, so the escape's final letter would never match.
|
|
2013
|
+
plain="$(LC_ALL=C sed -E $'s/\x1b\\[[0-9;]*[@-~]//g' <<<"$output")"
|
|
2014
|
+
if line="$(grep -m1 -iE '^[[:space:]]*(✖[[:space:]]*)?error:' <<<"$plain")"; then
|
|
2015
|
+
sed -E 's/^[[:space:]]*(✖[[:space:]]*)?[eE][rR][rR][oO][rR]:[[:space:]]*//' <<<"$line"
|
|
2016
|
+
fi
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1982
2019
|
# Reconciles one declared surface_clis entry. report_only (hostinger's loose
|
|
1983
2020
|
# /root/codex, unknown install method) is version-reported only -- run_surface_update
|
|
1984
2021
|
# is never called for it, by construction (this branch returns before
|
|
@@ -2017,6 +2054,26 @@ reconcile_one_surface_cli() {
|
|
|
2017
2054
|
echo "surface update failed: $name (exit $update_status)" >&2
|
|
2018
2055
|
return 1
|
|
2019
2056
|
fi
|
|
2057
|
+
# Same broadened check as the native/npm path below (independent
|
|
2058
|
+
# review, 2026-09-30): brew itself can exit 0 while its own output
|
|
2059
|
+
# reports a real problem. No install/upgrade claim without checking
|
|
2060
|
+
# for one first -- Homebrew has no version comparison in this branch
|
|
2061
|
+
# (it acts on cask PRESENCE, not a read version), so there is no
|
|
2062
|
+
# "version changed" success case to weigh it against here; any error
|
|
2063
|
+
# line found is a hard failure.
|
|
2064
|
+
local update_error_line
|
|
2065
|
+
update_error_line="$(surface_update_error_reason "$update_output")"
|
|
2066
|
+
if [[ -n "$update_error_line" ]]; then
|
|
2067
|
+
# Install vs upgrade distinguished in the failure wording too (second
|
|
2068
|
+
# coordinator pass, 2026-09-30): the same `cask_action[0]` that already
|
|
2069
|
+
# picks the real brew argv above also names which one failed.
|
|
2070
|
+
if [[ "${cask_action[0]}" == install ]]; then
|
|
2071
|
+
echo "surface $name: cask install failed ($update_error_line)" >&2
|
|
2072
|
+
else
|
|
2073
|
+
echo "surface $name: cask upgrade failed ($update_error_line)" >&2
|
|
2074
|
+
fi
|
|
2075
|
+
return 1
|
|
2076
|
+
fi
|
|
2020
2077
|
if [[ "${cask_action[0]}" == install ]]; then
|
|
2021
2078
|
echo "surface $name: Homebrew cask installed"
|
|
2022
2079
|
else
|
|
@@ -2045,7 +2102,7 @@ reconcile_one_surface_cli() {
|
|
|
2045
2102
|
echo "surface $name: report-only, currently ${ver:-unknown}"
|
|
2046
2103
|
return 0
|
|
2047
2104
|
fi
|
|
2048
|
-
local old_version new_version update_output update_status=0
|
|
2105
|
+
local old_version new_version update_output update_status=0 update_error_line
|
|
2049
2106
|
old_version="$(surface_cli_version "$resolved")"
|
|
2050
2107
|
update_output="$(run_with_timeout "$SURFACE_UPDATE_TIMEOUT_SECONDS" run_surface_update "$resolved" ${update_args[@]+"${update_args[@]}"} 2>&1)" || update_status=$?
|
|
2051
2108
|
printf '%s\n' "$update_output"
|
|
@@ -2056,12 +2113,48 @@ reconcile_one_surface_cli() {
|
|
|
2056
2113
|
echo "surface update failed: $name (exit $update_status)" >&2
|
|
2057
2114
|
return 1
|
|
2058
2115
|
fi
|
|
2116
|
+
# A surface's update command can exit 0 while its own output says the
|
|
2117
|
+
# check itself failed -- confirmed live for kimi (strato + hostinger,
|
|
2118
|
+
# 2026-09-30): `kimi upgrade` printed
|
|
2119
|
+
# "error: failed to check for updates: CDN /latest returned HTTP 521" and
|
|
2120
|
+
# still exited 0 (AGENTS.md: no silent fallback). Read new_version BEFORE
|
|
2121
|
+
# judging anything (independent review, 2026-09-30): a version CHANGE is
|
|
2122
|
+
# proof positive the update happened, which outranks an error line the
|
|
2123
|
+
# command also printed (e.g. a flaky post-install self-check after a
|
|
2124
|
+
# real, successful install) -- reported as a note on the success line,
|
|
2125
|
+
# never a failure. Only an UNCHANGED version with an error line is the
|
|
2126
|
+
# case this whole check exists for: the command claimed nothing changed,
|
|
2127
|
+
# and said why, and that is reported as a failure -- wording "update
|
|
2128
|
+
# check failed", not "could not check for updates", since the reported
|
|
2129
|
+
# reason may not even be about the check itself (kept generic rather than
|
|
2130
|
+
# asserting which).
|
|
2131
|
+
#
|
|
2132
|
+
# An EMPTY new_version (second coordinator pass, 2026-09-30: the binary
|
|
2133
|
+
# broke, or --version now produces no output) must never count as
|
|
2134
|
+
# "updated" just because empty differs from a non-empty old_version --
|
|
2135
|
+
# that used to read as "surface X: 1.2.3 -> unknown", an arrow-style
|
|
2136
|
+
# SUCCESS line for a version we cannot actually confirm. Checked before
|
|
2137
|
+
# the "changed" branch below, so only a REAL, non-empty, different
|
|
2138
|
+
# version ever reaches it.
|
|
2059
2139
|
new_version="$(surface_cli_version "$resolved")"
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2140
|
+
update_error_line="$(surface_update_error_reason "$update_output")"
|
|
2141
|
+
if [[ -z "$new_version" ]]; then
|
|
2142
|
+
echo "surface $name: update ran but version unreadable (${update_error_line:-"--version produced no output"}); was ${old_version:-unknown}" >&2
|
|
2143
|
+
return 1
|
|
2144
|
+
fi
|
|
2145
|
+
if [[ "$old_version" != "$new_version" ]]; then
|
|
2146
|
+
if [[ -n "$update_error_line" ]]; then
|
|
2147
|
+
echo "surface $name: updated ${old_version:-unknown} -> $new_version (reported: $update_error_line)"
|
|
2148
|
+
else
|
|
2149
|
+
echo "surface $name: ${old_version:-unknown} -> $new_version"
|
|
2150
|
+
fi
|
|
2151
|
+
return 0
|
|
2064
2152
|
fi
|
|
2153
|
+
if [[ -n "$update_error_line" ]]; then
|
|
2154
|
+
echo "surface $name: update check failed ($update_error_line); kept ${old_version:-unknown}" >&2
|
|
2155
|
+
return 1
|
|
2156
|
+
fi
|
|
2157
|
+
echo "surface $name: already latest ($new_version)"
|
|
2065
2158
|
return 0
|
|
2066
2159
|
}
|
|
2067
2160
|
|
|
@@ -2692,6 +2785,83 @@ else
|
|
|
2692
2785
|
echo "global-tools: $mode complete; no state changed"
|
|
2693
2786
|
fi
|
|
2694
2787
|
|
|
2788
|
+
# git_maintain_repos (2026-09-30, arm-only decision the same day): an
|
|
2789
|
+
# explicit, closed list of repositories this device's own checkout lives
|
|
2790
|
+
# at OUTSIDE any recursively-scanned home directory -- the Linux VPSes'
|
|
2791
|
+
# root-owned /opt/adl-azdo is the confirmed case for both strato-vps-1 and
|
|
2792
|
+
# hostinger-vps-1 (STATUS.md's 2026-09-29 first-deployment record;
|
|
2793
|
+
# hostinger's own fleet-cutover --apply landed 2026-09-29T23:43Z, confirmed
|
|
2794
|
+
# the same day). git-maintain's own scheduling (packages/fleet/src/
|
|
2795
|
+
# launch-agents.ts: a dedicated compiled binary, ad-hoc signed, run by a
|
|
2796
|
+
# launchd LaunchAgent under TCC) is macOS-only by construction, so it never
|
|
2797
|
+
# reaches these checkouts on its own. This runs the job directly, on
|
|
2798
|
+
# reconcile's own schedule (every platform this script runs on already has
|
|
2799
|
+
# one), passing each declared path as `--repo` argv -- never a recursive
|
|
2800
|
+
# scan of a shared parent like `/opt` (which also holds unrelated,
|
|
2801
|
+
# explicitly off-limits directories), and never a new environment
|
|
2802
|
+
# variable: the list itself comes only from the manifest, and
|
|
2803
|
+
# GIT_MAINTAIN_DRY below is this job's own pre-existing dry/real contract,
|
|
2804
|
+
# driven by reconcile's own --mode exactly like every other step in this
|
|
2805
|
+
# file, not a knob an operator sets by hand. Absent or empty (every mac
|
|
2806
|
+
# device today) is a silent no-op, the same convention
|
|
2807
|
+
# launch_agents/scratch_repos/surface_clis already use.
|
|
2808
|
+
#
|
|
2809
|
+
# ARM-ONLY, always, on this path (independent review, 2026-09-30): the full
|
|
2810
|
+
# job on this SAME checkout `bin/reconcile` runs from turned out to be new,
|
|
2811
|
+
# unapproved, root-run behaviour once actually reasoned through, not merely
|
|
2812
|
+
# a self-checkout read/write ordering hazard -- see the `--arm-only`
|
|
2813
|
+
# decision recorded in jobs/git-maintain/src/main.ts's own header comment
|
|
2814
|
+
# for the full list (a silent fast-forward-driven auto-deploy of
|
|
2815
|
+
# `origin/integration`/main with no approval gate, colliding with
|
|
2816
|
+
# fleet-cutover.sh re-reading fleet-retire-list.json from this same
|
|
2817
|
+
# checkout after reconcile runs, `git branch -D` as root, and a fetch that
|
|
2818
|
+
# still runs even in dry/plan/audit mode and exits 0 on its own failure).
|
|
2819
|
+
# `--arm-only` narrows this call to ONLY `armHooks`: no fetch, no
|
|
2820
|
+
# fast-forward, no branch prune, no bundle-refresh installer, no network,
|
|
2821
|
+
# and no Azure DevOps identity resolution. The only remaining write is
|
|
2822
|
+
# `.git/config`'s `core.hooksPath`, real only in apply mode
|
|
2823
|
+
# (GIT_MAINTAIN_DRY=1 below makes it a `[dry] would arm ...` log line and
|
|
2824
|
+
# nothing else, in both plan AND audit mode). That is never a tracked file
|
|
2825
|
+
# this script (or anything downstream of it) reads, so arm-only has no
|
|
2826
|
+
# self-checkout read/write ordering hazard left to avoid -- this step stays
|
|
2827
|
+
# in this late position anyway, unchanged from the full-job version: it is
|
|
2828
|
+
# still the simplest place for it, and there is no reason to special-case
|
|
2829
|
+
# arm-only's position just because its own hazard is gone.
|
|
2830
|
+
git_maintain_repos=()
|
|
2831
|
+
while IFS= read -r repo; do
|
|
2832
|
+
[[ -n "$repo" ]] && git_maintain_repos+=("$repo")
|
|
2833
|
+
done < <(jq -r --arg platform "$platform" --arg device "$device" \
|
|
2834
|
+
'.[$platform].devices[$device].git_maintain_repos // [] | .[]' "$manifest")
|
|
2835
|
+
if [[ "${#git_maintain_repos[@]}" -gt 0 ]]; then
|
|
2836
|
+
git_maintain_repo_args=(--arm-only)
|
|
2837
|
+
for repo in "${git_maintain_repos[@]}"; do
|
|
2838
|
+
git_maintain_repo_args+=(--repo "$repo")
|
|
2839
|
+
done
|
|
2840
|
+
# Unlike default-agent-file.ts/managed-settings.ts's plan branches earlier
|
|
2841
|
+
# in this file (which never fail, by construction, so they run
|
|
2842
|
+
# unguarded), this job's dry pass still validates each declared path is a
|
|
2843
|
+
# real repo -- a genuine failure mode even in a non-apply mode, e.g. a
|
|
2844
|
+
# declared checkout that has not been cloned yet. So every non-apply mode
|
|
2845
|
+
# is guarded exactly like apply mode here, deliberately deviating from
|
|
2846
|
+
# those sibling steps: report it, collect it, never let it abort the rest
|
|
2847
|
+
# of this script under this file's own `set -e`. `$mode` (plan OR audit)
|
|
2848
|
+
# is reported verbatim -- audit is a real, distinct reconcile mode, never
|
|
2849
|
+
# relabeled "plan".
|
|
2850
|
+
if [[ "$mode" != apply ]]; then
|
|
2851
|
+
echo "global-tools: git-maintain (declared repos, arm-only) mode=$mode device=$device -> ${git_maintain_repos[*]}"
|
|
2852
|
+
if ! GIT_MAINTAIN_DRY=1 bun "$repo_root/packages/fleet/jobs/git-maintain/src/main.ts" "${git_maintain_repo_args[@]}"; then
|
|
2853
|
+
echo "global-tools: TOOL INSTALL FAILED: git-maintain-repos" >&2
|
|
2854
|
+
failed_tools+=("git-maintain-repos")
|
|
2855
|
+
fi
|
|
2856
|
+
else
|
|
2857
|
+
echo "global-tools: git-maintain (declared repos, arm-only) mode=apply device=$device -> ${git_maintain_repos[*]}"
|
|
2858
|
+
if ! bun "$repo_root/packages/fleet/jobs/git-maintain/src/main.ts" "${git_maintain_repo_args[@]}"; then
|
|
2859
|
+
echo "global-tools: TOOL INSTALL FAILED: git-maintain-repos" >&2
|
|
2860
|
+
failed_tools+=("git-maintain-repos")
|
|
2861
|
+
fi
|
|
2862
|
+
fi
|
|
2863
|
+
fi
|
|
2864
|
+
|
|
2695
2865
|
# A recorded, continued-past tool failure (install_one_tool /
|
|
2696
2866
|
# build_one_bun_tool / install_one_embedding_model_tool / write_one_shim)
|
|
2697
2867
|
# still lets every post-pass above run -- sweep, kimi-hooks, global-agents,
|
package/bin/validate-manifest
CHANGED
|
@@ -43,10 +43,28 @@ if ! jq -e '
|
|
|
43
43
|
all(.minimum_free_gib >= 1) and
|
|
44
44
|
all((.home | type) == "string" and (.home | startswith("/"))) and
|
|
45
45
|
all((.default_agent | type) == "string" and (.default_agent | test("^agent-[a-z0-9-]+$"))) and
|
|
46
|
-
all((keys - ["administrator", "default_agent", "home", "launch_agents", "minimum_free_gib", "schedule_minute", "scratch_repos", "brew_upgrade_all", "surface_clis", "tools"] | length) == 0) and
|
|
46
|
+
all((keys - ["administrator", "default_agent", "home", "launch_agents", "minimum_free_gib", "schedule_minute", "scratch_repos", "brew_upgrade_all", "surface_clis", "git_maintain_repos", "tools"] | length) == 0) and
|
|
47
47
|
all((.scratch_repos // []) | type == "array" and (. | unique | length) == length and
|
|
48
48
|
all(type == "string" and startswith("/") and . != "/" and
|
|
49
49
|
(endswith("/") | not) and (contains("..") | not) and (test("\\s") | not))) and
|
|
50
|
+
# git_maintain_repos (2026-09-30): an explicit, closed list of
|
|
51
|
+
# repositories bin/reconcile passes to git-maintain via --repo argv --
|
|
52
|
+
# never a recursive scan root, deliberately, since these devices own
|
|
53
|
+
# checkouts that sit at a fixed path outside any device home (the Linux
|
|
54
|
+
# VPSes use /opt/adl-azdo) and a recursive scan of a shared parent like
|
|
55
|
+
# /opt would also walk into unrelated, explicitly off-limits
|
|
56
|
+
# directories. The azagent/vsts.agent substring bans are exactly that
|
|
57
|
+
# guard, enforced at the schema layer (case-insensitively -- a manifest
|
|
58
|
+
# value could be typed AzAgent2 as easily as azagent2) so a mistyped or
|
|
59
|
+
# copy-pasted entry can never point there. git-maintain itself refuses
|
|
60
|
+
# the same names again, case-insensitively and by realpath too, at the
|
|
61
|
+
# point of actual use (jobs/git-maintain/src/main.ts,
|
|
62
|
+
# isForbiddenAzurePipelinesPath) -- this schema check alone only ever
|
|
63
|
+
# sees the manifest literal string, never what a symlink resolves to.
|
|
64
|
+
all((.git_maintain_repos // []) | type == "array" and (. | unique | length) == length and
|
|
65
|
+
all(type == "string" and startswith("/") and . != "/" and
|
|
66
|
+
(endswith("/") | not) and (contains("..") | not) and (test("\\s") | not) and
|
|
67
|
+
(ascii_downcase | contains("azagent") | not) and (ascii_downcase | contains("vsts.agent") | not))) and
|
|
50
68
|
all((.brew_upgrade_all // false) | type == "boolean") and
|
|
51
69
|
all(
|
|
52
70
|
(.surface_clis // []) as $surfaces |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amsterdamdatalabs/adl-agent-control-linux-x64",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "adl CLI binary for linux/x64 (agent identity + Azure DevOps front door — see docs/architecture/cli-design.md)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"os": [
|