@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.9 → 0.1.12

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 (41) hide show
  1. package/bin/adl +2 -2
  2. package/bin/git +1 -1
  3. package/bin/git-ado +2 -2
  4. package/bin/lib/bun-compile-build.sh +1 -1
  5. package/bin/lib/fleet-host.sh +59 -0
  6. package/bin/lib/lean-ctx-observe-hooks.sh +9 -9
  7. package/bin/reconcile +222 -249
  8. package/bin/reconcile-host +111 -0
  9. package/bin/setup +216 -0
  10. package/bin/sync-and-reconcile +52 -39
  11. package/bin/validate-manifest +15 -44
  12. package/package.json +1 -3
  13. package/packages/agent-hooks/package.json +1 -2
  14. package/packages/agent-hooks/src/hooks/crg/background.ts +179 -0
  15. package/packages/agent-hooks/src/hooks/crg/handler.ts +2 -1
  16. package/packages/agent-hooks/src/hooks/crg/index.ts +36 -5
  17. package/packages/agent-hooks/src/rules/impl/fs-trash.ts +2 -2
  18. package/packages/agent-hooks/src/rules/impl/git-paths.ts +5 -6
  19. package/packages/agent-hooks/src/rules/impl/tool-grep.ts +8 -8
  20. package/packages/agent-settings/assets/GLOBAL-AGENTS.md +4 -2
  21. package/packages/agent-settings/src/global-surface-sweep.ts +5 -15
  22. package/packages/agent-skills/package.json +1 -1
  23. package/packages/azdo/package.json +1 -2
  24. package/packages/azdo/src/azdo-repo-doctor.ts +70 -75
  25. package/packages/fleet/manifest.json +5 -24
  26. package/packages/fleet/manifest.schema.json +8 -58
  27. package/packages/fleet/src/install-embedding-model.ts +1 -1
  28. package/packages/fleet/src/repo-maintain.ts +403 -0
  29. package/packages/fleet/src/scratch-reap.ts +1 -1
  30. package/packages/repo-controls/README.md +46 -8
  31. package/packages/repo-controls/assets/promote.ts +12 -7
  32. package/packages/repo-controls/assets/reconcile-guard.sh +113 -0
  33. package/packages/repo-controls/assets/version-bump.sh +91 -51
  34. package/packages/repo-controls/package.json +0 -1
  35. package/packages/repo-controls/src/azure-devops-governance.ts +17 -16
  36. package/packages/repo-controls/src/constants.ts +8 -0
  37. package/packages/repo-controls/src/hook.ts +3 -2
  38. package/packages/repo-controls/src/integrity.ts +15 -1
  39. package/packages/fleet/jobs/git-maintain/src/main.ts +0 -848
  40. package/packages/fleet/systemd/adl-azure-agent-update.service +0 -13
  41. package/packages/fleet/systemd/adl-azure-agent-update.timer +0 -10
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Single-flight, detached `code-review-graph update --skip-flows`.
3
+ *
4
+ * Why this exists: on a large repo `update` takes ~13 s, but the
5
+ * `PostToolUse` hook that triggers it is killed by the host at 5 s
6
+ * (`assets/hooks.json`, `"timeout": 5`), so a synchronous run never
7
+ * finishes and the graph goes stale on every edit. The hook therefore only
8
+ * DECIDES here and returns at once; the work runs in a detached process
9
+ * group that outlives the hook.
10
+ *
11
+ * Protocol (all state under `<root>/.adl/`, already git-ignored):
12
+ * - `crg-update.lock` holds the pid of the process running the update.
13
+ * - `crg-update.pending` marker: "an edit landed while an update ran".
14
+ * - `logs/crg-update.log` the update's own stdout+stderr (append).
15
+ *
16
+ * `requestUpdate` (the hook side):
17
+ * 1. Acquire the lock atomically (`wx`), holding the hook's own pid as a
18
+ * placeholder so a concurrent hook sees a live owner, then spawn the
19
+ * runner and overwrite the lock with the runner's pid.
20
+ * 2. Lock already held by a live process (and younger than
21
+ * `MAX_LOCK_AGE_MS`, a guard against pid reuse after a crash/reboot):
22
+ * touch the pending marker and spawn nothing. A dead pid, or an
23
+ * over-age lock, is stale: it is removed and step 1 retried once.
24
+ * 3. After touching the marker, re-check the owner: if it exited in the
25
+ * window between the first check and the marker write, acquire and
26
+ * spawn instead, so the queued edit is never stranded.
27
+ *
28
+ * `RUNNER_SCRIPT` (the detached side) runs the update, and while the
29
+ * pending marker exists clears it and runs once more. On its way out it
30
+ * releases the lock, then re-checks the marker once and re-acquires the
31
+ * lock (noclobber) if a hook queued in the meantime, closing the last gap.
32
+ */
33
+
34
+ import { closeSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
35
+ import { join } from 'node:path'
36
+
37
+ export const LOCK_REL = '.adl/crg-update.lock'
38
+ export const PENDING_REL = '.adl/crg-update.pending'
39
+ export const UPDATE_LOG_REL = '.adl/logs/crg-update.log'
40
+
41
+ /** A lock older than this is presumed left by a crash, whatever its pid says. */
42
+ export const MAX_LOCK_AGE_MS = 30 * 60 * 1000
43
+
44
+ export const RUNNER_SCRIPT = [
45
+ 'lock="$1"; pending="$2"; shift 2',
46
+ 'while :; do',
47
+ ' rm -f "$pending"',
48
+ ' "$@"',
49
+ ' if [ -e "$pending" ]; then continue; fi',
50
+ ' rm -f "$lock"',
51
+ ' if [ -e "$pending" ] && (set -C; echo $$ > "$lock") 2>/dev/null; then continue; fi',
52
+ ' break',
53
+ 'done',
54
+ ].join('\n')
55
+
56
+ export interface SpawnSpec {
57
+ readonly cmd: string
58
+ readonly args: readonly string[]
59
+ readonly cwd: string
60
+ readonly env: Readonly<Record<string, string | undefined>>
61
+ /** Absolute path the child's stdout+stderr are appended to. */
62
+ readonly logFile: string
63
+ }
64
+
65
+ export interface BackgroundDeps {
66
+ /** Start a detached, unref'd child; return its pid. Must not wait for it. */
67
+ readonly spawn: (spec: SpawnSpec) => number
68
+ readonly isAlive: (pid: number) => boolean
69
+ readonly now: () => number
70
+ readonly selfPid: () => number
71
+ /** Resolve `bin` against `path` (the hook's own PATH); `null` when it is not there. */
72
+ readonly resolve: (bin: string, path: string | undefined) => string | null
73
+ }
74
+
75
+ export type UpdateRequest = 'spawned' | 'queued'
76
+
77
+ const UPDATE_ARGS = ['update', '--skip-flows'] as const
78
+
79
+ function isPidAlive(pid: number): boolean {
80
+ try {
81
+ process.kill(pid, 0)
82
+ return true
83
+ } catch (error) {
84
+ // EPERM: the process exists but belongs to someone else.
85
+ return (error as NodeJS.ErrnoException).code === 'EPERM'
86
+ }
87
+ }
88
+
89
+ function spawnDetached(spec: SpawnSpec): number {
90
+ const fd = openSync(spec.logFile, 'a', 0o600)
91
+ try {
92
+ const proc = Bun.spawn([spec.cmd, ...spec.args], {
93
+ cwd: spec.cwd,
94
+ env: spec.env as Record<string, string>,
95
+ stdin: 'ignore',
96
+ stdout: fd,
97
+ stderr: fd,
98
+ // Own session/process group: survives the hook, unreachable by the host's timeout kill.
99
+ detached: true,
100
+ })
101
+ proc.unref()
102
+ return proc.pid
103
+ } finally {
104
+ closeSync(fd)
105
+ }
106
+ }
107
+
108
+ export const defaultBackgroundDeps: BackgroundDeps = {
109
+ spawn: spawnDetached,
110
+ isAlive: isPidAlive,
111
+ now: () => Date.now(),
112
+ selfPid: () => process.pid,
113
+ resolve: (bin, path) => Bun.which(bin, { PATH: path ?? '' }),
114
+ }
115
+
116
+ function ownerIsLive(deps: BackgroundDeps, lockPath: string): boolean {
117
+ try {
118
+ const pid = Number.parseInt(readFileSync(lockPath, 'utf8').trim(), 10)
119
+ const ageMs = deps.now() - statSync(lockPath).mtimeMs
120
+ return Number.isInteger(pid) && pid > 0 && ageMs < MAX_LOCK_AGE_MS && deps.isAlive(pid)
121
+ } catch {
122
+ // Vanished between the caller's check and this read: nobody owns it.
123
+ return false
124
+ }
125
+ }
126
+
127
+ /** `wx` create: true if this call created the lock, false if it already exists. */
128
+ function tryAcquire(lockPath: string, pid: number): boolean {
129
+ try {
130
+ writeFileSync(lockPath, `${pid}\n`, { flag: 'wx', mode: 0o600 })
131
+ return true
132
+ } catch (error) {
133
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') return false
134
+ throw error
135
+ }
136
+ }
137
+
138
+ function startRunner(deps: BackgroundDeps, root: string, env: SpawnSpec['env'], lockPath: string, pendingPath: string): void {
139
+ const pid = deps.spawn({
140
+ cmd: 'sh',
141
+ args: ['-c', RUNNER_SCRIPT, 'sh', lockPath, pendingPath, 'code-review-graph', ...UPDATE_ARGS],
142
+ cwd: root,
143
+ env,
144
+ logFile: join(root, UPDATE_LOG_REL),
145
+ })
146
+ writeFileSync(lockPath, `${pid}\n`, { mode: 0o600 })
147
+ }
148
+
149
+ /** Acquire + spawn. On a spawn failure the placeholder lock is released so the next edit can retry. */
150
+ function acquireAndStart(deps: BackgroundDeps, root: string, env: SpawnSpec['env'], lockPath: string, pendingPath: string): boolean {
151
+ if (!tryAcquire(lockPath, deps.selfPid())) return false
152
+ try {
153
+ startRunner(deps, root, env, lockPath, pendingPath)
154
+ } catch (error) {
155
+ rmSync(lockPath, { force: true })
156
+ throw error
157
+ }
158
+ return true
159
+ }
160
+
161
+ export function requestUpdate(deps: BackgroundDeps, root: string, env: SpawnSpec['env']): UpdateRequest {
162
+ mkdirSync(join(root, '.adl', 'logs'), { recursive: true, mode: 0o700 })
163
+ const lockPath = join(root, LOCK_REL)
164
+ const pendingPath = join(root, PENDING_REL)
165
+
166
+ if (acquireAndStart(deps, root, env, lockPath, pendingPath)) return 'spawned'
167
+ if (!ownerIsLive(deps, lockPath)) {
168
+ rmSync(lockPath, { force: true })
169
+ if (acquireAndStart(deps, root, env, lockPath, pendingPath)) return 'spawned'
170
+ }
171
+ writeFileSync(pendingPath, '', { mode: 0o600 })
172
+ // The owner may have exited between the check and the marker write.
173
+ if (!ownerIsLive(deps, lockPath)) {
174
+ rmSync(lockPath, { force: true })
175
+ // The new runner clears the marker as its first step, so the queued edit is covered.
176
+ if (acquireAndStart(deps, root, env, lockPath, pendingPath)) return 'spawned'
177
+ }
178
+ return 'queued'
179
+ }
@@ -13,9 +13,10 @@
13
13
  */
14
14
 
15
15
  import type { HookInvocation, HookResult } from '../contract.ts'
16
+ import { defaultBackgroundDeps } from './background.ts'
16
17
  import { bunRunner, createCrgHandler } from './index.ts'
17
18
 
18
- const crgHandler = createCrgHandler({ run: bunRunner })
19
+ const crgHandler = createCrgHandler({ run: bunRunner, background: defaultBackgroundDeps })
19
20
 
20
21
  export async function handleCrg(inv: HookInvocation): Promise<HookResult> {
21
22
  return crgHandler(inv)
@@ -47,6 +47,7 @@
47
47
 
48
48
  import type { HookHandler, HookInvocation, HookResult } from '../contract.ts'
49
49
  import { spawnWithTimeout } from '../../lib/spawn-with-timeout.ts'
50
+ import { defaultBackgroundDeps, requestUpdate, type BackgroundDeps } from './background.ts'
50
51
  import { toDecideInput } from '../../pre-tool-use/handler.ts'
51
52
  import { invocations } from '../../rules/shell/invocation.ts'
52
53
 
@@ -99,8 +100,38 @@ function missingBinaryNote(subcommand: string, result: RunResult): string {
99
100
  return `crg ${subcommand}: ${BIN} ${reason}\n`
100
101
  }
101
102
 
102
- /** `after-file-edit.sh`: silent `update --skip-flows`, stdout `{}`, always exit 0. */
103
- async function afterFileEdit(run: Runner, inv: HookInvocation): Promise<HookResult> {
103
+ const UPDATE_LABEL = 'code-review-graph update --skip-flows'
104
+
105
+ /**
106
+ * `after-file-edit.sh`: `update --skip-flows`, stdout `{}`, always exit 0.
107
+ *
108
+ * With `background` (production wiring) the update is spawned DETACHED and
109
+ * single-flight (see `./background.ts`) and the hook returns at once: the
110
+ * update takes longer than the host's 5 s hook timeout on a large repo, so a
111
+ * synchronous run was killed every time and the graph never refreshed.
112
+ * Without it (unit tests of the plain runner path) the update runs inline.
113
+ */
114
+ async function afterFileEdit(run: Runner, inv: HookInvocation, background: BackgroundDeps | undefined): Promise<HookResult> {
115
+ if (background !== undefined) {
116
+ // Fail loud up front: the detached runner's own "command not found" would land only in crg-update.log.
117
+ if (background.resolve(BIN, inv.env.PATH) === null) {
118
+ const note = missingBinaryNote('after-file-edit', { status: null, stdout: '', stderr: '' })
119
+ inv.record?.({ reason: `ERROR ${BIN} not on PATH`, message: note.trim() })
120
+ return { stdout: '{}\n', stderr: note, exitCode: 0 }
121
+ }
122
+ try {
123
+ const outcome = requestUpdate(background, inv.root, inv.env)
124
+ inv.record?.({
125
+ reason: 'PASS',
126
+ downstream: outcome === 'spawned' ? `spawned ${UPDATE_LABEL} (background)` : 'update already running; queued',
127
+ })
128
+ return { stdout: '{}\n', stderr: '', exitCode: 0 }
129
+ } catch (error) {
130
+ const message = error instanceof Error ? error.message : String(error)
131
+ inv.record?.({ reason: `ERROR background ${UPDATE_LABEL}`, message })
132
+ return { stdout: '{}\n', stderr: `crg after-file-edit: could not start background update: ${message}\n`, exitCode: 0 }
133
+ }
134
+ }
104
135
  const result = await runCrg(run, inv, ['update', '--skip-flows'])
105
136
  return { stdout: '{}\n', stderr: missingBinaryNote('after-file-edit', result), exitCode: 0 }
106
137
  }
@@ -156,11 +187,11 @@ async function sessionStop(run: Runner, inv: HookInvocation): Promise<HookResult
156
187
  return { stdout: `${JSON.stringify({ message, passed: true })}\n`, stderr: note, exitCode: 0 }
157
188
  }
158
189
 
159
- export function createCrgHandler(deps: { run: Runner }): HookHandler {
190
+ export function createCrgHandler(deps: { run: Runner; background?: BackgroundDeps }): HookHandler {
160
191
  return async (inv: HookInvocation): Promise<HookResult> => {
161
192
  switch (inv.subcommand) {
162
193
  case 'after-file-edit':
163
- return afterFileEdit(deps.run, inv)
194
+ return afterFileEdit(deps.run, inv, deps.background)
164
195
  case 'pre-commit':
165
196
  return preCommit(deps.run, inv)
166
197
  case 'session-stop':
@@ -205,5 +236,5 @@ export const bunRunner: Runner = async (cmd, args, opts) => {
205
236
  }
206
237
  }
207
238
 
208
- export const handleCrg: HookHandler = createCrgHandler({ run: bunRunner })
239
+ export const handleCrg: HookHandler = createCrgHandler({ run: bunRunner, background: defaultBackgroundDeps })
209
240
  export default handleCrg
@@ -7,8 +7,8 @@
7
7
  *
8
8
  * The original's eighth predicate -- any path with an `.agents` segment --
9
9
  * was dropped by owner decision on 2026-09-15. `.agents/` is reconciled
10
- * centrally every six hours (global-tools `git-maintain` refreshes repo-local
11
- * surfaces in every managed repo), so a trashed copy is restored rather than
10
+ * centrally every 12 hours (`adl reconcile`'s repository step runs `adl install --repo`,
11
+ * which refreshes repo-local surfaces in every managed repo), so a trashed copy is restored rather than
12
12
  * lost, and `trash(1)` is itself recoverable from ~/.Trash. Protecting it here
13
13
  * blocked routine work to guard against something the fleet already repairs.
14
14
  *
@@ -31,15 +31,14 @@ export const gitRm: RuleSpec = {
31
31
  },
32
32
  message(_finding: Finding, ctx: MessageContext) {
33
33
  return {
34
- user: 'Blocked git rm. It deletes tracked content and stages the deletion in one step; agents leave the file in place and hand the removal to you.',
34
+ user: 'Blocked git rm. It deletes tracked content and stages the deletion in one step; agents remove a tracked file with trash instead.',
35
35
  agent:
36
36
  'DENIED git rm. Every form is refused - with flags or without, --cached included. `git rm` deletes ' +
37
37
  'tracked content AND stages the deletion in one step, so it is both the delete and the commit-prep ' +
38
- 'for it, and it was the last deletion verb this hook left open after rm and trash were closed. ' +
39
- 'There is no flag that makes it acceptable and no `git rm` that this hook allows. ' +
40
- 'If a tracked file genuinely has to go: leave it exactly where it is, finish the rest of the change, ' +
41
- 'and tell the user which paths should be removed and why - removal is theirs to run, the same rule ' +
42
- `that already applies to committing a protected tree. ${gitAgentTail(ctx.snippet)}`,
38
+ 'for it. There is no flag that makes it acceptable and no `git rm` that this hook allows. ' +
39
+ 'If a tracked file genuinely has to go, run `trash <path>` (recoverable from ~/.Trash; fs.trash only ' +
40
+ 'refuses the working directory, a system root, $HOME and .git), then `git add <path>` to stage the ' +
41
+ `deletion with the rest of the change. ${gitAgentTail(ctx.snippet)}`,
43
42
  }
44
43
  },
45
44
  corpus: {
@@ -37,7 +37,7 @@ export const toolGrep: RuleSpec = {
37
37
  category: 'tool',
38
38
  severity: 'deny',
39
39
  enabledByDefault: true,
40
- summary: 'Deny the grep family; steer at rg / ast-grep / search / ctx_search.',
40
+ summary: 'Deny the grep family; steer at ast-grep / search / crg / rg / ctx_search.',
41
41
  params: noParams,
42
42
  match(inv: Invocation): Finding | null {
43
43
  if (inv.kind !== 'grep') return null
@@ -45,15 +45,15 @@ export const toolGrep: RuleSpec = {
45
45
  },
46
46
  message(_finding: Finding, ctx: MessageContext) {
47
47
  return {
48
- user: 'Blocked grep. Use `rg` (same job, faster, repo-aware), `ast-grep` for structural search, or `ctx_search` for the indexed search.',
48
+ user: 'Blocked grep. Pick the tool for the question: `ast-grep` for code shapes, `search` for docs by meaning, `crg` for callers and impact, `rg` for literal text, `ctx_search` for the indexed code search.',
49
49
  agent:
50
- 'DENIED grep. Use one of these instead - all are on PATH and already allowed:\n' +
51
- ' rg drop-in for grep, including stdin: `npm test 2>&1 | rg "pass|fail"`\n' +
52
- ' ast-grep structural/syntax search, for shapes rather than strings\n' +
53
- ' search hybrid semantic + keyword over indexed docs (`search query "<text>"`)\n' +
50
+ 'DENIED grep. Match the tool to the question - in this order, all on PATH and already allowed:\n' +
51
+ ' ast-grep code STRUCTURE: shapes rather than strings (`ast-grep -p "foo($A)" -l ts`)\n' +
52
+ ' search docs and plans by MEANING: hybrid semantic + keyword (`search query "<text>"`)\n' +
53
+ ' crg the code GRAPH: callers, callees, impact (`crg query callers_of <fn>`, `crg impact`)\n' +
54
+ ' rg LITERAL text or regex, including stdin: `npm test 2>&1 | rg "pass|fail"`\n' +
54
55
  ' ctx_search the indexed code search (action=symbol for an exact symbol)\n' +
55
- 'This applies to a pure output filter too: `rg` reads stdin exactly as grep does, ' +
56
- 'so there is no capability being removed here, only a slower tool. ' +
56
+ 'A pure output filter is no exception: `rg` reads stdin exactly as grep does. ' +
57
57
  `Blocked command: ${ctx.snippet}`,
58
58
  }
59
59
  },
@@ -9,7 +9,7 @@ repo's own `AGENTS.md`.
9
9
  - If a task is ambiguous, ask exactly one clarifying question.
10
10
 
11
11
  ## Truthfulness (non-negotiable)
12
- - **No fake-green tests** — never edit an assertion to force a pass. Red is honest.
12
+ - **No fake-green tests.** A test changes only when the intended behaviour changes, never to make unintended behaviour pass. Red is honest.
13
13
  - **No silent fallback or degrade** — if the real path fails, fail loud and stop.
14
14
  - **No silent drops** — no `|| true`, no swallowed errors, no skipped gates.
15
15
  - **No fakes** — no stubs, placeholders or TODOs passed off as done.
@@ -69,7 +69,9 @@ repo's own `AGENTS.md`.
69
69
 
70
70
  ## Tests
71
71
  - For features, bugfixes and non-trivial refactors: **tests first**, confirmed failing for the right reason; then minimal code to pass; then a separate review pass.
72
- - Every change ships a **new** test. Never edit an existing test to make work pass.
72
+ - Every change ships a **new** test.
73
+ - When a change deliberately alters behaviour, rework the existing tests that assert the old behaviour, in the same change. Name each reworked test and the decision that changed it.
74
+ - **Never fake green.** Don't loosen, skip or delete an assertion, or set an expectation to whatever the code returns, to hide a failure you didn't intend.
73
75
  - Skip for pure config, docs and trivial one-liners.
74
76
 
75
77
  ## Delegation & parallelism
@@ -29,7 +29,6 @@ import {
29
29
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
30
30
  import { fileURLToPath } from 'node:url'
31
31
  import { BEGIN as KIMI_BEGIN, END as KIMI_END } from './kimi-hooks.ts'
32
- import { declaredLaunchAgentPlists } from '../../../packages/fleet/src/launch-agents.ts'
33
32
 
34
33
  const LAUNCH_AGENTS_DIR = 'Library/LaunchAgents'
35
34
 
@@ -40,8 +39,6 @@ const LAUNCH_AGENTS_DIR = 'Library/LaunchAgents'
40
39
  * these strings verbatim) contain, case-insensitively, one of these agent
41
40
  * tool tokens. Everything else (GoogleUpdater, Keystone, other application
42
41
  * updaters) is not an agent surface and is neither a candidate nor reported.
43
- * LaunchAgents enact-vps declares in fleet/manifest.json are always in
44
- * scope and protected by identity (see loadProtectionList).
45
42
  */
46
43
  export const AGENT_TOOL_TOKENS = [
47
44
  'lean-ctx',
@@ -295,7 +292,6 @@ export const EMPTY_PROTECTION: ProtectionList = {
295
292
  }
296
293
 
297
294
  export const DEFAULT_PROTECTION_PATH = fileURLToPath(new URL('../protected.json', import.meta.url))
298
- export const DEFAULT_MANIFEST_PATH = fileURLToPath(new URL('../../fleet/manifest.json', import.meta.url))
299
295
 
300
296
  export interface SweepNote {
301
297
  readonly level: 'warn' | 'ok'
@@ -605,18 +601,12 @@ export function decodeProtectionList(value: unknown): ProtectionList {
605
601
  }
606
602
 
607
603
  /**
608
- * The checked-in protection list plus every LaunchAgent plist enact-vps
609
- * itself ships (declared under `launch_agents` in fleet/manifest.json).
610
- * Shipped-job protection is derived from the manifest, never hand-listed.
604
+ * The checked-in protection list. (It used to add every LaunchAgent the fleet
605
+ * manifest declared; the manifest declares none since git-maintain was retired
606
+ * on 2026-09-30, so nothing is derived from it any more.)
611
607
  */
612
- export function loadProtectionList(path = DEFAULT_PROTECTION_PATH, manifestPath = DEFAULT_MANIFEST_PATH): ProtectionList {
613
- const list = decodeProtectionList(JSON.parse(readFileSync(path, 'utf8')))
614
- const declared = declaredLaunchAgentPlists(JSON.parse(readFileSync(manifestPath, 'utf8')))
615
- const listed = new Set(list.paths.map((entry) => entry.path))
616
- const derived = declared
617
- .filter((entry) => !listed.has(entry.path))
618
- .map((entry) => ({ path: entry.path, reason: `LaunchAgent shipped by enact-vps (fleet/manifest.json ${entry.declaredAt})` }))
619
- return { ...list, paths: [...list.paths, ...derived] }
608
+ export function loadProtectionList(path = DEFAULT_PROTECTION_PATH): ProtectionList {
609
+ return decodeProtectionList(JSON.parse(readFileSync(path, 'utf8')))
620
610
  }
621
611
 
622
612
  function tomlHeaderName(header: string): string {
@@ -15,7 +15,7 @@
15
15
  "scripts": {
16
16
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
17
17
  "typecheck": "tsc -p tsconfig.json --noEmit",
18
- "test": "bun run typecheck && bun test"
18
+ "test": "bun run typecheck && bun test --coverage"
19
19
  },
20
20
  "dependencies": {
21
21
  "@iarna/toml": "^2.2.5",
@@ -5,8 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "bun src/mcp-server.ts",
8
- "test": "bun test",
9
- "test:coverage": "bun test --coverage",
8
+ "test": "bun test --coverage",
10
9
  "typecheck": "tsc --noEmit"
11
10
  },
12
11
  "dependencies": {