@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.
- 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/deploy-profile.json +1 -1
- 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/pre-tool-use/handler.ts +2 -2
- package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
- package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +12 -6
- package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
- package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +15 -14
- package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
- package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +8 -7
- package/packages/agent-hooks/src/rules/registry.ts +11 -2
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +9 -5
- package/packages/agent-settings/README.md +10 -4
- 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/code-review/SKILL.md +84 -66
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
- package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
- package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
- package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
- package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
- package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
- package/packages/agent-skills/assets/implement-feature/SKILL.md +153 -0
- package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
- package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
- package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
- package/packages/agent-skills/assets/prepare-promotion/SKILL.md +91 -0
- package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
- package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
- package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
- package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
- package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
- 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/agent-skills/assets/triage-workitems/SKILL.md +99 -0
- package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
- package/packages/agent-skills/assets/validate-design/SKILL.md +70 -0
- package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/workitems-process/SKILL.md +147 -0
- package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +194 -0
- package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
- package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
- package/packages/agent-skills/assets/write-feature/SKILL.md +95 -0
- package/packages/agent-skills/dist/deploy.d.ts +7 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +52 -5
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/src/deploy.ts +48 -4
- package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
- package/packages/azdo/src/activity-log.ts +118 -0
- package/packages/azdo/src/azdo-attachments.ts +108 -0
- package/packages/azdo/src/azdo-pl.ts +100 -3
- package/packages/azdo/src/azdo-pr.ts +194 -11
- package/packages/azdo/src/azdo-wit.ts +329 -18
- package/packages/azdo/src/cli.ts +42 -2
- package/packages/azdo/src/lib/agent-identity.ts +1 -1
- package/packages/azdo/src/lib/client.ts +7 -1
- package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
- package/packages/azdo/src/mcp-server.ts +53 -5
- package/packages/azdo/src/tool-dispatch.ts +53 -4
- package/packages/azdo/src/workflow-ladder.csv +125 -107
- package/packages/azdo/src/workflow-rules.ts +254 -97
- package/packages/repo-controls/README.md +24 -11
- package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
- package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
- 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/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
- 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
package/bin/adl
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/bin/adl
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
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.
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
144
|
-
session_id:
|
|
155
|
+
cwd,
|
|
156
|
+
session_id: sessionID,
|
|
145
157
|
};
|
|
146
|
-
const result = await runHookCommand(
|
|
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)
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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: "
|
|
199
|
-
|
|
200
|
-
|
|
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,
|
|
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
|
-
|
|
206
|
-
|
|
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: "
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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,
|
|
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)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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-
|
|
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-
|
|
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
|
+
}
|