@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.33 → 0.1.35

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 (32) hide show
  1. package/bin/adl +2 -2
  2. package/bin/reconcile +4 -3
  3. package/package.json +1 -1
  4. package/packages/agent-hooks/README.md +14 -0
  5. package/packages/agent-hooks/assets/opencode-plugin.template.ts +140 -104
  6. package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -18
  7. package/packages/agent-hooks/src/deploy/render.ts +12 -3
  8. package/packages/agent-hooks/src/rules/impl/git-branch.ts +63 -0
  9. package/packages/agent-hooks/src/rules/impl/git-helpers.ts +91 -3
  10. package/packages/agent-hooks/src/rules/impl/git-history.ts +4 -13
  11. package/packages/agent-hooks/src/rules/impl/git-target.ts +423 -0
  12. package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +8 -7
  13. package/packages/agent-hooks/src/rules/ports.ts +36 -0
  14. package/packages/agent-hooks/src/rules/registry.ts +3 -2
  15. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +1 -1
  16. package/packages/agent-settings/README.md +10 -4
  17. package/packages/agent-settings/assets/GLOBAL-AGENTS.md +5 -0
  18. package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +40 -0
  19. package/packages/agent-settings/src/global-agents.ts +43 -8
  20. package/packages/agent-skills/assets/adl-mechanics/SKILL.md +38 -0
  21. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +1 -1
  22. package/packages/agent-skills/assets/to-feature/SKILL.md +1 -1
  23. package/packages/agent-skills/assets/to-workitems/SKILL.md +1 -1
  24. package/packages/agent-skills/assets/to-workitems/references/AZURE-DEVOPS-HIERARCHY.md +3 -0
  25. package/packages/azdo/src/lib/agent-identity.ts +1 -1
  26. package/packages/repo-controls/README.md +12 -7
  27. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +60 -11
  28. package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +28 -0
  29. package/packages/repo-controls/assets/scaffold/root/INVARIANTS.md +17 -0
  30. package/packages/repo-controls/src/cli.ts +23 -5
  31. package/packages/repo-controls/src/constants.ts +2 -15
  32. package/packages/repo-controls/src/install.ts +15 -6
@@ -0,0 +1,40 @@
1
+ # Tarun's global agent instructions
2
+
3
+ My personal preferences, for every agent on every machine I own, for coding and non-coding work alike.
4
+ Where a repository has its own AGENTS.md, that file and the files it links add the repository's rules. They never relax Truthfulness, Verification before claiming done or Scope discipline here; on any other conflict, ask.
5
+
6
+ ## Communication
7
+ - Address me as **Tarun**.
8
+ - **Answer first.** No greeting, no "let me", no recap of what was just done, no closers.
9
+ - One idea per sentence. Short beats polite. Articles can go; not, never, no, only never do. Numbers, paths, commands exact.
10
+ - Bullet points over prose.
11
+ - **Open points:** one numbered list of open decisions per conversation. Show it only when it changes — item added or resolved. After a long gap or compaction, restate the full list. That is when it gets lost.
12
+ - **Questions:** one at a time, one or two sentences. Options inline: `A) … B) …`. A long multi-part question means split the work, not more prose.
13
+ - Full sentences for security warnings, irreversible actions, step-by-step orders, and when I'm confused. Then short resumes.
14
+ - No performance. If short phrasing isn't shorter, plain English wins. Never rewrite my prompts.
15
+
16
+ ## Truthfulness (non-negotiable)
17
+ - Never present something as done, working or checked when it is not. A failure reported plainly is worth more than a success that is not real.
18
+ - **No silent fallback or degrade** — if the real path fails, say so loudly and stop.
19
+ - **No silent drops** — no skipped steps, ignored errors or quietly narrowed scope.
20
+ - **No fakes** — no placeholders, stand-ins or TODOs passed off as finished.
21
+ - Unverified claims never enter anything that lasts: documents, messages, notes, memory.
22
+ - Never read secret stores or credential files, never pass a token or password in a command or message, and never claim to be me or set anything that marks the session as human (drafting in my name when I ask is fine).
23
+
24
+ ## Verification before claiming done
25
+ - **Evidence over assertions:** show what you ran or read, and make the check falsifiable ("did you search the source?" has an answer; "did you verify?" does not).
26
+ - **A search shows what was returned, not what exists.** Before saying something is absent, say how you looked, and run a control search the same way for something you know is there. Search wide first, then narrow. No hits for both means you searched the wrong place.
27
+ - **Ask the authoritative source, not the one in front of you:** the original over a copy, the live system over a description of it.
28
+ - **Re-check before reporting.** Cite the check you are reporting, not one you remember. Never claim "checked everything" without checking every case.
29
+ - A requirement you are about to not meet is a **stop**: tell me; never quietly decide to skip it.
30
+
31
+ ## Scope discipline
32
+ - Work in your own working directory. Touch only what is in scope: the files, accounts, people and systems the task names.
33
+ - Anything outside scope is raised as an open point for me (or recorded where that work is tracked): never silently done, never silently dropped.
34
+
35
+ ## Delegation & parallelism
36
+ - Where the surface supports helper agents, the main agent orchestrates and verifies and helpers do the hands-on work; small tasks may be done directly.
37
+ - Split independent work into **concurrent agents** launched together — never sequential when it can fan out.
38
+ - Match the model to the task: strongest for deep analysis and judgment, fastest for quick lookups.
39
+ - Author and reviewer are **distinct passes in separate contexts**. Never self-approve.
40
+ - Prefer speed through parallel runs and fast models; never skip verification for speed.
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Deploy the canonical `packages/agent-settings/assets/GLOBAL-AGENTS.md` out
2
+ * Deploy the canonical `packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md` out
3
3
  * to every AI-agent host's own global instruction file, so each host reads
4
4
  * the same content: `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`,
5
5
  * `~/.cursor/AGENTS.md`, `~/.kimi-code/AGENTS.md`,
6
6
  * `~/.config/opencode/AGENTS.md`.
7
7
  *
8
- * The canonical source is committed in this repo (`../assets/GLOBAL-AGENTS.md`
8
+ * The canonical source is committed in this repo (`../assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md`
9
9
  * relative to this file), not read from any other repo's checkout at
10
10
  * runtime — two different repos fighting over the same global target file is
11
11
  * exactly the collision this replaces.
12
12
  *
13
- * Ownership model: GLOBAL-AGENTS.md is the single source of truth for every
13
+ * Ownership model: TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md is the single source of truth for every
14
14
  * target. Deploy is always forced — there is no opt-out flag:
15
15
  * - missing target -> created from the source.
16
16
  * - content === source -> untouched (true no-op).
@@ -18,7 +18,7 @@
18
18
  * stale, both are drift). These exact files are
19
19
  * authoritative adl roots: replacement does
20
20
  * not retain a displaced copy.
21
- * Any change to a host's global instructions must go through GLOBAL-AGENTS.md.
21
+ * Any change to a host's global instructions must go through TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md.
22
22
  * Uninstall removes those same owned files regardless of their current bytes.
23
23
  */
24
24
 
@@ -31,6 +31,7 @@ import {
31
31
  rmSync,
32
32
  writeFileSync,
33
33
  } from 'node:fs'
34
+ import { hostname as osHostname } from 'node:os'
34
35
  import { dirname, isAbsolute, join, resolve } from 'node:path'
35
36
  import { fileURLToPath } from 'node:url'
36
37
 
@@ -47,7 +48,29 @@ export const AGENT_TARGETS: readonly AgentTarget[] = [
47
48
  { relativeDir: '.config/opencode', file: 'AGENTS.md' },
48
49
  ]
49
50
 
50
- export const DEFAULT_SOURCE_PATH = fileURLToPath(new URL('../assets/GLOBAL-AGENTS.md', import.meta.url))
51
+ export const DEFAULT_SOURCE_PATH = fileURLToPath(new URL('../assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md', import.meta.url))
52
+
53
+ export const DEFAULT_MANIFEST_PATH = fileURLToPath(new URL('../../fleet/manifest.json', import.meta.url))
54
+
55
+ export type FleetScope = { readonly inFleet: true; readonly device: string } | { readonly inFleet: false; readonly note: string }
56
+
57
+ export function deviceNameOf(hostname: string): string {
58
+ return hostname.split('.')[0] ?? hostname
59
+ }
60
+
61
+ // Fleet-only deploy rule and its rationale: README "Global agent instructions".
62
+ export function resolveFleetScope(hostname: string, manifestPath: string): FleetScope {
63
+ let manifest: { mac?: { devices?: object }; linux?: { devices?: object } }
64
+ try {
65
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
66
+ } catch (cause) {
67
+ throw new Error(`cannot read fleet manifest ${manifestPath}: ${messageOf(cause)}`)
68
+ }
69
+ const device = deviceNameOf(hostname)
70
+ const names = [...Object.keys(manifest.mac?.devices ?? {}), ...Object.keys(manifest.linux?.devices ?? {})]
71
+ if (names.includes(device)) return { inFleet: true, device }
72
+ return { inFleet: false, note: `skipped: '${device}' is not a device in packages/fleet/manifest.json, so no global file is written` }
73
+ }
51
74
 
52
75
  export type TargetState = 'missing' | 'matches' | 'drifted'
53
76
 
@@ -181,23 +204,35 @@ function readSource(sourcePath: string): string {
181
204
  }
182
205
 
183
206
  function usage(): never {
184
- throw new Error('usage: global-agents --home ABSOLUTE_PATH --mode deploy|status|uninstall')
207
+ throw new Error('usage: global-agents --home ABSOLUTE_PATH --mode deploy|status|uninstall [--host NAME] [--manifest PATH]')
185
208
  }
186
209
 
187
- export function main(argv: readonly string[]): void {
210
+ export function main(argv: readonly string[], hostname: string = osHostname()): void {
188
211
  let home = ''
189
212
  let mode = 'status'
213
+ let host = hostname
214
+ let manifest = DEFAULT_MANIFEST_PATH
190
215
 
191
216
  for (let index = 0; index < argv.length; index += 1) {
192
217
  const argument = argv[index]
193
218
  if (argument === '--home') home = argv[++index] ?? ''
194
219
  else if (argument === '--mode') mode = argv[++index] ?? ''
220
+ else if (argument === '--host') host = argv[++index] ?? ''
221
+ else if (argument === '--manifest') manifest = argv[++index] ?? ''
195
222
  else usage()
196
223
  }
197
- if (home.length === 0 || !['deploy', 'status', 'uninstall'].includes(mode)) usage()
224
+ if (home.length === 0 || host.length === 0 || manifest.length === 0 || !['deploy', 'status', 'uninstall'].includes(mode)) usage()
198
225
 
199
226
  const sourceContent = readSource(DEFAULT_SOURCE_PATH)
200
227
 
228
+ if (mode !== 'uninstall') {
229
+ const scope = resolveFleetScope(host, manifest)
230
+ if (!scope.inFleet) {
231
+ console.log(`global-agents: ${scope.note}`)
232
+ return
233
+ }
234
+ }
235
+
201
236
  if (mode === 'status') {
202
237
  const statuses = inspectGlobalAgents(home, sourceContent)
203
238
  for (const status of statuses) console.log(`${status.state}: ${status.path}`)
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: adl-mechanics
3
+ description: How agents reach Azure DevOps in an adl-controlled repository — agent identity, the git front controller, the adl MCP tools and their CLI fallback, repository-control hooks, and when `adl run` may start a sub-agent.
4
+ ---
5
+
6
+ # adl mechanics
7
+
8
+ How adl behaves on every machine and in every repository it controls.
9
+
10
+ ## Use When
11
+
12
+ - You are about to run a git network command, open or update a PR, read or write a work item, or start a build.
13
+ - A command is refused for identity, credentials or hooks, and you need to know why.
14
+ - The operator asks you to start a sub-agent session with `adl run`.
15
+
16
+ ## Agent identity
17
+
18
+ - Every Azure DevOps access (git, PRs, work items, builds, reads included) runs as an agent identity, never as the human. The default is the machine's default agent, set by fleet reconcile; it can always be overridden.
19
+ - **Override:** prefix a command with `ADL_AGENT_ID_AGENT=<agent-id>`. An MCP tool call can't carry a prefix, so it uses the session environment or the machine default; the CLI fallback (`adl mcp cli <command>`) takes the prefix like any command.
20
+ - Never read vault files. Never pass a token.
21
+ - **Never set `ADL_HUMAN_AZDO`.** It is the owner's human-terminal marker for `git-ado`'s and `adl id whoami`'s fail-closed human pass-through (stdin is a TTY and `ADL_HUMAN_AZDO=1`, exactly). An agent carrying it would let a child process forge that pass-through.
22
+
23
+ ## Git and Azure DevOps operations
24
+
25
+ - Use ordinary `git` for every Git operation. The managed `/usr/local/bin/git` front controller hands local and non-Azure DevOps work to the system Git, and routes HTTPS Azure DevOps network commands through `adl-agent-id exec --agent "$ADL_AGENT_ID_AGENT"`. `git-ado` handles the credential boundary; there is no second account or secret location.
26
+ - PRs, work items and builds go through the `adl` MCP server (`adl mcp serve`): `pr_list_or_get`, `pr_create_or_update`, `pr_review`, `build_list_or_get`, `build_run`, `workitem_list_or_get`, `workitem_create_or_update`. If the server is not working, fall back to `adl mcp cli <command>`.
27
+
28
+ ## Repository controls
29
+
30
+ - Run via `adl repo controls`. Git hooks run pre-commit (local checks) and pre-push (Azure DevOps governance).
31
+ - Each repository is reconciled on its own, with its `.agents/`, `.claude/`, `.codex/`, `.cursor/`, `.kimi-code/` and `.opencode/` projections.
32
+
33
+ ## Sub-agent sessions (`adl run`)
34
+
35
+ - Only on the operator's explicit request; an agent never starts one on its own. It is not an identity route and not a fallback for anything.
36
+ - **Headless:** `adl run <surface> --headless "<prompt>"`, surface one of `claude`, `codex`, `opencode`, `kimi`, `cursor`.
37
+ - **Identity:** add `--as <agent>`.
38
+ - **Model:** add `--model <model>`. adl passes each surface's own flag (claude and cursor `--model`; codex, opencode and kimi `-m`); a surface without one refuses it (exit 2). `--model` cannot be combined with `adl run -- <cmd>`.
@@ -92,7 +92,7 @@ run_ts = sys.argv[2]
92
92
  today = sys.argv[3]
93
93
  log_files = sys.argv[4:]
94
94
 
95
- cutoff = datetime.datetime.fromisoformat(today + "T00:00:00Z") - datetime.timedelta(days=days-1)
95
+ cutoff = datetime.datetime.fromisoformat(today + "T00:00:00+00:00") - datetime.timedelta(days=days-1)
96
96
 
97
97
  # Per (host, day), tally. Dedupe by (ts, command, rule) within each log.
98
98
  per_host_day = collections.defaultdict(lambda: collections.Counter())
@@ -8,7 +8,7 @@ disable-model-invocation: true
8
8
 
9
9
  Open a **Feature** on the board and give it a plan folder to live in.
10
10
 
11
- **A plan is a Feature, and a Feature is a directory** — `plans/features/az<id>-<slug>/spec.md`, named for the work item it is. This skill creates that Feature and that directory; `to-workitems` later fills its `items/` with PBIs and Bugs parented under it. Where the repo carries a `plans/AGENTS.md`, it governs; otherwise follow the global `## Plans and work items` contract in your host's agent instructions.
11
+ **A plan is a Feature, and a Feature is a directory** — `plans/features/az<id>-<slug>/spec.md`, named for the work item it is. This skill creates that Feature and that directory; `to-workitems` later fills its `items/` with PBIs and Bugs parented under it. Where the repo carries a `plans/AGENTS.md`, it governs; otherwise follow `## Plans and work items` in the repository's `CONTRIBUTING.md`.
12
12
 
13
13
  Do NOT interview the user — synthesize what you already know from the conversation and the codebase.
14
14
 
@@ -8,7 +8,7 @@ disable-model-invocation: true
8
8
 
9
9
  Put a plan's **backlog items** on the board.
10
10
 
11
- **A plan is a Feature. An item file is a PBI or a Bug.** One file each. The plan and its `## Items` table are written before this skill runs; the item files are not, because each is named for an id only this skill allocates. Where the repo carries a `plans/AGENTS.md`, it governs the layout; otherwise follow the global `## Plans and work items` contract in your host's agent instructions. [references/AZURE-DEVOPS-HIERARCHY.md](references/AZURE-DEVOPS-HIERARCHY.md) carries the board rules both depend on.
11
+ **A plan is a Feature. An item file is a PBI or a Bug.** One file each. The plan and its `## Items` table are written before this skill runs; the item files are not, because each is named for an id only this skill allocates. Where the repo carries a `plans/AGENTS.md`, it governs the layout; otherwise follow `## Plans and work items` in the repository's `CONTRIBUTING.md`. [references/AZURE-DEVOPS-HIERARCHY.md](references/AZURE-DEVOPS-HIERARCHY.md) carries the board rules both depend on.
12
12
 
13
13
  ## Use When
14
14
 
@@ -47,6 +47,9 @@ expressed by parenting; they need `System.LinkTypes.Dependency-Reverse` links,
47
47
  which `workitem_create_or_update`'s `predecessors` argument creates — one link
48
48
  per id, in the order given.
49
49
 
50
+ Supplying `parentId` on an **update** moves the item: the old parent link is
51
+ removed, never kept alongside the new one.
52
+
50
53
  `predecessors` is the only argument that creates them. It has no alias at the
51
54
  tool level: `blockedBy` is not read.
52
55
 
@@ -147,7 +147,7 @@ export interface AgentIdentityResolution {
147
147
  * - `env.ADL_HUMAN_AZDO === '1'`: the EXACT string `"1"`, not `"true"`,
148
148
  * not `"yes"`, not trimmed whitespace -- Tarun exports it from his own
149
149
  * `.zshrc`. An agent must never set it (packages/agent-settings'
150
- * GLOBAL-AGENTS.md, "Agent identity"; the hook's denial of an agent
150
+ * TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md, "Agent identity"; the hook's denial of an agent
151
151
  * setting it is packages/agent-hooks work, not this module's).
152
152
  *
153
153
  * No fallback, no trimming, no case-insensitivity: a near-miss value is
@@ -359,7 +359,7 @@ guidance (see [Compliance seeding](#compliance-seeding) below). The decisions
359
359
  and the gate list live in
360
360
  [`docs/architecture/compliance-controls.md`](../../docs/architecture/compliance-controls.md#strict-activation-and-switching-a-gate-off).
361
361
  `install --rewire` (what fleet reconcile runs on every enrolled repo) seeds nothing,
362
- scaffolds nothing and does not grade: it only re-deploys the hook stubs, `.workflows`
362
+ scaffolds nothing (no root template either) and does not grade: it only re-deploys the hook stubs, `.workflows`
363
363
  and the lock, and leaves adl-config.toml byte-identical. Without `uv`, the `adl install`
364
364
  wrapper prints a one-line warning that grading is skipped, finishes deploying and exits 0;
365
365
  pre-commit still fails loudly without `uv`. See
@@ -551,12 +551,17 @@ doctor report and never repairs or stages the managed surface: run
551
551
  An existing `[controls.compliance]` with no `trees` table gets it appended; one
552
552
  that already has a `trees` table (any keys) is left byte-identical.
553
553
 
554
- Install also scaffolds the repository entry files `AGENTS.md` and `CLAUDE.md`
555
- (`@AGENTS.md`) at the root when absent (never overwritten) — `SCAFFOLD_ROOT_FILES` in
556
- `src/constants.ts` is the list. `INVARIANTS.md` is deployed from
557
- `assets/GLOBAL-INVARIANTS.md`, the invariants that apply to ALL repositories, only when
558
- the target has none (never overwritten); the engine's root gate still requires
559
- `INVARIANTS.md`. See Scaffolding below
554
+ ### Root templates
555
+
556
+ The root templates `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md` and `INVARIANTS.md`
557
+ (`SCAFFOLD_ROOT_FILES` in `src/constants.ts`, sources in `assets/scaffold/root/`) are
558
+ written, when absent, only by a run that creates `adl-config.toml` (`adl install` on a
559
+ repository without one, or `adl migrate`; the internal `install --config-created` flag, set by `adl install`,
560
+ says so and is deliberately absent from help). `--rewire`, fleet reconcile and any later install never write them, even when a
561
+ file is missing. The only overwrite is `install --force-templates[=<file,...>]`: bare means
562
+ all four, a comma list picks files, an unknown name exits 2 listing the valid ones, and it
563
+ cannot be combined with `--rewire`. Each overwrite is an `update` action, also in `--json`; a forced template whose bytes already match is left unchanged and reported as no action.
564
+ The engine's root gate still requires `INVARIANTS.md`. See Scaffolding below
560
565
  for which folders it creates.
561
566
 
562
567
  #### Grading
@@ -1,22 +1,71 @@
1
1
  # Agent entry point
2
2
 
3
- This is the first file a human or agent should read on arriving at this
4
- repository. Edit it to describe what this repository is; the sections below
5
- describe the layout the adl repo controls enforce.
3
+ First file a human or agent reads here. Keep it short.
6
4
 
7
- ## Layout
5
+ ## What this repository is
6
+ - <!-- Owner: one or two lines on what this repo is and who uses it. -->
7
+
8
+ ## Layout (paths as declared in `[controls.compliance.trees]`)
8
9
 
9
10
  | Path | What lives there |
10
11
  | --- | --- |
11
12
  | `docs/` | reference documentation, see `docs/AGENTS.md` |
12
13
  | `plans/` | Features and the PBIs/Bugs under them, see `plans/AGENTS.md` |
13
14
  | `workflows/` | CI pipelines and build/release scripts, see `workflows/AGENTS.md` |
14
- | `.workflows/` | managed hook stubs and lock, rebuilt by `adl repo controls install`; never hand-edit |
15
-
16
- ## Rules
15
+ | `.workflows/` | managed hook stubs and lock; rebuilt by `adl repo controls install`, never hand-edit |
17
16
 
18
- - This repository is graded by `adl-compliance` on every commit; the enabled
19
- gates are `[controls.compliance].enabled` in `adl-config.toml`. A failing
20
- gate is fixed, not skipped.
21
- - `INVARIANTS.md` lists what this repository must never regress.
17
+ ## Where to look next
18
+ - `CONTRIBUTING.md`: plans and work items, commits and PRs, release flow, tasks ledger.
19
+ - `INVARIANTS.md`: what must never regress, how each is verified, protected paths.
20
+ - Graded on every commit by `adl repo controls compliance run`; enabled gates are `[controls.compliance].enabled` in `adl-config.toml`. A failing gate is fixed, not skipped.
22
21
  - Work-item status lives on the board, never in a file.
22
+
23
+ ## Keep this guidance alive
24
+ - **When:** you were corrected by the owner, a gate or test failed for a reason not written here, or you learned a rule the hard way.
25
+ - **Where (one place only):**
26
+ - convention or workflow → `CONTRIBUTING.md`
27
+ - must-never-regress rule → `INVARIANTS.md`, only with its verifying gate/test/command; no check, no invariant
28
+ - orientation (what lives where) → `AGENTS.md`
29
+ - folder-specific → that folder's `AGENTS.md`
30
+ - applies to every repository → tell the owner; never edit global files
31
+ - **How:** put the edit in the same commit/PR that taught you it; edit the existing bullet instead of adding a near-duplicate; one bullet, at most 2 lines.
32
+ - **Never record:** secrets, ids/URLs of one-off items, session or progress state, unverified claims, rules a gate already enforces and explains in its own message.
33
+
34
+ ## Working defaults for code
35
+ Tarun's global instructions cover communication, truthfulness, verification, scope and delegation. These are the coding defaults. Edit them for this repository, except "Truthfulness and verification in code", which a repository may add to but never relax.
36
+
37
+ ### Truthfulness and verification in code
38
+ - No `|| true`, no swallowed errors, no skipped gates.
39
+ - Negative claims need source-level proof: grep the implementation, not a tool listing. Search the whole package first, then narrow.
40
+ - Before reporting what the repository contains, ask the remote (`git show origin/<branch>:<path>`); before reporting a shipped tool's behaviour, ask the installed binary.
41
+ - Name the ref. Where a change is in flight, two branches answer differently and both are right about themselves; say which one you asked.
42
+ - Deployed is not running: a long-lived server or daemon keeps the code it started with. When a fix is installed and behaviour has not changed, restart the process first.
43
+
44
+ ### Checkouts
45
+ - Commit rules: `CONTRIBUTING.md`.
46
+ - At most 3 checkouts per repository, including the main folder; git refuses the 4th and names the command to use. Reconcile old worktrees first (merge, then `git worktree remove <path>`).
47
+ - Prefer the one checkout; parallel subagents share it, each with its own write scope. Only the orchestrator commits.
48
+
49
+ ### Tests
50
+ - 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.
51
+ - Every code change ships a new test. Exempt: pure config, docs, trivial one-liners.
52
+ - A test changes only when the intended behaviour changes. Don't loosen, skip or delete an assertion, or set an expectation to whatever the code returns, to hide a failure. Red is honest.
53
+ - When a change deliberately alters behaviour, rework the tests that assert the old behaviour in the same change; name each one and the decision that changed it.
54
+
55
+ ### Fixing things
56
+ - Fix at the source — the primitive, token or shared layer — so it propagates everywhere, not a narrow downstream patch.
57
+ - Consult official docs before coding against an unfamiliar SDK, framework or API.
58
+
59
+ ### Context tooling
60
+ - Match the tool to the question (when installed; otherwise `rg`):
61
+ - Orient in unfamiliar code → `crg architecture`, `crg communities`.
62
+ - Symbols and semantic code queries → `crg search "<text>"`.
63
+ - Callers, callees, blast radius → `crg query callers_of|callees_of|tests_for|importers_of <target>`, `crg impact`.
64
+ - Unreferenced code → `crg dead-code`.
65
+ - A code shape rather than a string → `ast-grep`.
66
+ - Prose in documentation folders (`docs/`, `plans/`) → `search <text>` (hybrid semantic + keyword).
67
+ - Literal text or regex → `rg`. Never `grep`; never `find | grep`, which filters file names and misses the file whose contents match.
68
+ - Files by name or tree → the host's native file-listing tools.
69
+
70
+ ### Temporary files
71
+ - Scratch and throwaway work goes in `.scratchpad/` at the repository root, never `/tmp` or `/private/tmp`. Create it if missing; it is gitignored. Real deliverables go to their proper home.
@@ -0,0 +1,28 @@
1
+ # Contributing
2
+
3
+ How to contribute to THIS repository. Orientation is in `AGENTS.md`; what must never regress, and the protected paths, are in `INVARIANTS.md`. Tree paths are those declared in `[controls.compliance.trees]`; `plans/` is the default.
4
+
5
+ ## Plans and work items
6
+ - A plan is a Feature; a backlog item is a PBI or a Bug. One file each; the board is the only place their state lives.
7
+ - Layout: `plans/features/az<feature id>-<slug>/spec.md`, items under `items/az<id>-<slug>.md`.
8
+ - The id is the identity: allocate the item on the board first, then name the file. Until then it is a row in the plan's `## Items` table.
9
+ - Nothing moves between directories; the spec text is the item's fields.
10
+ - A gap found along the way goes in `plans/GAPS-AND-FINDINGS.md` and becomes a Bug under the same Feature.
11
+
12
+ ## Commits and PRs
13
+ - Each commit is independently green and on one topic. Never `git stash`. Green wins over one topic, and say so.
14
+ - Subject: `<type>(<scope>): <lowercase subject>`; types `feat|fix|docs|refactor|perf|chore`; no `!`.
15
+ - Branch: `<type>/<kebab>`, same types, not configurable.
16
+ - `release:` is reserved for the promotion PR the pipeline raises; never write one by hand.
17
+ - A PR body needs non-empty `## Summary`, `## Why` and `## Changes`; the title follows the subject grammar.
18
+
19
+ ## Release flow
20
+ - `<type>/<kebab>` branch → PR → `integration` (auto-completes when green) → pipeline opens `integration → main` → owner approves. No agent opens that PR.
21
+ - A repository that differs states it under "Release flow of this repository".
22
+
23
+ ## Tasks ledger
24
+ - At the start and the end of a task, append one line to `docs/notes.md` (create it if missing, with YAML frontmatter and a `## Lines` section).
25
+ - Format `YYYY-MM-DD HH:MM <surface> <note>`, whole line ≤100 characters. Append-only, newest last, never rewrite earlier lines. One ledger per repository.
26
+
27
+ ## Release flow of this repository
28
+ - <!-- Owner: state any difference from the flow above; delete this section if none. -->
@@ -0,0 +1,17 @@
1
+ # Invariants
2
+
3
+ Must-never-regress rules. One bullet each: `rule` — verified by `gate/test/command`. No check, no invariant. Add yours under "This repository".
4
+
5
+ ## Estate-wide
6
+ - The compliance gates enabled in `adl-config.toml` pass before every commit — verified by `adl repo controls` pre-commit and CI. A gate is switched off only by the owner, by removing it from `[controls.compliance] enabled`.
7
+ - A compliance exemption list only shrinks: remove a file once it passes; never add one — verified by the gate that reads the register.
8
+ - No multi-line comments in code; the explanation lives in `docs/` — verified by `repository.no-multiline-comments`.
9
+ - The standard trees `docs`, `plans`, `workflows` and `compliance` are those declared in `[controls.compliance.trees]`, matching the layout in `AGENTS.md` — verified by the layout and docs gates.
10
+
11
+ ## Protected paths
12
+ - Landed only by the owner; an agent leaves them uncommitted and asks — verified by the pre-commit integrity gate.
13
+ - `adl-config.toml`, `compliance/` (or the compliance tree declared in `[controls.compliance.trees]`) and `workflows/`, plus any pattern this repository adds under `[controls.integrity].watched`.
14
+ - No dot folder is protected as a whole: `.workflows/` and the agent surface folders hold the repository's own hooks and skills.
15
+
16
+ ## This repository
17
+ - <!-- Owner: `rule` — verified by `gate/test/command`. -->
@@ -1,4 +1,4 @@
1
- import { HOOK_NAMES, TOOL, VERSION } from './constants.ts'
1
+ import { HOOK_NAMES, SCAFFOLD_ROOT_FILES, TOOL, VERSION } from './constants.ts'
2
2
  import { doctor } from './doctor.ts'
3
3
  import { azdoPush } from './azdo-push.ts'
4
4
  import { ensurePromotionPullRequest } from './promote.ts'
@@ -18,7 +18,8 @@ export interface Io {
18
18
  }
19
19
 
20
20
  const USAGE = `usage:
21
- ${TOOL} install [--repo <path>] [--dry-run] [--json] [--no-grade] [--rewire]
21
+ ${TOOL} install [--repo <path>] [--dry-run] [--json] [--no-grade] [--rewire] [--force-templates[=<file,...>]]
22
+ (--force-templates: overwrite the root templates; bare = all of ${SCAFFOLD_ROOT_FILES.join(', ')}, or a comma list)
22
23
  ${TOOL} grade [--repo <path>] (prints the compliance verdict as JSON; exit 0 whatever the verdict)
23
24
  ${TOOL} uninstall [--repo <path>] [--dry-run]
24
25
  ${TOOL} doctor [--repo <path>] [--json]
@@ -40,6 +41,8 @@ interface Parsed {
40
41
  flags: Set<string>
41
42
  positional: string[]
42
43
  rest: string[]
44
+ /** The raw `--force-templates=<list>` value; undefined for the bare flag. */
45
+ forced?: string
43
46
  }
44
47
 
45
48
  function parse(command: string, argv: string[], allowed: string[]): Parsed {
@@ -50,7 +53,11 @@ function parse(command: string, argv: string[], allowed: string[]): Parsed {
50
53
  parsed.rest = argv.slice(index + 1)
51
54
  break
52
55
  }
53
- if (arg === '--repo') {
56
+ if (arg === '--force-templates' || arg.startsWith('--force-templates=')) {
57
+ if (!allowed.includes('--force-templates')) throw new UsageError(`unknown option for ${command}: ${arg}`)
58
+ parsed.flags.add('--force-templates')
59
+ if (arg.includes('=')) parsed.forced = arg.slice(arg.indexOf('=') + 1)
60
+ } else if (arg === '--repo') {
54
61
  parsed.repo = argv[++index] ?? ''
55
62
  } else if (arg.startsWith('--')) {
56
63
  if (!allowed.includes(arg)) throw new UsageError(`unknown option for ${command}: ${arg}`)
@@ -66,6 +73,15 @@ function parse(command: string, argv: string[], allowed: string[]): Parsed {
66
73
  return parsed
67
74
  }
68
75
 
76
+ /** The root templates named by `--force-templates[=<list>]`; the bare flag means all, an unknown or empty name is refused. */
77
+ function forcedTemplates(value: string | undefined): string[] {
78
+ if (value === undefined) return [...SCAFFOLD_ROOT_FILES]
79
+ const names = value.split(',')
80
+ const unknown = names.filter((name) => !(SCAFFOLD_ROOT_FILES as readonly string[]).includes(name))
81
+ if (unknown.length > 0) throw new UsageError(`unknown root template for --force-templates: ${unknown.map((n) => `'${n}'`).join(', ')} (valid: ${SCAFFOLD_ROOT_FILES.join(', ')})`)
82
+ return names
83
+ }
84
+
69
85
  function printActions(io: Io, actions: Action[], dryRun: boolean): void {
70
86
  const tag = dryRun ? '[dry-run] ' : ''
71
87
  for (const action of actions) io.out(`${tag}${action.kind} ${action.target} (${action.reason})`)
@@ -84,10 +100,12 @@ async function dispatch(argv: string[], io: Io): Promise<number> {
84
100
  io.out(USAGE)
85
101
  return 0
86
102
  case 'install': {
87
- const parsed = parse(command, args, ['--dry-run', '--json', '--no-grade', '--rewire'])
103
+ const parsed = parse(command, args, ['--dry-run', '--json', '--no-grade', '--rewire', '--config-created', '--force-templates'])
88
104
  const rewire = parsed.flags.has('--rewire')
105
+ const forceTemplates = parsed.flags.has('--force-templates') ? forcedTemplates(parsed.forced) : []
106
+ if (rewire && forceTemplates.length > 0) throw new UsageError('--force-templates cannot be combined with --rewire (a re-wire never writes root templates)')
89
107
  const grade = parsed.flags.has('--no-grade') || rewire ? {} : { grade: { env: io.env } }
90
- const result = install(parsed.repo, { dryRun: parsed.flags.has('--dry-run'), rewire, ...grade })
108
+ const result = install(parsed.repo, { dryRun: parsed.flags.has('--dry-run'), rewire, configCreated: parsed.flags.has('--config-created'), forceTemplates, ...grade })
91
109
  if (parsed.flags.has('--json')) {
92
110
  io.out(JSON.stringify(result))
93
111
  return 0
@@ -2,16 +2,6 @@ import { readFileSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
3
 
4
4
  export const TOOL = 'adl-repo-controls'
5
- /**
6
- * Never published (2026-09-27): kept only as this
7
- * package's own internal identity (USAGE text, error messages, the lock
8
- * file's `tool` field) — none of that is a claim that `adl-repo-controls`
9
- * is installable. NPM_INSTALL/PACKAGE_NAME describe a registry package that
10
- * does not exist; do not point a user-facing "how do I get this on PATH"
11
- * message at them (see ADL_NPM_INSTALL below for the one that is real).
12
- */
13
- const PACKAGE_NAME = '@amsterdamdatalabs/adl-repo-controls'
14
- const NPM_INSTALL = `npm i -g ${PACKAGE_NAME}`
15
5
  /**
16
6
  * How a repository's committed git-hook stubs actually get `adl` onto
17
7
  * PATH: this package is folded into the `adl` binary's `repo controls`
@@ -72,11 +62,8 @@ export const RECONCILE_GUARD_REL = '.workflows/reconcile-guard.sh'
72
62
  /** The folder skeleton `install` bootstraps for the adl standard trees; see README, "Scaffolding". */
73
63
  export const SCAFFOLD_ROOT = join(PACKAGE_ROOT, 'assets', 'scaffold')
74
64
  export const SCAFFOLD_TREES = ['docs', 'plans', 'workflows'] as const
75
- /** Repository-root entry files, scaffolded from `assets/scaffold/root/` when absent (never overwritten). */
76
- export const SCAFFOLD_ROOT_FILES = ['AGENTS.md', 'CLAUDE.md'] as const
77
- /** The invariants that apply to ALL repositories; install deploys it as `INVARIANTS.md` only when the target has none. */
78
- export const GLOBAL_INVARIANTS_SOURCE = join(PACKAGE_ROOT, 'assets', 'GLOBAL-INVARIANTS.md')
79
- export const GLOBAL_INVARIANTS_TARGET = 'INVARIANTS.md'
65
+ /** Repository-root templates in `assets/scaffold/root/`: written only by a run that creates adl-config.toml, overwritten only by `--force-templates`. */
66
+ export const SCAFFOLD_ROOT_FILES = ['AGENTS.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'INVARIANTS.md'] as const
80
67
 
81
68
  export const MANAGED_ASSETS = [
82
69
  { rel: PROMOTE_REL, source: join(PACKAGE_ROOT, 'assets', 'promote.ts'), mode: 0o755, doctorId: 'promote' },
@@ -24,8 +24,6 @@ import {
24
24
  ADL_NPM_INSTALL,
25
25
  CONFIG_REL,
26
26
  GITIGNORE_REL,
27
- GLOBAL_INVARIANTS_SOURCE,
28
- GLOBAL_INVARIANTS_TARGET,
29
27
  HOOK_NAMES,
30
28
  HOOKS_PATH,
31
29
  LOCK_REL,
@@ -234,6 +232,10 @@ export interface InstallOptions {
234
232
  grade?: { env: Env }
235
233
  /** Re-wire only: no config seeding, no scaffolding, no grading (fleet reconcile; see README, "Re-wiring"). */
236
234
  rewire?: boolean
235
+ /** This run created adl-config.toml, so the root templates are written when absent; see README, "Root templates". */
236
+ configCreated?: boolean
237
+ /** Root templates to overwrite (`--force-templates`); the only way an existing template is replaced. */
238
+ forceTemplates?: readonly string[]
237
239
  }
238
240
 
239
241
  export interface InstallResult {
@@ -266,6 +268,7 @@ function readCentralMarker(repo: string, dryRun: boolean): string {
266
268
  export function install(repoArg: string, options: InstallOptions): InstallResult {
267
269
  const repo = resolveRepo(repoArg)
268
270
  const dryRun = options.dryRun === true
271
+ const configMissing = !existsSync(join(repo, CONFIG_REL))
269
272
  const initialConfigText = readCentralMarker(repo, dryRun)
270
273
  // Validate only repo-controls' declared tables before deleting its root.
271
274
  // Other owners' tables remain deliberately opaque.
@@ -336,6 +339,7 @@ export function install(repoArg: string, options: InstallOptions): InstallResult
336
339
  created: complianceResult.created,
337
340
  sha256: sha256(extractTableSpan(finalConfigText, 'controls.compliance') as string),
338
341
  }
342
+ scaffoldRootTemplates(ops, options.configCreated === true || (dryRun && configMissing), options.forceTemplates ?? [])
339
343
  scaffoldStandardTrees(finalConfigText, ops)
340
344
  }
341
345
 
@@ -507,10 +511,6 @@ function assertComplianceTree(value: unknown): void {
507
511
  /** Bootstrap the standard trees at their declared paths, only where the folder is missing; see README, "Scaffolding". */
508
512
  function scaffoldStandardTrees(configText: string, ops: Ops): void {
509
513
  const missing = declaredScaffoldTrees(configText).filter(({ base }) => !ops.occupied(base))
510
- for (const rel of SCAFFOLD_ROOT_FILES) {
511
- ops.create(rel, readFileSync(join(SCAFFOLD_ROOT, 'root', rel)), 'scaffold the repository entry files (user-owned after install)')
512
- }
513
- ops.create(GLOBAL_INVARIANTS_TARGET, readFileSync(GLOBAL_INVARIANTS_SOURCE), 'deploy the global invariants (user-owned after install)')
514
514
  for (const { name, base } of missing) {
515
515
  for (const rel of scaffoldFiles(join(SCAFFOLD_ROOT, name), '')) {
516
516
  ops.create(`${base}/${rel}`, readFileSync(join(SCAFFOLD_ROOT, name, rel)), `scaffold the ${name} tree (user-owned after install)`)
@@ -518,6 +518,15 @@ function scaffoldStandardTrees(configText: string, ops: Ops): void {
518
518
  }
519
519
  }
520
520
 
521
+ /** Root templates: created when absent on a run that created adl-config.toml, overwritten only for the forced names; see README, "Root templates". */
522
+ function scaffoldRootTemplates(ops: Ops, created: boolean, forced: readonly string[]): void {
523
+ for (const rel of SCAFFOLD_ROOT_FILES) {
524
+ const bytes = readFileSync(join(SCAFFOLD_ROOT, 'root', rel))
525
+ if (forced.includes(rel)) ops.write(rel, bytes, 'overwrite the root template (--force-templates)')
526
+ else if (created) ops.create(rel, bytes, 'scaffold the repository root template (user-owned after install)')
527
+ }
528
+ }
529
+
521
530
  /** Every file under `dir`, relative to it, sorted, depth-first. */
522
531
  function scaffoldFiles(dir: string, prefix: string): string[] {
523
532
  return readdirSync(join(dir, prefix), { withFileTypes: true })