@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.
Files changed (42) hide show
  1. package/bin/adl +2 -2
  2. package/bin/git +89 -8
  3. package/bin/lean-ctx-observe-strip +76 -3
  4. package/bin/reconcile +175 -5
  5. package/bin/validate-manifest +19 -1
  6. package/package.json +1 -1
  7. package/packages/agent-hooks/src/deploy/deploy.ts +98 -18
  8. package/packages/agent-hooks/src/deploy/doctor.ts +61 -11
  9. package/packages/agent-hooks/src/deploy/json-merge.ts +430 -18
  10. package/packages/agent-hooks/src/deploy/lock.ts +36 -2
  11. package/packages/agent-hooks/src/lean-ctx/env.ts +17 -0
  12. package/packages/agent-hooks/src/lean-ctx/mcp.ts +4 -2
  13. package/packages/agent-settings/assets/lean-ctx/config.toml +3 -0
  14. package/packages/agent-settings/assets/lean-ctx/gitignore +4 -0
  15. package/packages/agent-settings/assets/lean-ctx/layout.toml +2 -0
  16. package/packages/agent-settings/src/deploy/agents-deploy.ts +10 -5
  17. package/packages/agent-settings/src/deploy/doctor.ts +39 -3
  18. package/packages/agent-settings/src/deploy/lean-ctx-deploy.ts +104 -12
  19. package/packages/agent-settings/src/deploy/lean-ctx-merge.ts +580 -0
  20. package/packages/agent-settings/src/deploy/lock.ts +4 -1
  21. package/packages/agent-settings/src/deploy/mcp-deploy.ts +9 -6
  22. package/packages/agent-settings/src/deploy/settings.ts +6 -3
  23. package/packages/agent-skills/dist/deploy.d.ts +2 -0
  24. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  25. package/packages/agent-skills/dist/deploy.js +15 -8
  26. package/packages/agent-skills/dist/deploy.js.map +1 -1
  27. package/packages/agent-skills/dist/manifest.d.ts +1 -0
  28. package/packages/agent-skills/dist/manifest.d.ts.map +1 -1
  29. package/packages/agent-skills/dist/manifest.js +6 -2
  30. package/packages/agent-skills/dist/manifest.js.map +1 -1
  31. package/packages/agent-skills/src/deploy.ts +15 -8
  32. package/packages/agent-skills/src/manifest.ts +5 -2
  33. package/packages/azdo/src/azdo-repo-doctor.ts +412 -95
  34. package/packages/azdo/src/lib/graph-identity.ts +190 -3
  35. package/packages/fleet/jobs/git-maintain/src/main.ts +305 -45
  36. package/packages/fleet/manifest.json +2 -0
  37. package/packages/fleet/manifest.schema.json +9 -0
  38. package/packages/repo-controls/README.md +13 -6
  39. package/packages/repo-controls/assets/adl-config.default.toml +1 -0
  40. package/packages/repo-controls/assets/version-bump.sh +6 -1
  41. package/packages/repo-controls/src/cli.ts +6 -1
  42. package/packages/repo-controls/src/install.ts +88 -6
@@ -1,5 +1,7 @@
1
+ # >>> adl managed (do not edit within this block) >>>
1
2
  # lean-ctx layout pin (GL #623) — managed by lean-ctx, do not edit.
2
3
  # Marks this install as committed to the XDG four-dir layout so a stray
3
4
  # ~/.lean-ctx never re-collapses config/data/state/cache. Remove via
4
5
  # `lean-ctx doctor` only if you intentionally revert to a single-dir layout.
5
6
  mode = "xdg"
7
+ # <<< adl managed <<<
@@ -23,6 +23,8 @@ export interface DeployAgentsOptions {
23
23
  readonly targetRepo: string;
24
24
  readonly profile?: DeployProfile;
25
25
  readonly agents?: readonly AgentDef[];
26
+ /** Computes the exact same written/unchanged/removed/conflicts result without writing anything (independent review, 2026-09-29, finding 6). */
27
+ readonly dryRun?: boolean;
26
28
  }
27
29
 
28
30
  export interface AgentConflict {
@@ -41,6 +43,7 @@ export interface DeployAgentsResult {
41
43
 
42
44
  export function deployAgents(options: DeployAgentsOptions): DeployAgentsResult {
43
45
  const { assetsDir, targetRepo } = options;
46
+ const dryRun = options.dryRun === true;
44
47
 
45
48
  if (!existsSync(assetsDir) || !statSync(assetsDir).isDirectory()) {
46
49
  throw new Error(`agent-settings agents assets directory not found: ${assetsDir}`);
@@ -89,8 +92,10 @@ export function deployAgents(options: DeployAgentsOptions): DeployAgentsResult {
89
92
  if (exists && readFileSync(absPath, "utf8") === content) {
90
93
  unchanged.push(relPath);
91
94
  } else {
92
- mkdirSync(join(targetRepo, surface.folder), { recursive: true });
93
- writeFileSync(absPath, content);
95
+ if (!dryRun) {
96
+ mkdirSync(join(targetRepo, surface.folder), { recursive: true });
97
+ writeFileSync(absPath, content);
98
+ }
94
99
  written.push(relPath);
95
100
  }
96
101
  kept.push(def.name);
@@ -101,21 +106,21 @@ export function deployAgents(options: DeployAgentsOptions): DeployAgentsResult {
101
106
  if (kept.includes(name)) continue;
102
107
  const relPath = `${surface.folder}/${name}.${ext}`;
103
108
  const absPath = join(targetRepo, relPath);
104
- if (existsSync(absPath)) unlinkSync(absPath);
109
+ if (!dryRun && existsSync(absPath)) unlinkSync(absPath);
105
110
  removed.push(relPath);
106
111
  }
107
112
 
108
113
  newAgentEntries[surface.folder] = kept.sort();
109
114
  }
110
115
 
111
- writeLock(targetRepo, { schema: 1, mcp: lock.mcp, agents: newAgentEntries, leanCtx: lock.leanCtx });
116
+ if (!dryRun) writeLock(targetRepo, { schema: 1, mcp: lock.mcp, agents: newAgentEntries, leanCtx: lock.leanCtx });
112
117
 
113
118
  // Once anything was actually taken over, this package's own footprint in
114
119
  // the legacy lock's `files` section for these exact agent paths is stale:
115
120
  // ownership has fully moved to .agents/agent-settings.lock.json just
116
121
  // written above. A legacy lock present but untouched by any takeover (no
117
122
  // hash matched, or this repo never had one) is left exactly as found.
118
- if (anyLegacyTakeover && legacyDoc !== undefined && legacyLockPath !== undefined) {
123
+ if (!dryRun && anyLegacyTakeover && legacyDoc !== undefined && legacyLockPath !== undefined) {
119
124
  stripLegacyOwnership(legacyDoc, legacyLockPath, { files: legacyFileRemovals });
120
125
  }
121
126
 
@@ -12,6 +12,7 @@ import { existsSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { extensionFor, renderAgent } from "./agent-render.js";
14
14
  import { loadAgentSources, type AgentDef } from "./agent-toml.js";
15
+ import { resolveConfigTomlWrite, resolveGitignoreWrite, resolveLeanCtxWrite } from "./lean-ctx-merge.js";
15
16
  import { agentOwned, leanCtxOwned, mcpServerOwned, readLock, type SettingsLock } from "./lock.js";
16
17
  import { readDeployedMcpServerNames, readDeployedMcpServerValue } from "./mcp-inspect.js";
17
18
  import { renderMcpJsonEntry } from "./mcp-render.js";
@@ -175,13 +176,48 @@ export function doctorLeanCtx(options: DoctorLeanCtxOptions): DoctorResult {
175
176
  issues.push({ severity: "error", path: file.target, reason: ownedByUs ? "deploy-written lean-ctx file is missing from the target repo" : "expected lean-ctx file has not been deployed" });
176
177
  continue;
177
178
  }
178
- if (!ownedByUs) {
179
+ // Independent review, 2026-09-30, finding 6: .gitignore is exempted
180
+ // from this early conflict the same way deploy itself now is — a
181
+ // markerless, never-adl-written .agents/.gitignore is not a hard
182
+ // conflict; resolveGitignoreWrite's own append-not-conflict migration
183
+ // decides it below instead (mirrors lean-ctx-deploy.ts's own guard).
184
+ if (!ownedByUs && file.asset !== "gitignore") {
179
185
  issues.push({ severity: "error", path: file.target, reason: "a user-owned file conflicts with a lean-ctx asset path; not managed by deploy" });
180
186
  continue;
181
187
  }
182
188
  const expected = readFileSync(assetAbs, "utf8");
183
- if (readFileSync(targetAbs, "utf8") !== expected) {
184
- issues.push({ severity: "error", path: file.target, reason: "deploy-written lean-ctx file was modified since deploy" });
189
+ const current = readFileSync(targetAbs, "utf8");
190
+ // Same merge-contract analysis deploy itself uses (lean-ctx-merge.ts):
191
+ // a user addition outside the managed block alone is not drift; a
192
+ // changed/removed/malformed managed block, or a duplicate key, is.
193
+ if (current !== expected) {
194
+ const outcome =
195
+ file.asset === "config.toml"
196
+ ? resolveConfigTomlWrite(expected, current, ownedByUs)
197
+ : file.asset === "gitignore"
198
+ ? resolveGitignoreWrite(expected, current, ownedByUs)
199
+ : resolveLeanCtxWrite(expected, current, ownedByUs);
200
+ if (outcome.action === "write") {
201
+ // Tarun, 2026-09-30 (merge-contract correction, same day):
202
+ // "adl-owned entries SELF-HEAL" — when the drift is specifically
203
+ // an adl-owned scalar restored to adl's own value, name it
204
+ // explicitly here too, the same way a healed hook is named
205
+ // ("restored … local changes replaced"), instead of the generic
206
+ // "modified since deploy" wording.
207
+ if (outcome.restoredScalars !== undefined && outcome.restoredScalars.length > 0) {
208
+ for (const scalar of outcome.restoredScalars) {
209
+ issues.push({
210
+ severity: "error",
211
+ path: file.target,
212
+ reason: `restored adl value: ${scalar.key} = ${JSON.stringify(scalar.value)} (was ${JSON.stringify(scalar.previous)}); run deploy to apply`,
213
+ });
214
+ }
215
+ } else {
216
+ issues.push({ severity: "error", path: file.target, reason: "deploy-written lean-ctx file was modified since deploy" });
217
+ }
218
+ } else if (outcome.action === "conflict") {
219
+ issues.push({ severity: "error", path: file.target, reason: outcome.reason });
220
+ }
185
221
  }
186
222
  }
187
223
 
@@ -1,9 +1,29 @@
1
1
  /**
2
2
  * Writes the repo-local lean-ctx config files (config.toml, layout.toml)
3
- * and .agents/.gitignore from assets/lean-ctx/, per profile.leanCtx —
4
- * verbatim byte copies, no per-surface rendering: L3 (Tarun, 2026-09-28)
5
- * ships each asset exactly as it is; this module never edits the allowlist
6
- * or any other content inside config.toml.
3
+ * and .agents/.gitignore from assets/lean-ctx/, per profile.leanCtx. A
4
+ * fresh write (nothing at the target yet) is still a verbatim byte copy —
5
+ * no per-surface rendering — reproducing the asset exactly (the asset
6
+ * itself is a complete BEGIN/END-marked managed block; see
7
+ * lean-ctx-merge.ts).
8
+ *
9
+ * Merge contract (Tarun, 2026-09-29, merge contract; mechanism corrected
10
+ * by an independent review the same day — supersedes the original "L3,
11
+ * Tarun 2026-09-28" whole-file-verbatim rule, and this comment's own
12
+ * earlier per-key/array-entry description, both below): adl owns a single
13
+ * marked managed block per file and it SELF-HEALS as a whole the moment
14
+ * its content drifts from the asset. Everything OUTSIDE the markers is the
15
+ * user's, preserved byte-for-byte, never parsed or rewritten — see
16
+ * `lean-ctx-merge.ts`'s own header for exactly why (a full-file TOML parse
17
+ * both rejected valid user TOML and silently discarded a user's entire
18
+ * file on any parse failure; pure marker-text splicing cannot do either).
19
+ * `doctorLeanCtx` (doctor.ts) shares the same `resolveLeanCtxWrite`
20
+ * analysis, so "is this drift" is answered in exactly one place.
21
+ *
22
+ * A markerless file the lock already records as adl-written is a one-time
23
+ * MIGRATION target (wrapped in markers if its known prior content is still
24
+ * found verbatim; a CONFLICT, left untouched, otherwise) — never silently
25
+ * discarded. Malformed markers, and a key declared both inside the managed
26
+ * block and outside it, are conflicts too.
7
27
  *
8
28
  * Same ownership rule as mcp/agents (lock.ts): a pre-existing file at a
9
29
  * target path the lock does not record is a conflict, left untouched and
@@ -12,11 +32,15 @@
12
32
  *
13
33
  * Fails loud: a missing assets dir/target repo, a profile with no leanCtx
14
34
  * section, or a missing source asset file all throw. There is no
15
- * partial-success return value.
35
+ * partial-success return value. A resolveLeanCtxWrite CONFLICT is reported
36
+ * in the ordinary `conflicts` array (like any other), not a throw — the
37
+ * caller (apps/cli's `adl install`) is the one that turns a non-empty
38
+ * conflicts list into a loud, non-zero-exit failure.
16
39
  */
17
40
  import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
18
41
  import { dirname, join } from "node:path";
19
42
  import { legacyFileTakeover, legacyTakeoverForGitignore, type LegacyLockDoc, readLegacyLock, stripLegacyOwnership } from "./legacy-lock.js";
43
+ import { resolveConfigTomlWrite, resolveGitignoreWrite, resolveLeanCtxWrite } from "./lean-ctx-merge.js";
20
44
  import { leanCtxOwned, readLock, writeLock } from "./lock.js";
21
45
  import { type DeployProfile, loadProfile } from "./profile.js";
22
46
 
@@ -24,6 +48,8 @@ export interface DeployLeanCtxOptions {
24
48
  readonly assetsDir: string; // packages/agent-settings/assets/lean-ctx
25
49
  readonly targetRepo: string;
26
50
  readonly profile?: DeployProfile;
51
+ /** Computes the exact same written/unchanged/removed/conflicts result without writing anything (target files, lock, or legacy lock) — independent review, 2026-09-29, finding 6. */
52
+ readonly dryRun?: boolean;
27
53
  }
28
54
 
29
55
  export interface LeanCtxConflict {
@@ -31,16 +57,26 @@ export interface LeanCtxConflict {
31
57
  readonly reason: string;
32
58
  }
33
59
 
60
+ /** One adl-owned scalar restored to adl's own value this run (2026-09-30, Tarun's merge-contract correction: "adl-owned entries SELF-HEAL" — named explicitly, the same way a healed hook is named, never silent). */
61
+ export interface RestoredLeanCtxScalar {
62
+ readonly target: string;
63
+ readonly key: string;
64
+ readonly value: unknown;
65
+ readonly previous: unknown;
66
+ }
67
+
34
68
  export interface DeployLeanCtxResult {
35
69
  readonly written: readonly string[];
36
70
  readonly unchanged: readonly string[];
37
71
  readonly removed: readonly string[];
38
72
  readonly conflicts: readonly LeanCtxConflict[];
39
73
  readonly lockPath: string;
74
+ readonly restored: readonly RestoredLeanCtxScalar[];
40
75
  }
41
76
 
42
77
  export function deployLeanCtx(options: DeployLeanCtxOptions): DeployLeanCtxResult {
43
78
  const { assetsDir, targetRepo } = options;
79
+ const dryRun = options.dryRun === true;
44
80
 
45
81
  if (!existsSync(assetsDir) || !statSync(assetsDir).isDirectory()) {
46
82
  throw new Error(`agent-settings lean-ctx assets directory not found: ${assetsDir}`);
@@ -61,6 +97,7 @@ export function deployLeanCtx(options: DeployLeanCtxOptions): DeployLeanCtxResul
61
97
  const unchanged: string[] = [];
62
98
  const removed: string[] = [];
63
99
  const conflicts: LeanCtxConflict[] = [];
100
+ const restored: RestoredLeanCtxScalar[] = [];
64
101
  const newLeanCtxEntries: Record<string, boolean> = {};
65
102
  const kept: string[] = [];
66
103
  const legacyFileRemovals: string[] = [];
@@ -76,6 +113,15 @@ export function deployLeanCtx(options: DeployLeanCtxOptions): DeployLeanCtxResul
76
113
  const targetAbs = join(targetRepo, file.target);
77
114
  const exists = existsSync(targetAbs);
78
115
  let managedByUs = leanCtxOwned(lock, file.target);
116
+ // Set only when THIS run's legacy hash check (below) is what makes
117
+ // managedByUs true — that check already verified the file's exact
118
+ // bytes against the OLD engine's own lock, so unlike leanCtxOwned
119
+ // (our own lock's unverified "we wrote this at some point" record),
120
+ // there is no risk of undetected foreign/user content mixed in, and
121
+ // the file can be overwritten wholesale — the same as a fresh write,
122
+ // never through resolveLeanCtxWrite's cautious migration path (which
123
+ // exists precisely for the case this check has already ruled out).
124
+ let legacyVerifiedClean = false;
79
125
 
80
126
  if (exists && !managedByUs) {
81
127
  const currentContent = readFileSync(targetAbs, "utf8");
@@ -84,26 +130,71 @@ export function deployLeanCtx(options: DeployLeanCtxOptions): DeployLeanCtxResul
84
130
  // sha256), so fall back to that comparison for whichever target it is.
85
131
  if (legacyFileTakeover(currentContent, legacyDoc, file.target)) {
86
132
  managedByUs = true;
133
+ legacyVerifiedClean = true;
87
134
  legacyFileRemovals.push(file.target);
88
135
  anyLegacyTakeover = true;
89
136
  } else if (legacyTakeoverForGitignore(currentContent, legacyDoc, file.target)) {
90
137
  managedByUs = true;
138
+ legacyVerifiedClean = true;
91
139
  legacyGitignoreRemovals.push(file.target);
92
140
  anyLegacyTakeover = true;
93
141
  }
94
142
  }
95
143
 
96
- if (exists && !managedByUs) {
144
+ // Independent review, 2026-09-30, finding 6: this guard used to apply
145
+ // uniformly, so a user-authored .agents/.gitignore with no markers at
146
+ // all (never written by us — managedByUs false) hit a hard conflict
147
+ // right here, before resolveGitignoreWrite ever got a chance to run
148
+ // its own append-not-conflict migration. .gitignore is deliberately
149
+ // exempted: append is always safe there (a stray, harmless duplicate
150
+ // ignore line at worst), unlike config.toml/layout.toml where
151
+ // guessing at a foreign file's structure is not.
152
+ if (exists && !managedByUs && file.asset !== "gitignore") {
97
153
  conflicts.push({ target: file.target, reason: "a user-owned file already exists at this path; deploy left it untouched" });
98
154
  continue;
99
155
  }
100
156
 
101
- if (exists && readFileSync(targetAbs, "utf8") === content) {
157
+ if (!exists || legacyVerifiedClean) {
158
+ if (!dryRun) {
159
+ mkdirSync(dirname(targetAbs), { recursive: true });
160
+ writeFileSync(targetAbs, content);
161
+ }
162
+ written.push(file.target);
163
+ newLeanCtxEntries[file.target] = true;
164
+ kept.push(file.target);
165
+ continue;
166
+ }
167
+
168
+ const currentContent = readFileSync(targetAbs, "utf8");
169
+ if (currentContent === content) {
170
+ unchanged.push(file.target);
171
+ newLeanCtxEntries[file.target] = true;
172
+ kept.push(file.target);
173
+ continue;
174
+ }
175
+
176
+ const outcome =
177
+ file.asset === "config.toml"
178
+ ? resolveConfigTomlWrite(content, currentContent, managedByUs)
179
+ : file.asset === "gitignore"
180
+ ? resolveGitignoreWrite(content, currentContent, managedByUs)
181
+ : resolveLeanCtxWrite(content, currentContent, managedByUs);
182
+ if (outcome.action === "conflict") {
183
+ // Still tracked as ours going forward (a fixed-up file can migrate
184
+ // or self-heal on a later run) but never written to now.
185
+ conflicts.push({ target: file.target, reason: outcome.reason });
186
+ newLeanCtxEntries[file.target] = true;
187
+ kept.push(file.target);
188
+ continue;
189
+ }
190
+ if (outcome.action === "unchanged") {
102
191
  unchanged.push(file.target);
103
192
  } else {
104
- mkdirSync(dirname(targetAbs), { recursive: true });
105
- writeFileSync(targetAbs, content);
193
+ if (!dryRun) writeFileSync(targetAbs, outcome.text);
106
194
  written.push(file.target);
195
+ for (const scalar of outcome.restoredScalars ?? []) {
196
+ restored.push({ target: file.target, key: scalar.key, value: scalar.value, previous: scalar.previous });
197
+ }
107
198
  }
108
199
  newLeanCtxEntries[file.target] = true;
109
200
  kept.push(file.target);
@@ -112,11 +203,11 @@ export function deployLeanCtx(options: DeployLeanCtxOptions): DeployLeanCtxResul
112
203
  for (const target of Object.keys(lock.leanCtx)) {
113
204
  if (kept.includes(target)) continue;
114
205
  const targetAbs = join(targetRepo, target);
115
- if (existsSync(targetAbs)) unlinkSync(targetAbs);
206
+ if (!dryRun && existsSync(targetAbs)) unlinkSync(targetAbs);
116
207
  removed.push(target);
117
208
  }
118
209
 
119
- writeLock(targetRepo, { schema: 1, mcp: lock.mcp, agents: lock.agents, leanCtx: newLeanCtxEntries });
210
+ if (!dryRun) writeLock(targetRepo, { schema: 1, mcp: lock.mcp, agents: lock.agents, leanCtx: newLeanCtxEntries });
120
211
 
121
212
  // Once anything was actually taken over, this package's own footprint in
122
213
  // the legacy lock (these exact `files` entries and/or the `.agents/.gitignore`
@@ -124,7 +215,7 @@ export function deployLeanCtx(options: DeployLeanCtxOptions): DeployLeanCtxResul
124
215
  // .agents/agent-settings.lock.json just written above. A legacy lock
125
216
  // present but untouched by any takeover (no hash matched, or this repo
126
217
  // never had one) is left exactly as found.
127
- if (anyLegacyTakeover && legacyDoc !== undefined && legacyLockPath !== undefined) {
218
+ if (!dryRun && anyLegacyTakeover && legacyDoc !== undefined && legacyLockPath !== undefined) {
128
219
  stripLegacyOwnership(legacyDoc, legacyLockPath, { files: legacyFileRemovals, gitignore: legacyGitignoreRemovals });
129
220
  }
130
221
 
@@ -134,5 +225,6 @@ export function deployLeanCtx(options: DeployLeanCtxOptions): DeployLeanCtxResul
134
225
  removed: removed.sort(),
135
226
  conflicts: conflicts.sort((a, b) => a.target.localeCompare(b.target)),
136
227
  lockPath: join(targetRepo, ".agents/agent-settings.lock.json"),
228
+ restored,
137
229
  };
138
230
  }