@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.7 → 0.1.8

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.
Files changed (40) hide show
  1. package/bin/adl +2 -2
  2. package/bin/lean-ctx-observe-strip +76 -3
  3. package/bin/reconcile +175 -5
  4. package/bin/validate-manifest +19 -1
  5. package/package.json +1 -1
  6. package/packages/agent-hooks/src/deploy/deploy.ts +98 -18
  7. package/packages/agent-hooks/src/deploy/doctor.ts +61 -11
  8. package/packages/agent-hooks/src/deploy/json-merge.ts +430 -18
  9. package/packages/agent-hooks/src/deploy/lock.ts +36 -2
  10. package/packages/agent-hooks/src/lean-ctx/env.ts +17 -0
  11. package/packages/agent-hooks/src/lean-ctx/mcp.ts +4 -2
  12. package/packages/agent-settings/assets/lean-ctx/config.toml +3 -0
  13. package/packages/agent-settings/assets/lean-ctx/gitignore +4 -0
  14. package/packages/agent-settings/assets/lean-ctx/layout.toml +2 -0
  15. package/packages/agent-settings/src/deploy/agents-deploy.ts +10 -5
  16. package/packages/agent-settings/src/deploy/doctor.ts +39 -3
  17. package/packages/agent-settings/src/deploy/lean-ctx-deploy.ts +104 -12
  18. package/packages/agent-settings/src/deploy/lean-ctx-merge.ts +580 -0
  19. package/packages/agent-settings/src/deploy/lock.ts +4 -1
  20. package/packages/agent-settings/src/deploy/mcp-deploy.ts +9 -6
  21. package/packages/agent-settings/src/deploy/settings.ts +6 -3
  22. package/packages/agent-skills/dist/deploy.d.ts +2 -0
  23. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  24. package/packages/agent-skills/dist/deploy.js +15 -8
  25. package/packages/agent-skills/dist/deploy.js.map +1 -1
  26. package/packages/agent-skills/dist/manifest.d.ts +1 -0
  27. package/packages/agent-skills/dist/manifest.d.ts.map +1 -1
  28. package/packages/agent-skills/dist/manifest.js +6 -2
  29. package/packages/agent-skills/dist/manifest.js.map +1 -1
  30. package/packages/agent-skills/src/deploy.ts +15 -8
  31. package/packages/agent-skills/src/manifest.ts +5 -2
  32. package/packages/azdo/src/azdo-repo-doctor.ts +401 -91
  33. package/packages/azdo/src/lib/graph-identity.ts +190 -3
  34. package/packages/fleet/jobs/git-maintain/src/main.ts +305 -45
  35. package/packages/fleet/manifest.json +2 -0
  36. package/packages/fleet/manifest.schema.json +9 -0
  37. package/packages/repo-controls/README.md +13 -6
  38. package/packages/repo-controls/assets/adl-config.default.toml +1 -0
  39. package/packages/repo-controls/src/cli.ts +6 -1
  40. package/packages/repo-controls/src/install.ts +88 -6
@@ -190,12 +190,19 @@ adl-repo-controls --version | help
190
190
  ```
191
191
 
192
192
  **install** requires an existing root `adl-config.toml`: that is the central
193
- management marker. It deletes and rebuilds the complete adl-owned
194
- `.workflows/` root, then appends its `[controls]` table only when that table is
195
- missing. It appends the single shared root ignore `/.enact/logs/` and
196
- `/.coverage-cache/` to `.gitignore`, writes the lock, and sets
197
- `core.hooksPath=.workflows/hooks` with `git config --local`.
198
- - Re-running is state-idempotent: the rebuilt root is byte-identical.
193
+ management marker. It reconciles the complete adl-owned `.workflows/` root
194
+ against every managed path's actual bytes (independent review, 2026-09-29:
195
+ a blind wholesale delete-and-rebuild touched every file's mtime — even ones
196
+ that already matched — on every run, which broke idempotency for anything
197
+ reading those mtimes) — a path whose content is already byte- and
198
+ mode-identical is left alone untouched, and anything else (missing,
199
+ altered, or retired) is still deleted or repaired. It then appends its
200
+ `[controls]` table only when that table is missing. It appends the single
201
+ shared root ignore `/.enact/logs/` and `/.coverage-cache/` to `.gitignore`,
202
+ writes the lock, and sets `core.hooksPath=.workflows/hooks` with
203
+ `git config --local`.
204
+ - Re-running is a true no-op: the rebuilt root is byte-identical AND no
205
+ managed file's mtime changes when nothing needed correcting.
199
206
  - `--dry-run` prints every action and changes nothing (files or git config).
200
207
  - Extra, altered, missing, and retired managed files below `.workflows/` are
201
208
  deleted or repaired on the next install. Pre-hard-cut log evidence is kept in
@@ -81,7 +81,12 @@ async function dispatch(argv: string[], io: Io): Promise<number> {
81
81
  const parsed = parse(command, args, ['--dry-run'])
82
82
  const result = install(parsed.repo, { dryRun: parsed.flags.has('--dry-run') })
83
83
  printActions(io, result.actions, result.dryRun)
84
- io.out(`${result.dryRun ? '[dry-run] ' : ''}${TOOL} ${VERSION} installed into ${result.repo}`)
84
+ // Independent review, 2026-09-29, finding 8: a genuine no-op run (and
85
+ // any --dry-run preview) never claims to have "installed" anything —
86
+ // only a real run that actually changed something says so.
87
+ if (!result.dryRun && result.actions.length > 0) {
88
+ io.out(`${TOOL} ${VERSION} installed into ${result.repo}`)
89
+ }
85
90
  return 0
86
91
  }
87
92
  case 'uninstall': {
@@ -212,9 +212,20 @@ export interface InstallResult {
212
212
  dryRun: boolean
213
213
  }
214
214
 
215
- function readCentralMarker(repo: string): string {
215
+ /**
216
+ * `dryRun`: a --dry-run preview must be able to describe what a real
217
+ * install WOULD do even for a repository with no adl-config.toml on disk
218
+ * yet (independent review, 2026-09-29, finding 6 — `adl install
219
+ * --dry-run`'s own step 1 would CREATE this exact marker moments before
220
+ * this step runs for real, and a dry-run writes nothing to the real repo
221
+ * first). Simulated as the same minimal `version = 1` template that
222
+ * creation step writes, never persisted. A REAL run (dryRun=false) is
223
+ * unaffected: a missing marker still throws exactly as before.
224
+ */
225
+ function readCentralMarker(repo: string, dryRun: boolean): string {
216
226
  const configPath = join(repo, CONFIG_REL)
217
227
  if (!existsSync(configPath)) {
228
+ if (dryRun) return 'version = 1\n'
218
229
  throw new Error(`${CONFIG_REL} is missing — this repository is not centrally managed`)
219
230
  }
220
231
  return readFileSync(configPath, 'utf8')
@@ -222,11 +233,11 @@ function readCentralMarker(repo: string): string {
222
233
 
223
234
  export function install(repoArg: string, options: InstallOptions): InstallResult {
224
235
  const repo = resolveRepo(repoArg)
225
- const initialConfigText = readCentralMarker(repo)
236
+ const dryRun = options.dryRun === true
237
+ const initialConfigText = readCentralMarker(repo, dryRun)
226
238
  // Validate only repo-controls' declared tables before deleting its root.
227
239
  // Other owners' tables remain deliberately opaque.
228
240
  validateOwnedConfig(initialConfigText)
229
- const dryRun = options.dryRun === true
230
241
  // The lock is outside neither configuration nor runtime boundaries, so read
231
242
  // its bookkeeping before replacing its containing root. It is never used
232
243
  // to preserve root content.
@@ -240,8 +251,12 @@ export function install(repoArg: string, options: InstallOptions): InstallResult
240
251
  }
241
252
  // This root is a single owner, so drift, stale files, old lock formats, and
242
253
  // removed managed files are pruned before the authoritative projection is
243
- // rebuilt. Pre-hard-cut log evidence is quarantined in place.
244
- reconcileWorkflowsRoot(repo, ops, 'authoritative repo-controls root reconciliation')
254
+ // rebuilt. Pre-hard-cut log evidence is quarantined in place. Selective,
255
+ // not wholesale (independent review, 2026-09-29): only paths NOT in
256
+ // managedPaths are removed, so a managed file that has not actually
257
+ // changed is left on disk for Ops.write()'s own byte comparison below to
258
+ // correctly report 'unchanged' and leave its mtime alone.
259
+ reconcileManagedWorkflowsRoot(repo, ops, [...managedPaths, LOCK_REL], 'stale .workflows content')
245
260
 
246
261
  const files: Lock['files'] = {}
247
262
  // Standalone CI assets are copied BYTE-FOR-BYTE, executable, and locked.
@@ -452,6 +467,71 @@ function removeSymlinksBelow(repo: string, relative: string, ops: Ops): void {
452
467
  }
453
468
  }
454
469
 
470
+ interface CollectedFile {
471
+ readonly rel: string
472
+ /** A symlink sitting at a path that TEXTUALLY matches a managed path is never "the managed file, unchanged" -- it is the wrong kind of node there and must be removed so `Ops.write()` creates a real file, never writes through the link into whatever it targets. */
473
+ readonly isSymlink: boolean
474
+ }
475
+
476
+ /** Every FILE (or symlink, treated as a leaf -- never traversed) under `rel`, repo-relative, depth-first. `[]` when `rel` does not exist. */
477
+ function collectFiles(repo: string, rel: string): CollectedFile[] {
478
+ const stats = lstatSync(join(repo, rel), { throwIfNoEntry: false })
479
+ if (stats === undefined) return []
480
+ if (stats.isSymbolicLink()) return [{ rel, isSymlink: true }]
481
+ if (!stats.isDirectory()) return [{ rel, isSymlink: false }]
482
+ return readdirSync(join(repo, rel)).flatMap((entry) => collectFiles(repo, `${rel}/${entry}`))
483
+ }
484
+
485
+ /** Removes any now-empty directory under `rel` (never `rel` itself, never the preserved subtree), depth-first. An ancestor of the preserved subtree is still walked (so its OTHER children get pruned) but is never itself removed, since the preserved subtree keeps it non-empty. */
486
+ function pruneEmptyDirs(repo: string, rel: string, preserveRel: string | undefined): void {
487
+ if (preserveRel !== undefined && rel === preserveRel) return
488
+ const abs = join(repo, rel)
489
+ const stats = lstatSync(abs, { throwIfNoEntry: false })
490
+ if (stats === undefined || !stats.isDirectory() || stats.isSymbolicLink()) return
491
+ for (const entry of readdirSync(abs)) pruneEmptyDirs(repo, `${rel}/${entry}`, preserveRel)
492
+ if (rel !== WORKFLOWS_ROOT && readdirSync(abs).length === 0) rmSync(abs, { recursive: true })
493
+ }
494
+
495
+ /**
496
+ * Selective counterpart to `reconcileWorkflowsRoot`, used only by
497
+ * `install()` (independent review, 2026-09-29): the wholesale
498
+ * remove-then-rewrite that function does is correct for `uninstall()`
499
+ * (nothing should survive), but for `install()` it defeated `Ops.write()`'s
500
+ * own byte comparison — every managed file always looked freshly absent,
501
+ * so a no-op run still reported every file as newly 'create'd and touched
502
+ * every mtime. This instead removes only paths NOT in `managedPaths` (and
503
+ * never the preserved pre-hard-cut log-quarantine subtree), leaving every
504
+ * managed path exactly as it is on disk for `Ops.write()` to diff for real.
505
+ */
506
+ function reconcileManagedWorkflowsRoot(repo: string, ops: Ops, managedPaths: readonly string[], reason: string): void {
507
+ // lstat, not existsSync: existsSync follows links and reports false for a
508
+ // dangling one, which would otherwise leave a stale (possibly dangling)
509
+ // symlink AT the root itself in place -- and a later `mkdirSync(...,
510
+ // {recursive: true})` for a managed path under it throws EEXIST against
511
+ // that non-directory entry rather than creating a real .workflows/.
512
+ const rootStats = lstatSync(join(repo, WORKFLOWS_ROOT), { throwIfNoEntry: false })
513
+ if (rootStats === undefined) return
514
+ if (rootStats.isSymbolicLink()) {
515
+ ops.remove(WORKFLOWS_ROOT, 'the managed root was a symlink, not a real directory')
516
+ return
517
+ }
518
+ const workflowLogs = LEGACY_LOG_QUARANTINES[0]
519
+ const preserveRel = realDirectoryHasData(repo, workflowLogs.rel) ? workflowLogs.rel : undefined
520
+ const managed = new Set(managedPaths)
521
+
522
+ for (const { rel, isSymlink } of collectFiles(repo, WORKFLOWS_ROOT)) {
523
+ if (preserveRel !== undefined && (rel === preserveRel || rel.startsWith(`${preserveRel}/`))) continue
524
+ if (managed.has(rel) && !isSymlink) continue
525
+ ops.remove(rel, isSymlink && managed.has(rel) ? 'a managed path was a symlink, not a real file' : reason)
526
+ }
527
+ // The preserved quarantine subtree is otherwise skipped entirely above,
528
+ // but a symlink placed inside it is never legitimate quarantined
529
+ // evidence -- removed without traversing its target, same as
530
+ // reconcileWorkflowsRoot's own uninstall path.
531
+ if (preserveRel !== undefined) removeSymlinksBelow(repo, preserveRel, ops)
532
+ pruneEmptyDirs(repo, WORKFLOWS_ROOT, preserveRel)
533
+ }
534
+
455
535
  export type UninstallOptions = InstallOptions
456
536
 
457
537
  export interface UninstallResult extends InstallResult {
@@ -460,7 +540,9 @@ export interface UninstallResult extends InstallResult {
460
540
 
461
541
  export function uninstall(repoArg: string, options: UninstallOptions): UninstallResult {
462
542
  const repo = resolveRepo(repoArg)
463
- validateOwnedConfig(readCentralMarker(repo))
543
+ // Never simulated, dry-run or not: uninstalling a repo with no marker at
544
+ // all is not a "what would happen" question worth answering.
545
+ validateOwnedConfig(readCentralMarker(repo, false))
464
546
  const dryRun = options.dryRun === true
465
547
  const lock = priorLockForReconciliation(repo)
466
548
  const ops = new Ops(repo, dryRun)