@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.34 → 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 (104) 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/deploy-profile.json +1 -1
  7. package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -18
  8. package/packages/agent-hooks/src/deploy/render.ts +12 -3
  9. package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
  10. package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
  11. package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
  12. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +12 -6
  13. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
  14. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
  15. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +15 -14
  16. package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
  17. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  18. package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +8 -7
  19. package/packages/agent-hooks/src/rules/registry.ts +11 -2
  20. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +9 -5
  21. package/packages/agent-settings/README.md +10 -4
  22. package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +40 -0
  23. package/packages/agent-settings/src/global-agents.ts +43 -8
  24. package/packages/agent-skills/assets/adl-mechanics/SKILL.md +38 -0
  25. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +1 -1
  26. package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
  27. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
  28. package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
  29. package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
  30. package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
  31. package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
  32. package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
  33. package/packages/agent-skills/assets/implement-feature/SKILL.md +153 -0
  34. package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
  35. package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
  36. package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
  37. package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
  38. package/packages/agent-skills/assets/prepare-promotion/SKILL.md +91 -0
  39. package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
  40. package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
  41. package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
  42. package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
  43. package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
  44. package/packages/agent-skills/assets/to-feature/SKILL.md +1 -1
  45. package/packages/agent-skills/assets/to-workitems/SKILL.md +1 -1
  46. package/packages/agent-skills/assets/to-workitems/references/AZURE-DEVOPS-HIERARCHY.md +3 -0
  47. package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
  48. package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
  49. package/packages/agent-skills/assets/validate-design/SKILL.md +70 -0
  50. package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
  51. package/packages/agent-skills/assets/workitems-process/SKILL.md +147 -0
  52. package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +194 -0
  53. package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
  54. package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
  55. package/packages/agent-skills/assets/write-feature/SKILL.md +95 -0
  56. package/packages/agent-skills/dist/deploy.d.ts +7 -0
  57. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  58. package/packages/agent-skills/dist/deploy.js +52 -5
  59. package/packages/agent-skills/dist/deploy.js.map +1 -1
  60. package/packages/agent-skills/src/deploy.ts +48 -4
  61. package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
  62. package/packages/azdo/src/activity-log.ts +118 -0
  63. package/packages/azdo/src/azdo-attachments.ts +108 -0
  64. package/packages/azdo/src/azdo-pl.ts +100 -3
  65. package/packages/azdo/src/azdo-pr.ts +194 -11
  66. package/packages/azdo/src/azdo-wit.ts +329 -18
  67. package/packages/azdo/src/cli.ts +42 -2
  68. package/packages/azdo/src/lib/agent-identity.ts +1 -1
  69. package/packages/azdo/src/lib/client.ts +7 -1
  70. package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
  71. package/packages/azdo/src/mcp-server.ts +53 -5
  72. package/packages/azdo/src/tool-dispatch.ts +53 -4
  73. package/packages/azdo/src/workflow-ladder.csv +125 -107
  74. package/packages/azdo/src/workflow-rules.ts +254 -97
  75. package/packages/repo-controls/README.md +24 -11
  76. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
  77. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
  78. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +60 -11
  79. package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +28 -0
  80. package/packages/repo-controls/assets/scaffold/root/INVARIANTS.md +17 -0
  81. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
  82. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
  83. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
  84. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
  85. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
  86. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
  87. 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
  88. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
  89. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
  90. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
  91. 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
  92. 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
  93. 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
  94. 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
  95. 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
  96. 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
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
  98. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
  100. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
  101. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
  102. package/packages/repo-controls/src/cli.ts +23 -5
  103. package/packages/repo-controls/src/constants.ts +2 -15
  104. package/packages/repo-controls/src/install.ts +15 -6
package/bin/adl CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/bin/adl
3
- size: 82945504 bytes
4
- sha256: b6594d01b2ebea8949f80faa8da029598f8be7513fb1e3c7b63ddc01df4c8f02
3
+ size: 82998752 bytes
4
+ sha256: a9c4655905b5f4b937f9d93dadeb7ad5ae28dec1392a767225242478684aff98
package/bin/reconcile CHANGED
@@ -186,6 +186,7 @@ collect_sweep_backups() {
186
186
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
187
187
  repo_root="$(cd "$script_dir/.." && pwd)"
188
188
  manifest="${manifest:-$repo_root/packages/fleet/manifest.json}"
189
+ manifest="$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest")"
189
190
  # shellcheck source=lib/bun-compile-build.sh
190
191
  source "$script_dir/lib/bun-compile-build.sh"
191
192
  # shellcheck source=lib/adl-package.sh
@@ -3055,13 +3056,13 @@ fi
3055
3056
  if [[ "$global_agents_enabled" == true ]]; then
3056
3057
  if [[ "$mode" != apply ]]; then
3057
3058
  echo "global-tools: global agents mode=status home=$administrator_home"
3058
- "$BASH" "$script_dir/global-agents" --home "$administrator_home" --mode status
3059
+ "$BASH" "$script_dir/global-agents" --home "$administrator_home" --mode status --host "$device" --manifest "$manifest"
3059
3060
  else
3060
3061
  echo "global-tools: global agents mode=deploy home=$administrator_home (as $administrator)"
3061
3062
  # The exact target files are authoritative global artifacts. Drift is
3062
- # replaced from GLOBAL-AGENTS.md with no backup or leave-in-place branch.
3063
+ # replaced from TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md with no backup or leave-in-place branch.
3063
3064
  (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
3064
- "$BASH" "$script_dir/global-agents" --home "$administrator_home" --mode deploy)
3065
+ "$BASH" "$script_dir/global-agents" --home "$administrator_home" --mode deploy --host "$device" --manifest "$manifest")
3065
3066
  fi
3066
3067
  fi
3067
3068
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amsterdamdatalabs/adl-agent-control-linux-x64",
3
- "version": "0.1.34",
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": [
@@ -474,6 +474,17 @@ rendered by `src/deploy/opencode-template.ts` from `assets/opencode-plugin.templ
474
474
  `assets/hooks.json` and the OpenCode surface in `deploy-profile.json`. Do not edit
475
475
  the deployed file by hand; re-run `adl install`.
476
476
 
477
+ - The deployed file uses OpenCode v2's plugin shape (`export default { id, setup }`,
478
+ with `setup(ctx)` imperatively registering hooks via `ctx.session.hook` /
479
+ `ctx.tool.hook` / `ctx.event.subscribe` — confirmed against
480
+ `github.com/anomalyco/opencode/blob/dev/packages/core/src/plugin/module.ts`
481
+ and the promise adapter at
482
+ `github.com/anomalyco/opencode/blob/dev/packages/plugin/src/promise/adapter.ts`).
483
+ v2 dropped v1's `chat.message` + `experimental.chat.system.transform` events; the
484
+ template maps the equivalent flow to `ctx.session.hook("prompt", ...)` (run
485
+ `user-prompt-submit` per submission, stash the additionalContext) and
486
+ `ctx.session.hook("context", ...)` (push stashed context into `event.system`
487
+ immediately before provider dispatch).
477
488
  - The deployed file has no multi-line comments (Tarun 2026-10-08, Bug 602), so it
478
489
  passes compliance control REP-007. Its header is a single line pointing here.
479
490
  - Fallback projection: an event with no documented OpenCode equivalent runs on a
@@ -481,5 +492,8 @@ the deployed file by hand; re-run `adl install`.
481
492
  `packages/agent-hooks/deploy-profile.json` (today `SessionEnd -> SessionIdle`).
482
493
  The generated file lists the map as one line, `// Event fallbacks: A -> B, C -> D`,
483
494
  and emits no line at all when the map is empty.
495
+ - `UserPromptSubmit` is intentionally not routed to any opencode slot — the v2
496
+ public plugin model has no replacement for v1's `chat.message` event; the rule
497
+ still runs for claude/codex/cursor/kimi through their own carriers.
484
498
  - Source of truth for hook commands is `assets/hooks.json`; the generated arrays
485
499
  carry `command`, `matcher`, `timeoutMs` and `fallbackFor` per entry.
@@ -2,6 +2,8 @@
2
2
 
3
3
  __ADL_FALLBACK_ENTRIES__
4
4
 
5
+ // OpenCode v2 plugin: `export default { id, setup }`; setup(ctx) imperatively registers hooks via ctx.session.hook / ctx.tool.hook / ctx.event.subscribe.
6
+
5
7
  interface HookCommandSpec {
6
8
  command: string;
7
9
  matcher?: string;
@@ -27,10 +29,6 @@ const HOOKS_TOOL_EXECUTE_AFTER: HookCommandSpec[] = [
27
29
  __ADL_ARRAY_HOOKS_TOOL_EXECUTE_AFTER__
28
30
  ];
29
31
 
30
- const HOOKS_CHAT_MESSAGE: HookCommandSpec[] = [
31
- __ADL_ARRAY_HOOKS_CHAT_MESSAGE__
32
- ];
33
-
34
32
  const HOOKS_EVENT_SESSION_CREATED: HookCommandSpec[] = [
35
33
  __ADL_ARRAY_HOOKS_EVENT_SESSION_CREATED__
36
34
  ];
@@ -47,30 +45,41 @@ async function runHookCommand(
47
45
  payload: Record<string, unknown>,
48
46
  cwd: string,
49
47
  ): Promise<{ exitCode: number | null; stdout: string; stderr: string }> {
50
- try {
51
- const proc = Bun.spawn(["/bin/sh", "-c", spec.command], {
52
- cwd,
53
- stdin: "pipe",
54
- stdout: "pipe",
55
- stderr: "pipe",
56
- ...(spec.timeoutMs ? { timeout: spec.timeoutMs, killSignal: "SIGTERM" } : {}),
57
- });
58
- proc.stdin.write(JSON.stringify(payload));
59
- proc.stdin.end();
60
- const [stdout, stderr] = await Promise.all([
61
- new Response(proc.stdout).text(),
62
- new Response(proc.stderr).text(),
63
- ]);
64
- await proc.exited;
65
- if (proc.exitCode !== 0 && proc.exitCode !== 2) {
66
- console.error("[" + PLUGIN_NAME + "] hook command exited " + proc.exitCode + ": " + spec.command);
48
+ const { spawn } = await import("node:child_process");
49
+ return new Promise((resolve) => {
50
+ try {
51
+ const proc = spawn("/bin/sh", ["-c", spec.command], {
52
+ cwd,
53
+ stdio: ["pipe", "pipe", "pipe"],
54
+ });
55
+ let stdout = "";
56
+ let stderr = "";
57
+ proc.stdout!.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); });
58
+ proc.stderr!.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); });
59
+ const timer = spec.timeoutMs
60
+ ? setTimeout(() => { try { proc.kill("SIGTERM"); } catch {} }, spec.timeoutMs)
61
+ : null;
62
+ proc.on("error", (err) => {
63
+ if (timer) clearTimeout(timer);
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ console.error("[" + PLUGIN_NAME + "] hook command crashed: " + spec.command + " -- " + message);
66
+ resolve({ exitCode: null, stdout: "", stderr: message });
67
+ });
68
+ proc.on("exit", (code) => {
69
+ if (timer) clearTimeout(timer);
70
+ if (code !== 0 && code !== 2) {
71
+ console.error("[" + PLUGIN_NAME + "] hook command exited " + code + ": " + spec.command);
72
+ }
73
+ resolve({ exitCode: code, stdout, stderr });
74
+ });
75
+ proc.stdin!.write(JSON.stringify(payload));
76
+ proc.stdin!.end();
77
+ } catch (err) {
78
+ const message = err instanceof Error ? err.message : String(err);
79
+ console.error("[" + PLUGIN_NAME + "] hook command crashed: " + spec.command + " -- " + message);
80
+ resolve({ exitCode: null, stdout: "", stderr: message });
67
81
  }
68
- return { exitCode: proc.exitCode, stdout, stderr };
69
- } catch (err) {
70
- const message = err instanceof Error ? err.message : String(err);
71
- console.error("[" + PLUGIN_NAME + "] hook command crashed: " + spec.command + " -- " + message);
72
- return { exitCode: null, stdout: "", stderr: message };
73
- }
82
+ });
74
83
  }
75
84
 
76
85
  function denyReason(result: { exitCode: number | null; stdout: string; stderr: string }): string | undefined {
@@ -113,9 +122,7 @@ function applyUpdatedInput(
113
122
  }
114
123
  }
115
124
 
116
- const adlPromptContext = new Map<string, string>();
117
-
118
- function extractHookContext(result: { exitCode: number | null; stdout: string; stderr: string }): string | undefined {
125
+ function extractHookContext(result: { stdout: string }): string | undefined {
119
126
  const trimmed = result.stdout.trim();
120
127
  if (!trimmed) return undefined;
121
128
  try {
@@ -130,91 +137,120 @@ function extractHookContext(result: { exitCode: number | null; stdout: string; s
130
137
  return trimmed;
131
138
  }
132
139
 
133
- export const __ADL_PLUGIN_IDENTIFIER__ = async ({ directory }: { directory: string }) => {
134
- return {
135
- "chat.message": async (input: any, output: any) => {
136
- const parts = output && Array.isArray(output.parts) ? output.parts : [];
137
- const promptText = parts.filter((p: any) => p && p.type === "text").map((p: any) => p.text).join(" ");
138
- let context: string | undefined;
139
- for (const spec of HOOKS_CHAT_MESSAGE) {
140
+ export default {
141
+ id: "__ADL_PLUGIN_NAME__",
142
+ setup: async (ctx: any) => {
143
+ const cwd = ctx.location?.directory ?? process.cwd();
144
+ const adlPromptContext = new Map<string, string[]>();
145
+
146
+ // Per-prompt: run user-prompt-submit, stash additionalContext keyed by sessionID.
147
+ await ctx.session.hook("prompt", async (event: any) => {
148
+ const sessionID: string | undefined = event?.sessionID;
149
+ if (!sessionID) return;
150
+ const promptText: string = event?.prompt?.text ?? "";
151
+ const out: string[] = [];
140
152
  const payload = {
141
153
  hook_event_name: "UserPromptSubmit",
142
154
  prompt: promptText,
143
- cwd: directory,
144
- session_id: input && input.sessionID,
155
+ cwd,
156
+ session_id: sessionID,
145
157
  };
146
- const result = await runHookCommand(spec, payload, directory);
158
+ const result = await runHookCommand(
159
+ { command: "adl hook user-prompt-submit --host=opencode", timeoutMs: 15000 },
160
+ payload,
161
+ cwd,
162
+ );
147
163
  const extracted = extractHookContext(result);
148
- if (extracted) context = context ? context + " " + extracted : extracted;
149
- }
150
- const sessionID = input && input.sessionID;
151
- if (sessionID) {
152
- if (context) adlPromptContext.set(sessionID, context);
164
+ if (extracted) out.push(extracted);
165
+ if (out.length) adlPromptContext.set(sessionID, out);
153
166
  else adlPromptContext.delete(sessionID);
154
- }
155
- },
156
- "experimental.chat.system.transform": async (input: any, output: any) => {
157
- const sessionID = input && input.sessionID;
158
- if (!sessionID) return;
159
- const context = adlPromptContext.get(sessionID);
160
- if (context && output && Array.isArray(output.system)) {
161
- output.system.push(context);
162
- }
163
- },
164
- "tool.execute.before": async (input: ToolHookInput, output: ToolHookOutput) => {
165
- for (const spec of HOOKS_TOOL_EXECUTE_BEFORE) {
166
- if (spec.matcher && !new RegExp("^(?:" + spec.matcher + ")$", "i").test(input.tool)) continue;
167
- const payload = {
168
- hook_event_name: "PreToolUse",
169
- tool_name: input.tool,
170
- tool_input: output.args,
171
- cwd: directory,
172
- session_id: input.sessionID,
173
- };
174
- const result = await runHookCommand(spec, payload, directory);
175
- const reason = denyReason(result);
176
- if (reason) throw new Error(reason);
177
- applyUpdatedInput(result, output);
178
- }
179
- },
180
- "tool.execute.after": async (input: ToolHookInput, output: ToolHookOutput) => {
181
- for (const spec of HOOKS_TOOL_EXECUTE_AFTER) {
182
- if (spec.matcher && !new RegExp("^(?:" + spec.matcher + ")$", "i").test(input.tool)) continue;
183
- const payload = {
184
- hook_event_name: "PostToolUse",
185
- tool_name: input.tool,
186
- tool_input: output.args,
187
- cwd: directory,
188
- session_id: input.sessionID,
189
- };
190
- const result = await runHookCommand(spec, payload, directory);
191
- // PostToolUse cannot block: the tool call already completed.
192
- }
193
- },
194
- event: async ({ event }: { event: any }) => {
195
- if (event && event.type === "session.created") {
196
- for (const spec of HOOKS_EVENT_SESSION_CREATED) {
167
+ });
168
+
169
+ // Before provider dispatch: push stashed context into the system prompt array.
170
+ await ctx.session.hook("context", (event: any) => {
171
+ const sessionID: string | undefined = event?.sessionID;
172
+ if (!sessionID) return;
173
+ const out = adlPromptContext.get(sessionID);
174
+ if (!out || out.length === 0) return;
175
+ if (!Array.isArray(event.system)) return;
176
+ for (const text of out) {
177
+ event.system.push({ type: "text", text });
178
+ }
179
+ });
180
+
181
+ // Pre-tool-use: deny / rewrite. Always runs pre-tool-use; crg:pre-commit only on bash.
182
+ await ctx.tool.hook("execute.before", async (event: any) => {
183
+ const input = event as ToolHookInput;
184
+ const output = { args: (event?.input ?? {}) as Record<string, unknown> };
185
+ for (const spec of HOOKS_TOOL_EXECUTE_BEFORE) {
186
+ if (spec.matcher && !new RegExp("^(?:" + spec.matcher + ")$", "i").test(input.tool)) continue;
197
187
  const payload = {
198
- hook_event_name: "SessionStart",
199
- cwd: directory,
200
- session_id: event.properties && (event.properties.sessionID || (event.properties.info && event.properties.info.id)),
188
+ hook_event_name: "PreToolUse",
189
+ tool_name: input.tool,
190
+ tool_input: output.args,
191
+ cwd,
192
+ session_id: input.sessionID,
201
193
  };
202
- await runHookCommand(spec, payload, directory);
194
+ const result = await runHookCommand(spec, payload, cwd);
195
+ const reason = denyReason(result);
196
+ if (reason) throw new Error(reason);
197
+ applyUpdatedInput(result, output);
203
198
  }
204
- }
205
- if (event && event.type === "session.idle") {
206
- for (const spec of HOOKS_EVENT_SESSION_IDLE) {
199
+ if (output.args !== event?.input && event && "input" in event) {
200
+ event.input = output.args;
201
+ }
202
+ });
203
+
204
+ // Post-tool-use: observational, can't block. Runs after-file-edit on write|edit.
205
+ await ctx.tool.hook("execute.after", async (event: any) => {
206
+ const input = event as ToolHookInput;
207
+ for (const spec of HOOKS_TOOL_EXECUTE_AFTER) {
208
+ if (spec.matcher && !new RegExp("^(?:" + spec.matcher + ")$", "i").test(input.tool)) continue;
207
209
  const payload = {
208
- hook_event_name: "SessionIdle",
209
- cwd: directory,
210
- session_id: event.properties && (event.properties.sessionID || (event.properties.info && event.properties.info.id)),
211
- ...(spec.fallbackFor ? { fallback_for: spec.fallbackFor } : {}),
210
+ hook_event_name: "PostToolUse",
211
+ tool_name: input.tool,
212
+ tool_input: (event?.input ?? {}),
213
+ cwd,
214
+ session_id: input.sessionID,
212
215
  };
213
- await runHookCommand(spec, payload, directory);
216
+ await runHookCommand(spec, payload, cwd);
214
217
  }
218
+ });
219
+
220
+ // session.created / session.idle: subscribe to the event stream and dispatch.
221
+ if (ctx.event && typeof ctx.event.subscribe === "function") {
222
+ (async () => {
223
+ try {
224
+ for await (const event of ctx.event.subscribe()) {
225
+ const type = event?.type;
226
+ if (type === "session.created") {
227
+ const sessionID: string | undefined = event?.properties?.sessionID ?? event?.properties?.info?.id;
228
+ for (const spec of HOOKS_EVENT_SESSION_CREATED) {
229
+ const payload = {
230
+ hook_event_name: "SessionStart",
231
+ cwd,
232
+ session_id: sessionID,
233
+ };
234
+ await runHookCommand(spec, payload, cwd);
235
+ }
236
+ } else if (type === "session.idle") {
237
+ const sessionID: string | undefined = event?.properties?.sessionID ?? event?.properties?.info?.id;
238
+ for (const spec of HOOKS_EVENT_SESSION_IDLE) {
239
+ const payload = {
240
+ hook_event_name: "SessionIdle",
241
+ cwd,
242
+ session_id: sessionID,
243
+ ...(spec.fallbackFor ? { fallback_for: spec.fallbackFor } : {}),
244
+ };
245
+ await runHookCommand(spec, payload, cwd);
246
+ }
247
+ }
248
+ }
249
+ } catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ console.error("[" + PLUGIN_NAME + "] event subscriber crashed: " + message);
252
+ }
253
+ })();
215
254
  }
216
255
  },
217
- };
218
- };
219
-
220
- export default __ADL_PLUGIN_IDENTIFIER__;
256
+ };
@@ -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",
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Fills assets/opencode-plugin.template.ts with the rendered hook arrays
3
- * (render.ts's renderOpencodeSlots), the profile's pluginName, and the
4
- * identifier derived from it — reproducing this repo's own committed
5
- * `.opencode/plugins/adl-repo.ts` byte-for-byte for the current
6
- * deploy-profile.json + assets/hooks.json, except the header (a one-line
7
- * comment plus an optional one-line fallback comment; no multi-line comments in
8
- * deployed code, Bug 602). See README "Generated OpenCode plugin".
3
+ * (render.ts's renderOpencodeSlots) and the profile's pluginName — reproducing
4
+ * this repo's own committed `.opencode/plugins/adl-repo.ts` byte-for-byte for
5
+ * the current deploy-profile.json + assets/hooks.json, except the header
6
+ * (a one-line comment plus an optional one-line fallback comment; no multi-line
7
+ * comments in deployed code, Bug 602). See README "Generated OpenCode plugin".
9
8
  */
10
9
  import { readFileSync } from 'node:fs'
11
10
  import { join } from 'node:path'
@@ -13,14 +12,6 @@ import type { HookSurface } from './profile.ts'
13
12
  import { OPENCODE_SLOTS, type RenderedOpencodeEntry, renderOpencodeSlots } from './render.ts'
14
13
  import type { CanonicalHooksDoc } from './source.ts'
15
14
 
16
- /** kebab-case -> camelCase, e.g. "adl-repo" -> "adlRepo". Ported from the deleted generator's toIdentifier (git show 5b34d10:packages/agent-skills/src/create/opencode.ts). */
17
- function toCamelCase(pluginName: string): string {
18
- return pluginName
19
- .split('-')
20
- .map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
21
- .join('')
22
- }
23
-
24
15
  function serializeEntry(entry: RenderedOpencodeEntry): string {
25
16
  const fields: string[] = [`command: ${JSON.stringify(entry.command)}`]
26
17
  if (entry.matcher !== undefined) fields.push(`matcher: ${JSON.stringify(entry.matcher)}`)
@@ -68,9 +59,5 @@ export function renderOpencodePlugin(options: RenderOpencodePluginOptions): stri
68
59
  if (!out.includes('__ADL_PLUGIN_NAME__')) throw new Error(`opencode plugin template is missing marker __ADL_PLUGIN_NAME__: ${templatePath}`)
69
60
  out = out.split('__ADL_PLUGIN_NAME__').join(surface.pluginName)
70
61
 
71
- const identifier = `${toCamelCase(surface.pluginName)}Plugin`
72
- if (!out.includes('__ADL_PLUGIN_IDENTIFIER__')) throw new Error(`opencode plugin template is missing marker __ADL_PLUGIN_IDENTIFIER__: ${templatePath}`)
73
- out = out.split('__ADL_PLUGIN_IDENTIFIER__').join(identifier)
74
-
75
62
  return out
76
63
  }
@@ -119,24 +119,33 @@ export interface RenderedOpencodeEntry {
119
119
  const OPENCODE_SLOT_FOR_EVENT: Readonly<Record<string, string>> = {
120
120
  PreToolUse: 'HOOKS_TOOL_EXECUTE_BEFORE',
121
121
  PostToolUse: 'HOOKS_TOOL_EXECUTE_AFTER',
122
- UserPromptSubmit: 'HOOKS_CHAT_MESSAGE',
123
122
  SessionStart: 'HOOKS_EVENT_SESSION_CREATED',
124
123
  SessionEnd: 'HOOKS_EVENT_SESSION_IDLE',
125
124
  }
126
125
 
126
+ /**
127
+ * Canonical events intentionally NOT routed to any opencode plugin slot.
128
+ * v2's public plugin model dropped the v1 `chat.message` +
129
+ * `experimental.chat.system.transform` pair that carried UserPromptSubmit,
130
+ * so the rule still runs for claude/codex/cursor/kimi through their own
131
+ * carriers but is invisible to the opencode surface. Add a new entry here
132
+ * only with the same justification.
133
+ */
134
+ const OPENCODE_UNSUPPORTED_CANONICAL_EVENTS: ReadonlySet<string> = new Set(['UserPromptSubmit'])
135
+
127
136
  export const OPENCODE_SLOTS: readonly string[] = [
128
137
  'HOOKS_TOOL_EXECUTE_BEFORE',
129
138
  'HOOKS_TOOL_EXECUTE_AFTER',
130
- 'HOOKS_CHAT_MESSAGE',
131
139
  'HOOKS_EVENT_SESSION_CREATED',
132
140
  'HOOKS_EVENT_SESSION_IDLE',
133
141
  ]
134
142
 
135
- /** slot name -> rendered entries, in source order. Every event in `doc` must have a known slot (see OPENCODE_SLOT_FOR_EVENT). */
143
+ /** slot name -> rendered entries, in source order. Every event in `doc` must have a known slot or be in OPENCODE_UNSUPPORTED_CANONICAL_EVENTS. */
136
144
  export function renderOpencodeSlots(doc: CanonicalHooksDoc, surface: HookSurface): ReadonlyMap<string, readonly RenderedOpencodeEntry[]> {
137
145
  const out = new Map<string, RenderedOpencodeEntry[]>()
138
146
  for (const slot of OPENCODE_SLOTS) out.set(slot, [])
139
147
  for (const [event, groups] of doc) {
148
+ if (OPENCODE_UNSUPPORTED_CANONICAL_EVENTS.has(event)) continue
140
149
  const slot = OPENCODE_SLOT_FOR_EVENT[event]
141
150
  if (slot === undefined) throw new Error(`opencode-plugin has no template slot for canonical event "${event}"`)
142
151
  // `fallbackFor` names the CANONICAL event being substituted (e.g.
@@ -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
+ }