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

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 (84) hide show
  1. package/bin/adl +2 -2
  2. package/package.json +1 -1
  3. package/packages/agent-hooks/deploy-profile.json +1 -1
  4. package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
  5. package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
  6. package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
  7. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +12 -6
  8. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
  9. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
  10. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +15 -14
  11. package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
  12. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  13. package/packages/agent-hooks/src/rules/registry.ts +11 -2
  14. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +8 -4
  15. package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
  16. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
  17. package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
  18. package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
  19. package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
  20. package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
  21. package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
  22. package/packages/agent-skills/assets/implement-feature/SKILL.md +153 -0
  23. package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
  24. package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
  25. package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
  26. package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
  27. package/packages/agent-skills/assets/prepare-promotion/SKILL.md +91 -0
  28. package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
  29. package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
  30. package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
  31. package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
  32. package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
  33. package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
  34. package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
  35. package/packages/agent-skills/assets/validate-design/SKILL.md +70 -0
  36. package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
  37. package/packages/agent-skills/assets/workitems-process/SKILL.md +147 -0
  38. package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +194 -0
  39. package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
  40. package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
  41. package/packages/agent-skills/assets/write-feature/SKILL.md +95 -0
  42. package/packages/agent-skills/dist/deploy.d.ts +7 -0
  43. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  44. package/packages/agent-skills/dist/deploy.js +52 -5
  45. package/packages/agent-skills/dist/deploy.js.map +1 -1
  46. package/packages/agent-skills/src/deploy.ts +48 -4
  47. package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
  48. package/packages/azdo/src/activity-log.ts +118 -0
  49. package/packages/azdo/src/azdo-attachments.ts +108 -0
  50. package/packages/azdo/src/azdo-pl.ts +100 -3
  51. package/packages/azdo/src/azdo-pr.ts +194 -11
  52. package/packages/azdo/src/azdo-wit.ts +329 -18
  53. package/packages/azdo/src/cli.ts +42 -2
  54. package/packages/azdo/src/lib/client.ts +7 -1
  55. package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
  56. package/packages/azdo/src/mcp-server.ts +53 -5
  57. package/packages/azdo/src/tool-dispatch.ts +53 -4
  58. package/packages/azdo/src/workflow-ladder.csv +125 -107
  59. package/packages/azdo/src/workflow-rules.ts +254 -97
  60. package/packages/repo-controls/README.md +12 -4
  61. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
  62. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
  63. package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +5 -5
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
  67. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
  68. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
  69. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
  70. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/fixtures/open-work-cap-clean/plans/features/az601-clean/intent.md +17 -0
  71. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
  72. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
  73. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
  74. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/design.md +35 -0
  75. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/file-map-reference.md +19 -0
  76. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/intent.md +25 -0
  77. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/items/az351-restore-entrypoint.md +25 -0
  78. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/requirements.md +44 -0
  79. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-violation/plans/features/az347-test-coverage/requirements.md +44 -0
  80. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
  81. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
  82. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
  83. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
  84. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
package/bin/adl CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/bin/adl
3
- size: 82949600 bytes
4
- sha256: 42e867b263b1cb13e200275f93036ccdaca15e3902ed11b60efbdea3730d96a3
3
+ size: 82998752 bytes
4
+ sha256: a9c4655905b5f4b937f9d93dadeb7ad5ae28dec1392a767225242478684aff98
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amsterdamdatalabs/adl-agent-control-linux-x64",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "adl CLI binary for linux/x64 (agent identity + Azure DevOps front door — see docs/architecture/cli-design.md)",
5
5
  "license": "UNLICENSED",
6
6
  "os": [
@@ -40,7 +40,7 @@
40
40
  "file": ".opencode/plugins/adl-repo.ts",
41
41
  "hostSuffix": "opencode",
42
42
  "matchers": {
43
- "Bash": "bash",
43
+ "Bash": "bash|shell",
44
44
  "Write|Edit|MultiEdit|StrReplace|str_replace_editor|edit_file|str_replace|replace_in_file|create_file|write_file": "write|edit"
45
45
  },
46
46
  "unmappedMatcher": "keep",
@@ -284,7 +284,7 @@ const IDENTITY_RULE_IDS: ReadonlySet<RuleId> = new Set([
284
284
  * The four role-gate rules that deny a SPECIFIC Azure DevOps MCP tool call by
285
285
  * the caller's identity (`adl.workitem-requires-tarun-or-vivek`,
286
286
  * `adl.pr-create-update-role`, `adl.pr-review-role`,
287
- * `adl.build-run-denied`). None of these are in `IDENTITY_RULE_IDS` above —
287
+ * `adl.build-run-role`). None of these are in `IDENTITY_RULE_IDS` above —
288
288
  * that set covers the rules that fire when no identity is resolved or
289
289
  * carried, never a rule that denies WITH an identity already resolved, on the
290
290
  * strength of who that identity is. Each of them can only fire on the same
@@ -297,7 +297,7 @@ const ROLE_GATE_RULE_IDS: ReadonlySet<RuleId> = new Set([
297
297
  'adl.workitem-requires-tarun-or-vivek',
298
298
  'adl.pr-create-update-role',
299
299
  'adl.pr-review-role',
300
- 'adl.build-run-denied',
300
+ 'adl.build-run-role',
301
301
  ])
302
302
 
303
303
  type IdentitySourceLabel = 'inline' | 'env' | 'default-file' | 'none'
@@ -0,0 +1,116 @@
1
+ /** `adl.build-run-role` — docs/operations/agent-permissions.md row 12 (build run), decision H1; the allow condition and the caller-declared id-is-its-own-PR-validation-build approximation (hook has no network; azdo core must re-verify) are documented on match() below and in the summary. */
2
+
3
+ import type { Invocation } from '../shell/invocation.ts'
4
+ import type { Finding, MessageContext, RuleContext, RuleSpec } from '../spec.ts'
5
+ import { noParams } from '../spec.ts'
6
+ import { BRANCH_PREFIXES } from '../../../../azdo/src/lib/commit-types.ts'
7
+ import { BUILDER_AGENTS, resolveFromEnvOrFile } from './adl-identity.ts'
8
+ import { detailString } from './messages.ts'
9
+ import { mcpToolName } from './mcp-tool.ts'
10
+ import { effectiveIdentityForMcpCli, toolCallFor } from './adl-mcp-cli.ts'
11
+
12
+ const RULE_ID = 'adl.build-run-role'
13
+
14
+ const PROTECTED_BRANCHES: ReadonlySet<string> = new Set(['integration', 'main'])
15
+ const FEATURE_PREFIXES: readonly string[] = BRANCH_PREFIXES.map((prefix) => `${prefix}/`)
16
+
17
+ type Violation = 'not-a-builder' | 'missing-id' | 'missing-branch' | 'non-feature-branch'
18
+
19
+ function normalizeBranchRef(ref: string): string {
20
+ return ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref
21
+ }
22
+
23
+ function isFeatureBranchName(branch: string): boolean {
24
+ return FEATURE_PREFIXES.some((prefix) => branch.startsWith(prefix))
25
+ }
26
+
27
+ export const adlBuildRunRole: RuleSpec = {
28
+ id: RULE_ID,
29
+ category: 'tool-call',
30
+ severity: 'deny',
31
+ enabledByDefault: true,
32
+ summary:
33
+ 'Deny build_run except the lead (agent-tarun/agent-vivek) re-queuing its own PR’s validation pipeline on that PR’s feature branch (docs/operations/agent-permissions.md row 12, H1) — the MCP tool AND its adl mcp cli equivalent.',
34
+ params: noParams,
35
+ match(inv: Invocation, ctx: RuleContext<Record<string, never>>): Finding | null {
36
+ // Also catch `adl mcp cli build-run` — same tool, same grade.
37
+ const call = toolCallFor(inv)
38
+ if (call === null) return null
39
+ if (mcpToolName(call.tool) !== 'build_run') return null
40
+
41
+ // Grade the EFFECTIVE identity for THIS call — an inline ADL_AGENT_ID_AGENT= override on the same CLI invocation wins, like the other role rules.
42
+ const resolution = resolveFromEnvOrFile(ctx.env)
43
+ const fallback = resolution.ok ? resolution.agent : null
44
+ const agent = effectiveIdentityForMcpCli(inv, ctx.command ?? '', fallback)
45
+ if (agent === null) return null // no identity at all: adl.run-required-for-writes's job
46
+
47
+ // 1. Only the lead re-queues CI — the role mismatch reports first regardless of shape.
48
+ if (!BUILDER_AGENTS.has(agent)) {
49
+ return { ruleId: RULE_ID, severity: 'deny', detail: { tool: call.tool, violation: 'not-a-builder' satisfies Violation } }
50
+ }
51
+
52
+ // 2. The definition id, validated exactly like azdo-pl.ts's buildRun (a positive integer).
53
+ const id = call.input.id
54
+ if (typeof id !== 'number' || !Number.isInteger(id) || id < 1) {
55
+ return { ruleId: RULE_ID, severity: 'deny', detail: { tool: call.tool, violation: 'missing-id' satisfies Violation } }
56
+ }
57
+
58
+ // 3. The PR source branch, caller-declared exactly like pr_review's targetBranch: required, and provably a feature branch.
59
+ const branch = call.input.branch
60
+ if (typeof branch !== 'string' || branch.trim().length === 0) {
61
+ return { ruleId: RULE_ID, severity: 'deny', detail: { tool: call.tool, violation: 'missing-branch' satisfies Violation } }
62
+ }
63
+ const normalized = normalizeBranchRef(branch.trim())
64
+ if (PROTECTED_BRANCHES.has(normalized) || !isFeatureBranchName(normalized)) {
65
+ return { ruleId: RULE_ID, severity: 'deny', detail: { tool: call.tool, violation: 'non-feature-branch' satisfies Violation } }
66
+ }
67
+
68
+ return null
69
+ },
70
+ message(finding: Finding, ctx: MessageContext) {
71
+ const tool = detailString(finding, 'tool')
72
+ const violation = detailString(finding, 'violation') as Violation
73
+
74
+ if (violation === 'not-a-builder') {
75
+ const user = 'this operation can only be done with agent-tarun or agent-vivek'
76
+ return {
77
+ user,
78
+ agent:
79
+ `DENIED ${tool}: ${user}. Only the lead may re-queue its own PR’s validation pipeline on that PR’s ` +
80
+ `source branch — agent-reviewer runs no implementation PRs, so it has no PR CI to re-queue. Blocked: ${ctx.snippet}`,
81
+ }
82
+ }
83
+
84
+ if (violation === 'missing-id') {
85
+ const user = 'a build definition id is required'
86
+ return {
87
+ user,
88
+ agent: `DENIED ${tool}: ${user} — pass the definition id of your PR’s validation pipeline (a positive integer). Blocked: ${ctx.snippet}`,
89
+ }
90
+ }
91
+
92
+ if (violation === 'missing-branch') {
93
+ const user = 'name the source branch of your own PR to re-queue its validation pipeline'
94
+ return {
95
+ user,
96
+ agent:
97
+ `DENIED ${tool}: ${user}. The branch argument is what lets this hook scope the re-queue to your own ` +
98
+ `PR’s CI — without it, pipelines run on their own triggers. Re-queue with \`branch\` naming your PR’s ` +
99
+ `feature branch. Blocked: ${ctx.snippet}`,
100
+ }
101
+ }
102
+
103
+ const user = 'only a feature branch that is your own PR’s source branch may be re-queued'
104
+ return {
105
+ user,
106
+ agent:
107
+ `DENIED ${tool}: ${user}. \`integration\`/\`main\` and every unrecognized branch name are denied — a PR’s ` +
108
+ `source branch always carries a \`${FEATURE_PREFIXES.join(' ')}\` prefix. Blocked: ${ctx.snippet}`,
109
+ }
110
+ },
111
+ corpus: {
112
+ // Tool-call rule; real coverage is tests/unit/rules-adl-build-run-role.test.ts (the shared shell corpus only proves non-firing on unrelated commands).
113
+ deny: [],
114
+ allow: ['ls -la', 'git status'],
115
+ },
116
+ }
@@ -0,0 +1,159 @@
1
+ /** `adl.git-user-override-block` — blanket deny (2026-10-12) of every `user.name` / `user.email` / `user.useConfigOnly` override on a git command: inline `-c user.*=…` / `--config-env user.*=…` (separated, bundled, or bare next-token), the `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>` env-var equivalent, AND any `git config` invocation at all (read or write — the whole subcommand is closed; an agent that needs the current identity runs `git var GIT_AUTHOR_IDENT` or reads `.git/config` directly). The only allowed identity path is `ADL_AGENT_ID_AGENT=agent-tarun` on the command line so the front controller (`bin/git-ado`) stamps the right committer. Placed 3rd in `GIT_RULES`, right after the two `core.*` override rules, so a `user.*` ride-along on any subcommand reports as the user override, not the subcommand rule. The pre-commit hook (the `prepare-commit-msg` / `commit-msg` pair `bin/git-ado` calls) used to catch the *result* on `git commit`, but not on `git commit-tree` / `git fast-import` / etc. — this rule closes the gap at the command level, before git ever sees the override. */
2
+
3
+ import { isHelpOnlyInvocation } from './git-helpers.ts'
4
+ import type { Finding, MessageContext, RuleContext, RuleSpec } from '../spec.ts'
5
+ import { noParams } from '../spec.ts'
6
+ import { gitAgentTail } from './messages.ts'
7
+ import type { Invocation } from '../shell/invocation.ts'
8
+ import { tokenValue } from '../shell/token.ts'
9
+
10
+ /** `user.name`, `user.email`, `user.useConfigOnly` — case-insensitive. */
11
+ const USER_KEY = /^user\.(name|email|useconfigonly)\b/i
12
+
13
+ /** `GIT_CONFIG_COUNT=<n>` where `<n>` is a non-zero integer — captured so the env-var form's scope gate (the COUNT must be non-zero for git to read any of the KEY/VALUE pairs) is enforced, not assumed. */
14
+ const GIT_CONFIG_COUNT_RE = /\bGIT_CONFIG_COUNT\s*=\s*([1-9]\d*)\b/i
15
+ /** `GIT_CONFIG_KEY_<n>=<value>` — `<value>` is checked against `USER_KEY` separately. */
16
+ const GIT_CONFIG_KEY_RE = /\bGIT_CONFIG_KEY_\d+\s*=\s*([^\s'"`;&|<>(){}]+)/gi
17
+
18
+ function gitInvocation(inv: Invocation): Invocation & { kind: 'git' } | null {
19
+ return inv.kind === 'git' ? inv : null
20
+ }
21
+
22
+ /** True when `tokens` (the post-`-c` value tokens) contains a `user.*` key; strips a single leading `-c` so the bundled `-cuser.name=Foo` form (whole-token consumed by `scanGitGlobals`) matches the same regex as the separated form. */
23
+ function tokensOverrideUser(tokens: readonly { readonly value: string }[]): boolean {
24
+ for (const token of tokens) {
25
+ const raw = tokenValue(token as Parameters<typeof tokenValue>[0])
26
+ // Bundled `-cuser.name=Foo`: drop the leading `-c` so the regex below matches.
27
+ const value = raw.startsWith('-c') && raw.length > 2 ? raw.slice(2) : raw
28
+ if (USER_KEY.test(value)) return true
29
+ }
30
+ return false
31
+ }
32
+
33
+ /** True when `command` carries the `GIT_CONFIG_*` env-var form of a `user.*` override: a non-zero `GIT_CONFIG_COUNT=<n>` AND at least one `GIT_CONFIG_KEY_<n>=user.<…>` (both must be present; a lone KEY without a non-zero COUNT is a no-op and not a bypass). */
34
+ function envVarOverrideUser(command: string): boolean {
35
+ if (!GIT_CONFIG_COUNT_RE.test(command)) return false
36
+ // Reset the shared `g`-flag regex's lastIndex between calls (stateful).
37
+ GIT_CONFIG_KEY_RE.lastIndex = 0
38
+ for (;;) {
39
+ const match = GIT_CONFIG_KEY_RE.exec(command)
40
+ if (match === null) return false
41
+ if (USER_KEY.test(match[1] as string)) return true
42
+ }
43
+ }
44
+
45
+ export const adlGitUserOverrideBlock: RuleSpec = {
46
+ id: 'adl.git-user-override-block',
47
+ category: 'git',
48
+ severity: 'deny',
49
+ enabledByDefault: true,
50
+ summary: 'Blanket deny (2026-10-12) of every `user.*` override on a git command: inline `-c` / `--config-env`, the `GIT_CONFIG_*` env-var equivalent, AND any `git config` invocation at all (read or write — the whole subcommand is closed; `git var GIT_AUTHOR_IDENT` is the only way an agent reads the current identity). The only allowed identity path is `ADL_AGENT_ID_AGENT=agent-tarun` on the command line.',
51
+ params: noParams,
52
+ match(inv, ctx: RuleContext<Record<string, never>>): Finding | null {
53
+ const git = gitInvocation(inv)
54
+ if (!git) return null
55
+ if (isHelpOnlyInvocation(git.rest)) return null
56
+ // Blanket inline-override surface: `-c user.*=…` / `--config-env user.*=…` (and the bare-token / bundled forms) land in `git.globals`.
57
+ if (tokensOverrideUser(git.globals)) {
58
+ return { ruleId: 'adl.git-user-override-block', severity: 'deny', detail: {} }
59
+ }
60
+ // Env-var form: `GIT_CONFIG_*` assignments are consumed by `walk()` before the `git` Invocation is built; scan the raw `ctx.command`.
61
+ if (ctx.command !== undefined && envVarOverrideUser(ctx.command)) {
62
+ return { ruleId: 'adl.git-user-override-block', severity: 'deny', detail: {} }
63
+ }
64
+ // Per the 2026-10-12 widening: `git config` is BANNED for agents — every invocation, read or write. An agent that needs to know the current `user.name` runs `git var GIT_AUTHOR_IDENT` or reads `.git/config` directly; `git config` of any kind is the surface that touches persisted config and is closed.
65
+ if (git.subcommand === 'config') {
66
+ return { ruleId: 'adl.git-user-override-block', severity: 'deny', detail: {} }
67
+ }
68
+ return null
69
+ },
70
+ message(_finding: Finding, ctx: MessageContext) {
71
+ return {
72
+ user:
73
+ 'Blocked a user.name / user.email / user.useConfigOnly override on a git command. ' +
74
+ 'Inline `-c user.*=…`, `--config-env user.*=`, `GIT_CONFIG_KEY_<n>=user.*` env-var overrides, AND any `git config` invocation are all banned in an agent session.',
75
+ agent:
76
+ 'DENIED user-identity override on a git command. `git -c user.name=…` / `-c user.email=…` / ' +
77
+ '`--config-env user.*=…` (in every form — separated, bundled as `-cuser.*=…`, or bare ' +
78
+ '`-c user.* …`); the `GIT_CONFIG_COUNT=<n> GIT_CONFIG_KEY_<n>=user.* GIT_CONFIG_VALUE_<n>=…` ' +
79
+ 'env-var form (per `git-config(1)`); AND `git config …` of any kind (read or write — the whole ' +
80
+ 'subcommand is closed in an agent session) are blanket-denied in an agent session (2026-10-12). ' +
81
+ 'The only allowed identity path is `ADL_AGENT_ID_AGENT=agent-tarun` on the command line: ' +
82
+ '`ADL_AGENT_ID_AGENT=agent-tarun git commit -m …` so the front controller (`bin/git-ado`) ' +
83
+ 'stamps the right committer. To read the current identity, run `git var GIT_AUTHOR_IDENT` or ' +
84
+ 'read `.git/config` directly — `git config` is NEVER the right answer in an agent session, ' +
85
+ `even as a one-shot. ${gitAgentTail(ctx.snippet)}`,
86
+ }
87
+ },
88
+ corpus: {
89
+ deny: [
90
+ // `-c user.*=` (separate token)
91
+ 'git -c user.name=Foo commit -m x',
92
+ 'git -c user.email=foo@bar commit -m x',
93
+ 'git -c user.useConfigOnly=true commit -m x',
94
+ // `-c` bundled with `=` (no space) — the same form `scanGitGlobals` folds
95
+ 'git -cuser.name=Foo commit -m x',
96
+ 'git -cuser.email=foo@bar commit -m x',
97
+ // bare `-c` + separate-token value (next-token form)
98
+ 'git -c user.name Foo commit -m x',
99
+ 'git -c user.email foo@bar commit -m x',
100
+ // Case-insensitive
101
+ 'git -c User.Name=Foo commit -m x',
102
+ 'git -c USER.EMAIL=foo@bar commit -m x',
103
+ // `--config-env` (the env-var form, not just `-c`)
104
+ 'git --config-env user.name=Foo commit -m x',
105
+ // `user.*` ride-along with a non-commit subcommand — still blocked (blanket)
106
+ 'git -c user.name=Foo push',
107
+ 'git -c user.name=Foo push origin main',
108
+ 'git -c user.email=foo@bar fetch',
109
+ 'git -c user.name=Foo log --oneline',
110
+ 'git -c user.email=foo@bar status',
111
+ // The GIT_CONFIG_* env-var form — git's own alternative to `-c`; COUNT must be non-zero AND a KEY_<n>=user.* must be present for the bypass to take effect (the rule denies on that pair).
112
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo git commit -m x',
113
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.email GIT_CONFIG_VALUE_0=foo@bar git commit -m x',
114
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.useConfigOnly GIT_CONFIG_VALUE_0=true git commit -m x',
115
+ // Same, riding along with a non-commit subcommand
116
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo git push',
117
+ 'GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo GIT_CONFIG_KEY_1=user.email GIT_CONFIG_VALUE_1=foo@bar git commit -m x',
118
+ // Persistent form: `git config user.* …` (including `--global` / `--local` / `--unset` / `--remove-section user`).
119
+ 'git config user.name "agent-tarun"',
120
+ 'git config --global user.name "agent-tarun"',
121
+ 'git config --local user.email "agent-tarun@<org-domain>"',
122
+ 'git config user.useConfigOnly true',
123
+ 'git config --unset user.name',
124
+ 'git config --remove-section user',
125
+ 'git config --remove-section-all user',
126
+ // Per the 2026-10-12 widening: every `git config` invocation is denied (read or write — the whole subcommand is closed; an agent that needs the current identity runs `git var GIT_AUTHOR_IDENT` or reads `.git/config` directly).
127
+ 'git config --list',
128
+ 'git config --list --local',
129
+ 'git config --get user.name',
130
+ 'git config --get-all user.email',
131
+ 'git config --get-regexp "^user\\."',
132
+ 'git config http.proxy http://proxy:8080',
133
+ 'git config --global core.editor vim',
134
+ 'git config --local init.defaultBranch main',
135
+ ],
136
+ allow: [
137
+ // ordinary git traffic, no user.* anywhere
138
+ 'git status',
139
+ 'git log --oneline',
140
+ 'git commit -m "ordinary commit"',
141
+ // help invocations are exempted (the only thing in `rest` is a flag)
142
+ 'git -c user.name=Foo --help',
143
+ // other `-c` keys ride along freely (inline-override surface is the rule, not all `-c` flags)
144
+ 'git -c http.proxy=http://proxy:8080 clone https://github.com/foo/bar',
145
+ 'git -c core.autocrlf=false commit -m x',
146
+ 'git -c init.defaultBranch=main init',
147
+ // The legitimate identity path: prefix every commit with `ADL_AGENT_ID_AGENT=agent-tarun` so the front controller stamps the right committer. This is what the rule tells the agent to do.
148
+ 'ADL_AGENT_ID_AGENT=agent-tarun git commit -m "an agent commit"',
149
+ // The legitimate identity read: `git var GIT_AUTHOR_IDENT` (NOT `git config --get user.name`).
150
+ 'git var GIT_AUTHOR_IDENT',
151
+ // `GIT_CONFIG_*` with no `user.*` key — not a user-override. Agents may legitimately override `http.proxy` or `init.defaultBranch` via the env-var form.
152
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.proxy GIT_CONFIG_VALUE_0=http://proxy:8080 git clone https://github.com/foo/bar',
153
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=init.defaultBranch GIT_CONFIG_VALUE_0=main git init',
154
+ // `GIT_CONFIG_KEY_<n>=user.*` with no COUNT or COUNT=0 — git ignores the key without a non-zero count, so this is a no-op and not a bypass.
155
+ 'GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo git commit -m x',
156
+ 'GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo git commit -m x',
157
+ ],
158
+ },
159
+ }
@@ -56,12 +56,12 @@ interface GatedCommand {
56
56
  /**
57
57
  * Only the flags a role rule actually inspects: `pr-create-or-update`'s
58
58
  * `--action`/`--auto-complete`/`--no-auto-complete`/`--merge-strategy`/
59
- * `--delete-source-branch` (`isPrCompletionCall`, pr-completion.ts) and
59
+ * `--delete-source-branch` (`isPrCompletionCall`, pr-completion.ts),
60
60
  * `pr-review`'s `--comment`/`--vote`/`--target-branch`
61
- * (`adl-pr-review-role.ts`). `workitem-create-or-update` and `build-run` are
62
- * graded on identity alone — no input field changes their outcome — so
63
- * their flag lists are empty; the command still needs an entry here so it is
64
- * RECOGNIZED at all.
61
+ * (`adl-pr-review-role.ts`), and `build-run`'s `--id`/`--branch`
62
+ * (`adl.build-run-role`). `workitem-create-or-update` is graded on identity
63
+ * alone — no input field changes its outcome — so its flag list is empty;
64
+ * every command still needs an entry here so it is RECOGNIZED at all.
65
65
  */
66
66
  const GATED_COMMANDS: Readonly<Record<string, GatedCommand>> = {
67
67
  'workitem-create-or-update': { tool: 'workitem_create_or_update', flags: [] },
@@ -83,7 +83,13 @@ const GATED_COMMANDS: Readonly<Record<string, GatedCommand>> = {
83
83
  { flag: '--target-branch', key: 'targetBranch', type: 'string' },
84
84
  ],
85
85
  },
86
- 'build-run': { tool: 'build_run', flags: [] },
86
+ 'build-run': {
87
+ tool: 'build_run',
88
+ flags: [
89
+ { flag: '--id', key: 'id', type: 'integer' },
90
+ { flag: '--branch', key: 'branch', type: 'string' },
91
+ ],
92
+ },
87
93
  }
88
94
 
89
95
  /**
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * `adl.pr-complete-pending-decision` — docs/operations/agent-permissions.md row 9
3
- * ("PR complete / set auto-complete"), marked **OPEN** in the matrix: denied
4
- * for BOTH `agent-tarun`/`agent-vivek` AND `agent-reviewer` "until Tarun
5
- * decides", as its own separately named rule specifically so it can be
6
- * relaxed (or given a real role split) without touching
3
+ * ("PR complete / set auto-complete"), DECIDED: denied for every identity —
4
+ * `agent-tarun`/`agent-vivek` AND `agent-reviewer` alike — as its own
5
+ * separately named rule specifically so the denial (or a future role split)
6
+ * can be relaxed without touching
7
7
  * `adl.pr-create-update-role` (row 5), which keeps allowing a plain PR
8
8
  * create/update for builders and `agent-reviewer` throughout
9
9
  * (docs/specs/two-branch-standard-2026-09-27.md §1b.5: auto-complete is armed
@@ -16,9 +16,9 @@
16
16
  * owner decision unrelated to this row, and denying that would block PR
17
17
  * creation entirely).
18
18
  *
19
- * Deliberately does NOT require an identity to be present: row 9 is OPEN
20
- * regardless of who is asking, including a caller with no session identity at
21
- * all (whichever rule reports first, the outcome is the same: denied).
19
+ * Deliberately does NOT require an identity to be present: row 9 denies
20
+ * EVERY identity, including a caller with no session identity at all
21
+ * (whichever rule reports first, the outcome is the same: denied).
22
22
  */
23
23
 
24
24
  import type { Invocation } from '../shell/invocation.ts'
@@ -36,7 +36,7 @@ export const adlPrCompletePendingDecision: RuleSpec = {
36
36
  severity: 'deny',
37
37
  enabledByDefault: true,
38
38
  summary:
39
- 'Deny PR completion / auto-complete for every identity — open decision (docs/operations/agent-permissions.md row 9) — the MCP tool AND its adl mcp cli equivalent.',
39
+ 'Deny PR completion / auto-complete for every identity (docs/operations/agent-permissions.md row 9) — the MCP tool AND its adl mcp cli equivalent.',
40
40
  params: noParams,
41
41
  match(inv: Invocation): Finding | null {
42
42
  // `adl mcp cli
@@ -54,7 +54,7 @@ export const adlPrCompletePendingDecision: RuleSpec = {
54
54
  return {
55
55
  user,
56
56
  agent:
57
- `DENIED ${user} (docs/operations/agent-permissions.md row 9, marked OPEN). Create or update the PR's title/description ` +
57
+ `DENIED ${user} (docs/operations/agent-permissions.md row 9: denied for every identity). Create or update the PR's title/description ` +
58
58
  `normally; do not pass autoComplete, mergeStrategy, or deleteSourceBranch. Blocked: ${ctx.snippet}`,
59
59
  }
60
60
  },
@@ -34,11 +34,12 @@
34
34
  * regardless of who is asking, and the two rules never fight over the same
35
35
  * call.
36
36
  *
37
- * Rows 6 and 7 (PR comment; PR vote) are the same allow-set shape, but
38
- * `packages/azdo` has no comment/vote tool today — grepped 2026-09-27,
39
- * nothing named `pr_review`/`pr_comment`/`pr_vote` exists. PENDING: add a
40
- * sibling check here (or a new rule) when Stream B ships one, rather than
41
- * matching a guessed tool name now.
37
+ * Rows 6-8 (PR comment; PR vote on `main`; PR vote on `integration`) ship
38
+ * their own gate now: the sibling `adl-pr-review-role.ts` rules
39
+ * `pr_review` (shipped in `packages/azdo/src/azdo-pr-review.ts`, C3
40
+ * 2026-09-27) — comment allowed for the same allow-set as row 5, vote
41
+ * allowed for `agent-reviewer` only and only on `main`. This rule does not
42
+ * match `pr_review` at all; the two rules never fight over the same call.
42
43
  *
43
44
  * Requires an identity to already be present (`ADL_AGENT_ID_AGENT`
44
45
  * non-blank) — with no identity at all, `adl.run-required-for-writes`
@@ -2,21 +2,22 @@
2
2
  * Editing the files that wire the host's hooks.
3
3
  *
4
4
  * Handoff 5, rule 5: deny Write/Edit/MultiEdit/NotebookEdit on
5
- * `.claude/settings.json`, `.claude/settings.local.json`, or
6
- * `.codex/config.toml`. These are the files that install the hook into the
7
- * host (including the `.local.json` overrides). Editing any of them is a way
8
- * to switch the gate off without anyone noticing.
5
+ * `.claude/settings.json`, `.claude/settings.local.json`,
6
+ * `.codex/config.toml`, or `adl-config.toml`. These are the files that
7
+ * install the hook into the host (including the `.local.json` overrides)
8
+ * or that the running hook reads on every call — editing any of them is a
9
+ * way to switch the gate off without anyone noticing.
9
10
  *
10
- * `adl-config.toml` was the fourth file until 2026-10-01 (Tarun: "nobody
11
- * should stop editing adl-config.toml"). It is no longer on the list, so an
12
- * agent can edit, move or delete it. Two things follow, both accepted by the
13
- * owner: (1) every hook run re-reads it, so `[hooks] enabled = false` (a
14
- * silent no-op, `main.ts`'s `runHook`) or a `[rules."<id>"] enabled = false`
15
- * takes effect on the agent's next tool call, ahead of any commit — the
16
- * commit-time layers (`git.protected-tree-commit`, repo-controls' integrity
17
- * gate) are the only review left; (2) the carriers that stayed on the list
18
- * below are only as strong as that file, since it can also switch THIS rule
19
- * off.
11
+ * `adl-config.toml` left the list on 2026-10-01 (Tarun: "nobody should
12
+ * stop editing adl-config.toml") and rejoined it on 2026-10-12, after a
13
+ * review-of-the-door confirmed the gap: every hook run re-reads it, so
14
+ * `[hooks] enabled = false` (a silent no-op, `main.ts`'s `runHook`) or a
15
+ * `[rules."<id>"] enabled = false` takes effect on the agent's next tool
16
+ * call, ahead of any commit. The commit-time layers
17
+ * (`git.protected-tree-commit`, repo-controls' integrity gate) fire on the
18
+ * COMMITTED file, not on the working-tree copy that the running hook
19
+ * reads. The carriers on the list below are only as strong as that file,
20
+ * since it can also switch THIS rule off.
20
21
  *
21
22
  * Hooks are local-repo-first (Tarun, 2026-09-29): there is no managed-hooks
22
23
  * lock and no global carrier, so these per-repo carrier files are the ONLY
@@ -49,10 +49,9 @@ export const gitHooksPath: RuleSpec = {
49
49
  'git -c core.hooksPath=/dev/null commit -m x',
50
50
  'git -c core.hooksPath= push',
51
51
  'git -c core.hooksPath=/tmp/empty commit --amend',
52
- 'git config core.hooksPath /dev/null',
53
- 'git config --global core.hooksPath /tmp/empty',
54
52
  ],
55
- allow: ['git stash --help', 'git config user.name t'],
53
+ // The `git config core.hooksPath …` positive controls were here for years — gone as of Bug 695 (2026-10-12): `adl.git-user-override-block` denies every `git config` invocation in the engine, so a `git config …` string is denied upstream and never exercises the `subcommand === 'config' && overridesHooksPath(git.rest)` branch (the `-c` deny entries above still cover the override surface).
54
+ allow: ['git stash --help'],
56
55
  },
57
56
  }
58
57
 
@@ -91,11 +90,8 @@ export const gitCoreBare: RuleSpec = {
91
90
  deny: [
92
91
  'git -c core.bare=true status',
93
92
  'git -c core.bare=false commit -m x',
94
- 'git config core.bare true',
95
- 'git config --bool core.bare true',
96
- 'git config --global core.bare true',
97
- 'git config --unset core.bare',
98
93
  ],
99
- allow: ['git status', 'git config user.name t'],
94
+ // The `git config core.bare …` positive controls were here for years — gone as of Bug 695 (2026-10-12): `adl.git-user-override-block` denies every `git config` invocation in the engine, so a `git config …` string is denied upstream and never exercises the `subcommand === 'config' && flipsCoreBare(git.rest)` branch (the `-c` deny entries above still cover the override surface).
95
+ allow: ['git status'],
100
96
  },
101
97
  }
@@ -50,7 +50,7 @@ export function toolNameEndingWithKnownSuffix(tool: string, knownSuffixes: Reado
50
50
  *
51
51
  * Shared by every rule that gates an Azure DevOps MCP tool by name
52
52
  * (adl-run-required.ts, adl-pr-role.ts, adl-pr-complete-pending.ts,
53
- * adl-workitem-role.ts, adl-build-run-denied.ts) so the parsing cannot drift
53
+ * adl-workitem-role.ts, adl-build-run-role.ts) so the parsing cannot drift
54
54
  * between them.
55
55
  *
56
56
  * Prefix-less fallback (added 2026-09-28 with the one-agent-identity
@@ -25,6 +25,7 @@ import type { AnyRuleSpec, RuleId } from './spec.ts'
25
25
  import { asAnyRuleSpec, corpusIsEmpty } from './spec.ts'
26
26
 
27
27
  import { gitHooksPath, gitCoreBare } from './impl/git-core.ts'
28
+ import { adlGitUserOverrideBlock } from './impl/adl-git-user-override-block.ts'
28
29
  import { gitStash, gitClean, gitRebase, gitFilterBranch, gitRestore, gitReset, gitUpdateIndex } from './impl/git-transitions.ts'
29
30
  import { gitNoVerify, gitAmend } from './impl/git-verify.ts'
30
31
  import { gitProtectedTreeCommit } from './impl/git-protected-tree.ts'
@@ -63,7 +64,7 @@ import { adlPrCreateUpdateRole } from './impl/adl-pr-role.ts'
63
64
  import { adlPrReviewRole } from './impl/adl-pr-review-role.ts'
64
65
  import { adlPrCompletePendingDecision } from './impl/adl-pr-complete-pending.ts'
65
66
  import { adlWorkitemRequiresTarunOrVivek } from './impl/adl-workitem-role.ts'
66
- import { adlBuildRunDenied } from './impl/adl-build-run-denied.ts'
67
+ import { adlBuildRunRole } from './impl/adl-build-run-role.ts'
67
68
  import { toolFind } from './impl/tool-find.ts'
68
69
  import { toolGrep } from './impl/tool-grep.ts'
69
70
  import { toolNativeSearch } from './impl/tool-native-search.ts'
@@ -85,6 +86,14 @@ import { agentTamperProtection } from './impl/agent-tamper-protection.ts'
85
86
  export const GIT_RULES: readonly AnyRuleSpec[] = [
86
87
  gitHooksPath,
87
88
  gitCoreBare,
89
+ // Right after the `core.*` override rules: a `user.*` override rides
90
+ // along with any subcommand via `-c user.*=`, exactly the way
91
+ // `core.hooksPath` / `core.bare` do — a `git -c user.name=human commit`
92
+ // / `push` should be reported as the user override, not whatever
93
+ // commit/push policy would otherwise report. The user's call (2026-10-12)
94
+ // is "no use of git -c user* at all … blanket ban for agents"; see
95
+ // CONTRIBUTING.md line 18 and INVARIANTS.md line 55.
96
+ adlGitUserOverrideBlock,
88
97
  gitStash,
89
98
  gitClean,
90
99
  gitRebase,
@@ -150,7 +159,7 @@ export const TOOL_RULES: readonly AnyRuleSpec[] = [
150
159
  adlPrReviewRole,
151
160
  adlPrCompletePendingDecision,
152
161
  adlWorkitemRequiresTarunOrVivek,
153
- adlBuildRunDenied,
162
+ adlBuildRunRole,
154
163
  ].map(
155
164
  asAnyRuleSpec,
156
165
  )
@@ -29,10 +29,14 @@ export const PROTECTED_CARRIER_PATHS: ReadonlySet<string> = new Set([
29
29
  '.claude/settings.json',
30
30
  '.claude/settings.local.json',
31
31
  '.codex/config.toml',
32
- // `adl-config.toml` was the fourth entry until 2026-10-01 (Tarun: "nobody
33
- // should stop editing adl-config.toml"). It stays human-landed at commit time
34
- // (`git.protected-tree-commit`, repo-controls' integrity gate), but an agent
35
- // can now write it, and every hook run re-reads it.
32
+ // `adl-config.toml` is the repo's hook switch — every hook run re-reads it,
33
+ // so an agent that writes `[hooks] enabled = false` or disables any rule
34
+ // flips the gate off before the next tool call lands. Re-listed here on
35
+ // 2026-10-12 (Tarun: "we left a door open, close it"). The earlier
36
+ // 'human-only at commit time' assumption was wrong: the commit-time gate
37
+ // fires for the COMMITTED file, not for edits that never stage, and the
38
+ // working-tree copy is what every running hook reads.
39
+ 'adl-config.toml',
36
40
  '.codex/hooks.json',
37
41
  '.cursor/hooks.json',
38
42
  '.opencode/plugins/adl-repo.ts',