@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.
- package/bin/adl +2 -2
- package/bin/reconcile +4 -3
- package/package.json +1 -1
- package/packages/agent-hooks/README.md +14 -0
- package/packages/agent-hooks/assets/opencode-plugin.template.ts +140 -104
- package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -18
- package/packages/agent-hooks/src/deploy/render.ts +12 -3
- package/packages/agent-hooks/src/rules/impl/git-branch.ts +63 -0
- package/packages/agent-hooks/src/rules/impl/git-helpers.ts +91 -3
- package/packages/agent-hooks/src/rules/impl/git-history.ts +4 -13
- package/packages/agent-hooks/src/rules/impl/git-target.ts +423 -0
- package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +8 -7
- package/packages/agent-hooks/src/rules/ports.ts +36 -0
- package/packages/agent-hooks/src/rules/registry.ts +3 -2
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +1 -1
- package/packages/agent-settings/README.md +10 -4
- package/packages/agent-settings/assets/GLOBAL-AGENTS.md +5 -0
- package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +40 -0
- package/packages/agent-settings/src/global-agents.ts +43 -8
- package/packages/agent-skills/assets/adl-mechanics/SKILL.md +38 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +1 -1
- package/packages/agent-skills/assets/to-feature/SKILL.md +1 -1
- package/packages/agent-skills/assets/to-workitems/SKILL.md +1 -1
- package/packages/agent-skills/assets/to-workitems/references/AZURE-DEVOPS-HIERARCHY.md +3 -0
- package/packages/azdo/src/lib/agent-identity.ts +1 -1
- package/packages/repo-controls/README.md +12 -7
- package/packages/repo-controls/assets/scaffold/root/AGENTS.md +60 -11
- package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +28 -0
- package/packages/repo-controls/assets/scaffold/root/INVARIANTS.md +17 -0
- package/packages/repo-controls/src/cli.ts +23 -5
- package/packages/repo-controls/src/constants.ts +2 -15
- 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:
|
|
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
|
|
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
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
`
|
|
557
|
-
`
|
|
558
|
-
|
|
559
|
-
`
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
15
|
-
|
|
16
|
-
## Rules
|
|
15
|
+
| `.workflows/` | managed hook stubs and lock; rebuilt by `adl repo controls install`, never hand-edit |
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- `
|
|
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 === '--
|
|
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
|
|
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 })
|