@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.35 → 0.1.46
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 +3 -11
- package/bin/setup +15 -15
- package/package.json +1 -1
- package/packages/agent-hooks/deploy-profile.json +1 -1
- package/packages/agent-hooks/package.json +1 -1
- package/packages/agent-hooks/src/main.ts +23 -2
- 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 +22 -12
- 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/adl-run-required.ts +11 -5
- package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +18 -5
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +30 -25
- package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
- package/packages/agent-hooks/src/rules/impl/git-target.ts +5 -0
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +4 -2
- package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +230 -11
- package/packages/agent-hooks/src/rules/registry.ts +11 -2
- package/packages/agent-hooks/src/rules/shell/git-identity.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/invocation.ts +14 -2
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +10 -7
- package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +2 -2
- package/packages/agent-settings/package.json +1 -1
- package/packages/agent-settings/src/deploy/agents-deploy.ts +19 -1
- package/packages/agent-settings/src/deploy/index.ts +1 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +4 -6
- 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 +150 -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 +90 -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/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 +64 -0
- package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/wayfinder/SKILL.md +1 -1
- package/packages/agent-skills/assets/workitems-process/SKILL.md +145 -0
- package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +227 -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 +94 -0
- package/packages/agent-skills/dist/deploy.d.ts +52 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +218 -7
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/dist/index.d.ts +1 -1
- package/packages/agent-skills/dist/index.d.ts.map +1 -1
- package/packages/agent-skills/dist/index.js +1 -1
- package/packages/agent-skills/dist/index.js.map +1 -1
- package/packages/agent-skills/package.json +1 -1
- package/packages/agent-skills/src/deploy.ts +288 -6
- package/packages/agent-skills/src/index.ts +10 -1
- package/packages/azdo/README.md +13 -4
- package/packages/azdo/bunfig.toml +6 -0
- package/packages/azdo/package.json +1 -1
- 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 +109 -0
- package/packages/azdo/src/azdo-pl.ts +100 -3
- package/packages/azdo/src/azdo-pr.ts +376 -17
- package/packages/azdo/src/azdo-wit.ts +594 -34
- package/packages/azdo/src/cli.ts +119 -12
- package/packages/azdo/src/lib/client.ts +95 -17
- package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
- package/packages/azdo/src/mcp-server.ts +121 -8
- package/packages/azdo/src/tool-dispatch.ts +107 -23
- package/packages/azdo/src/workflow-ladder.csv +125 -107
- package/packages/azdo/src/workflow-rules.ts +254 -97
- package/packages/fleet/src/install-adl-package.ts +4 -3
- package/packages/repo-controls/README.md +12 -4
- package/packages/repo-controls/assets/promote.ts +1 -0
- 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/CONTRIBUTING.md +5 -5
- 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/fixtures/okf-profile-untyped/docs/research/no-type.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-experiment.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-research.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-last-update.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +1 -1
- 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/package.json +1 -1
- package/packages/repo-controls/src/config.ts +21 -2
- package/packages/repo-controls/src/doctor.ts +9 -14
- package/packages/repo-controls/src/findings-ratchet.ts +12 -0
- package/packages/repo-controls/src/hook.ts +61 -4
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** A1 agent activity log — design: docs/research/adl-process-handover/process-design.md § Activity log. */
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { resolveAgentIdentity } from './lib/agent-identity.ts';
|
|
7
|
+
|
|
8
|
+
export interface ActivityEntry {
|
|
9
|
+
time: string;
|
|
10
|
+
surface: string;
|
|
11
|
+
agent: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
note: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Verified child-process surface markers, first match wins; a marker is listed only with a citation to the surface's own docs. */
|
|
17
|
+
const SURFACE_MARKERS: ReadonlyArray<{ readonly surface: string; readonly env: string; readonly equals?: string }> = [
|
|
18
|
+
// Claude Code spawns set CLAUDECODE=1 (code.claude.com/docs/en/env-vars, verified 2026-10-12).
|
|
19
|
+
{ surface: 'claude', env: 'CLAUDECODE', equals: '1' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** The surface this process runs under: first verified marker match, else 'cli' (no marker is not an error). */
|
|
23
|
+
export function resolveSurface(env: NodeJS.ProcessEnv = process.env): string {
|
|
24
|
+
for (const marker of SURFACE_MARKERS) {
|
|
25
|
+
const value = env[marker.env];
|
|
26
|
+
if (value !== undefined && (marker.equals === undefined || value === marker.equals)) {
|
|
27
|
+
return marker.surface;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return 'cli';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The git work-tree top level of `cwd` (or the process's own), or a loud refusal outside a work tree — the log lives at `<repo>/.adl/agent-activity.sqlite`. */
|
|
34
|
+
export function repoTopLevel(cwd?: string): string {
|
|
35
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
36
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
timeout: 5000,
|
|
39
|
+
});
|
|
40
|
+
const topLevel = result.status === 0 ? result.stdout.trim() : '';
|
|
41
|
+
if (!topLevel) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'the activity log lives at <repo>/.adl/agent-activity.sqlite, and this working directory is not a git work tree',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return topLevel;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The ONE place the log's path is built. */
|
|
50
|
+
export function activityDbPath(cwd?: string): string {
|
|
51
|
+
return join(repoTopLevel(cwd), '.adl', 'agent-activity.sqlite');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function openDb(cwd?: string): DatabaseSync {
|
|
55
|
+
const path = activityDbPath(cwd);
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
57
|
+
const db = new DatabaseSync(path);
|
|
58
|
+
db.exec(
|
|
59
|
+
`CREATE TABLE IF NOT EXISTS activity (
|
|
60
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
61
|
+
time TEXT NOT NULL,
|
|
62
|
+
surface TEXT NOT NULL,
|
|
63
|
+
agent TEXT NOT NULL,
|
|
64
|
+
repo TEXT NOT NULL,
|
|
65
|
+
note TEXT NOT NULL
|
|
66
|
+
)`,
|
|
67
|
+
);
|
|
68
|
+
return db;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertNote(note: unknown): asserts note is string {
|
|
72
|
+
if (typeof note !== 'string' || note.trim() === '') {
|
|
73
|
+
throw new Error('note must be a non-empty string');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Append one entry, filling time/surface/agent/repo; `env`/`now` keep tests hermetic. Returns the stored entry as JSON. */
|
|
78
|
+
export function activityAdd(
|
|
79
|
+
note: unknown,
|
|
80
|
+
opts: { cwd?: string; env?: NodeJS.ProcessEnv; now?: Date } = {},
|
|
81
|
+
): string {
|
|
82
|
+
assertNote(note);
|
|
83
|
+
const env = opts.env ?? process.env;
|
|
84
|
+
const entry: ActivityEntry = {
|
|
85
|
+
time: (opts.now ?? new Date()).toISOString(),
|
|
86
|
+
surface: resolveSurface(env),
|
|
87
|
+
agent: resolveAgentIdentity(env).agent,
|
|
88
|
+
repo: basename(repoTopLevel(opts.cwd)),
|
|
89
|
+
note,
|
|
90
|
+
};
|
|
91
|
+
const db = openDb(opts.cwd);
|
|
92
|
+
try {
|
|
93
|
+
db.prepare('INSERT INTO activity (time, surface, agent, repo, note) VALUES (?, ?, ?, ?, ?)').run(
|
|
94
|
+
entry.time,
|
|
95
|
+
entry.surface,
|
|
96
|
+
entry.agent,
|
|
97
|
+
entry.repo,
|
|
98
|
+
entry.note,
|
|
99
|
+
);
|
|
100
|
+
} finally {
|
|
101
|
+
db.close();
|
|
102
|
+
}
|
|
103
|
+
return JSON.stringify(entry, null, 2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Every entry, oldest first (the reading order of the ledger this replaces). */
|
|
107
|
+
export function activityList(opts: { cwd?: string } = {}): string {
|
|
108
|
+
const db = openDb(opts.cwd);
|
|
109
|
+
let entries: unknown[];
|
|
110
|
+
try {
|
|
111
|
+
entries = db
|
|
112
|
+
.prepare('SELECT time, surface, agent, repo, note FROM activity ORDER BY id ASC')
|
|
113
|
+
.all();
|
|
114
|
+
} finally {
|
|
115
|
+
db.close();
|
|
116
|
+
}
|
|
117
|
+
return JSON.stringify({ repo: basename(repoTopLevel(opts.cwd)), entries }, null, 2);
|
|
118
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// T2 work-item attachments (board-rules.md § Attachments): Features only, frozen once attached — the one delete allowed is a mistaken upload in its own run, for a plan-r* file while every Feature item is still at or before Awaiting Approval. API facts (WIT REST 7.1): upload POSTs .../_apis/wit/attachments?fileName={name} (octet-stream, returns { id, url }); attach PATCHes the work item adding an AttachedFile relation (test/rev gives optimistic concurrency); Attachments-Delete is 7.2-preview only, so delete detaches and the stored file stays.
|
|
2
|
+
import { AdoClient } from './lib/client.ts';
|
|
3
|
+
|
|
4
|
+
let client: AdoClient | undefined
|
|
5
|
+
|
|
6
|
+
async function getClient(): Promise<AdoClient> {
|
|
7
|
+
client ??= new AdoClient()
|
|
8
|
+
return client
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Test seam: forget the cached client — see azdo-wit.ts's resetClientForTests() for why. */
|
|
12
|
+
export function resetClientForTests(): void {
|
|
13
|
+
client = undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type WorkitemAttachmentsArgs = {
|
|
17
|
+
id: number;
|
|
18
|
+
action: 'read' | 'upload' | 'delete';
|
|
19
|
+
/** upload/delete: the attachment file name. */
|
|
20
|
+
fileName?: string;
|
|
21
|
+
/** upload: the file content, sent as an octet-stream body. */
|
|
22
|
+
content?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface AttachmentRelation {
|
|
26
|
+
index: number;
|
|
27
|
+
name: string;
|
|
28
|
+
url: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function attachedFiles(item: any): AttachmentRelation[] {
|
|
32
|
+
return (item.relations ?? [])
|
|
33
|
+
.map((relation: any, index: number) => ({ relation, index }))
|
|
34
|
+
.filter(({ relation }: any) => relation.rel === 'AttachedFile')
|
|
35
|
+
.map(({ relation, index }: any) => ({
|
|
36
|
+
index,
|
|
37
|
+
name: relation.attributes?.name ?? relation.url,
|
|
38
|
+
url: relation.url,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** P4 reuses this upload when it attaches the mandatory plan/impl report at PR raise. */
|
|
43
|
+
export async function uploadAttachment(c: AdoClient, workItemId: number, fileName: string, content: string): Promise<void> {
|
|
44
|
+
const uploaded = await c.post<any>(
|
|
45
|
+
'/wit/attachments',
|
|
46
|
+
content,
|
|
47
|
+
{ fileName, 'api-version': '7.1' },
|
|
48
|
+
'application/octet-stream',
|
|
49
|
+
);
|
|
50
|
+
if (!uploaded?.url) throw new Error(`attachment upload for '${fileName}' returned no url`);
|
|
51
|
+
const item = await c.get<any>(`/wit/workitems/${workItemId}`, { 'api-version': '7.1' });
|
|
52
|
+
await c.patch<any>(
|
|
53
|
+
`/wit/workitems/${workItemId}`,
|
|
54
|
+
[
|
|
55
|
+
{ op: 'test', path: '/rev', value: item.rev ?? item.fields?.['System.Rev'] },
|
|
56
|
+
{ op: 'add', path: '/relations/-', value: { rel: 'AttachedFile', url: uploaded.url } },
|
|
57
|
+
],
|
|
58
|
+
{ 'api-version': '7.1' },
|
|
59
|
+
'application/json-patch+json',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The T2 guard (board-rules.md § Attachments): a plan-r* file refuses deletion once any item of its Feature has left Awaiting Approval — the plan G1 approved stays on the board. */
|
|
64
|
+
async function assertPlanAttachmentDeletable(c: AdoClient, feature: any, fileName: string): Promise<void> {
|
|
65
|
+
const relationId = (url: string) => Number(/(\d+)\/?$/.exec(url)?.[1]);
|
|
66
|
+
// `feature.relations` is always an array here: attachedFiles() returned a target off this same object, so the property was a non-empty array moments earlier -- the nullish fallback was unreachable.
|
|
67
|
+
const childIds = feature.relations
|
|
68
|
+
.filter((relation: any) => relation.rel === 'System.LinkTypes.Hierarchy-Forward')
|
|
69
|
+
.map((relation: any) => relationId(relation.url))
|
|
70
|
+
.filter((id: number) => Number.isInteger(id));
|
|
71
|
+
if (!childIds.length) return;
|
|
72
|
+
const children = await c.post<any>('/wit/workitemsbatch', { ids: childIds, $expand: 'all' }, { 'api-version': '7.1' });
|
|
73
|
+
const left = (children.value ?? []).find((child: any) => !['New', 'In Planning', 'Awaiting Approval'].includes(child.fields?.['System.State']));
|
|
74
|
+
if (left) {
|
|
75
|
+
throw new Error(`refusing to delete '${fileName}': an item of Feature ${feature.id} has left Awaiting Approval (item ${left.id} is at ${left.fields?.['System.State']}) — the approved plan stays on the board`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function workitemAttachments(args: WorkitemAttachmentsArgs): Promise<string> {
|
|
80
|
+
const c = await getClient();
|
|
81
|
+
if (args.action === 'upload') {
|
|
82
|
+
if (!args.fileName) throw new Error('fileName is required for upload');
|
|
83
|
+
if (args.content === undefined || args.content === '') throw new Error('content is required for upload');
|
|
84
|
+
await uploadAttachment(c, args.id, args.fileName, args.content);
|
|
85
|
+
return JSON.stringify({ id: args.id, attached: args.fileName }, null, 2);
|
|
86
|
+
}
|
|
87
|
+
const item = await c.get<any>(`/wit/workitems/${args.id}`, { 'api-version': '7.1', $expand: 'all' });
|
|
88
|
+
const files = attachedFiles(item);
|
|
89
|
+
if (args.action === 'read') {
|
|
90
|
+
return JSON.stringify({ id: args.id, attachments: files.map(({ name, url }) => ({ name, url })) }, null, 2);
|
|
91
|
+
}
|
|
92
|
+
if (!args.fileName) throw new Error('fileName is required for delete');
|
|
93
|
+
const target = files.find((file) => file.name === args.fileName);
|
|
94
|
+
if (!target) throw new Error(`work item ${args.id} has no AttachedFile named '${args.fileName}'`);
|
|
95
|
+
if (target.name.startsWith('plan-r')) {
|
|
96
|
+
await assertPlanAttachmentDeletable(c, item, target.name);
|
|
97
|
+
}
|
|
98
|
+
// On 7.1 delete detaches: remove the relation, the stored file stays; test/rev gives the same optimistic concurrency as attach.
|
|
99
|
+
await c.patch<any>(
|
|
100
|
+
`/wit/workitems/${args.id}`,
|
|
101
|
+
[
|
|
102
|
+
{ op: 'test', path: '/rev', value: item.rev ?? item.fields?.['System.Rev'] },
|
|
103
|
+
{ op: 'remove', path: `/relations/${target.index}` },
|
|
104
|
+
],
|
|
105
|
+
{ 'api-version': '7.1' },
|
|
106
|
+
'application/json-patch+json',
|
|
107
|
+
);
|
|
108
|
+
return JSON.stringify({ id: args.id, deleted: target.name }, null, 2);
|
|
109
|
+
}
|
|
@@ -9,10 +9,38 @@
|
|
|
9
9
|
* automatically) falling back to the AdoClient defaults. Nothing is
|
|
10
10
|
* hardcoded here.
|
|
11
11
|
*
|
|
12
|
+
* buildRun carries the second half of H1
|
|
13
|
+
* (docs/research/adl-process-handover/board-rules.md § Hooks, row
|
|
14
|
+
* `adl.build-run-role`): the hook has no network access, so it grades only
|
|
15
|
+
* what the call itself proves and names this function as the re-verifier of
|
|
16
|
+
* the caller-declared claim. Before queuing, buildRun requires ALL of:
|
|
17
|
+
* 1. `branch` present (the hook requires it too; the tool refuses on its
|
|
18
|
+
* own, never relying on the hook alone);
|
|
19
|
+
* 2. an OPEN pull request in this repository whose source branch equals
|
|
20
|
+
* the call's `branch` (after `refs/heads/` removal);
|
|
21
|
+
* 3. that PR raised by the CALLING agent identity — `createdBy.id`
|
|
22
|
+
* compared against lib/graph-identity.ts's resolveAgentIdentityId, the
|
|
23
|
+
* ONE identity-form producer in this repo;
|
|
24
|
+
* 4. the named definition is that PR's validation pipeline: a run of
|
|
25
|
+
* definition `id` exists on the PR's merge ref (`refs/pull/<id>/merge`,
|
|
26
|
+
* the sourceBranch ADO queues PR validation builds under — the same ref
|
|
27
|
+
* build_list_or_get's poll hint names).
|
|
28
|
+
* RESIDUAL APPROXIMATION (stated, not silently widened): check 4 proves
|
|
29
|
+
* the definition IS a validation pipeline of THIS PR via a run that
|
|
30
|
+
* already exists; a definition whose policy would queue on the PR but has
|
|
31
|
+
* never produced a run is refused. That is sound for the re-queue use case
|
|
32
|
+
* (an expired check implies a run that existed) but stricter than "any
|
|
33
|
+
* policy-bound definition" — the PR resource carries no build-policy
|
|
34
|
+
* reference (`completionOptions` names no definition), so an existing run
|
|
35
|
+
* on the merge ref is the strongest relationship the REST API exposes.
|
|
36
|
+
*
|
|
12
37
|
* Timeline model: Stage → Phase → Job → Task, chained by parentId
|
|
13
38
|
* (Checkpoint records carry no user-facing state and are skipped).
|
|
14
39
|
*/
|
|
15
|
-
import { AdoClient } from './lib/client.ts'
|
|
40
|
+
import { AdoClient, resolveRepository } from './lib/client.ts'
|
|
41
|
+
import { resolveAgentIdentity } from './lib/agent-identity.ts';
|
|
42
|
+
import { resolveAgentIdentityId } from './lib/graph-identity.ts';
|
|
43
|
+
import { pullRequestsPath } from './azdo-pr.ts'
|
|
16
44
|
|
|
17
45
|
let client: AdoClient | undefined
|
|
18
46
|
|
|
@@ -21,6 +49,11 @@ async function getClient(): Promise<AdoClient> {
|
|
|
21
49
|
return client
|
|
22
50
|
}
|
|
23
51
|
|
|
52
|
+
/** Test seam: forget the cached client — see azdo-pr.ts's resetClientForTests() for why. */
|
|
53
|
+
export function resetClientForTests(): void {
|
|
54
|
+
client = undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
24
57
|
/** GET a JSON payload. */
|
|
25
58
|
async function getJson<T>(c: AdoClient, path: string, params?: Record<string, string>): Promise<T> {
|
|
26
59
|
return c.get<T>(path, { 'api-version': '7.1', ...params });
|
|
@@ -160,13 +193,77 @@ export async function buildListOrGet(args: BuildListOrGetArgs = {}): Promise<str
|
|
|
160
193
|
|
|
161
194
|
export type BuildRunArgs = { id: string; branch?: string };
|
|
162
195
|
|
|
196
|
+
function normalizeBranchRef(ref: string): string {
|
|
197
|
+
return ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* H1's azdo half — see this file's header. Queries (all read-only, all
|
|
202
|
+
* before the queue POST): the open PRs on the named source branch, the
|
|
203
|
+
* caller's own ADO identity id (Graph Subject Query → Storage Keys), and the
|
|
204
|
+
* validation runs on the PR's merge ref. Throws with the exact failed check;
|
|
205
|
+
* queues only when every check passes.
|
|
206
|
+
*/
|
|
207
|
+
async function assertOwnPrValidationBuild(c: AdoClient, definitionId: number, branch: string): Promise<void> {
|
|
208
|
+
const sourceBranch = normalizeBranchRef(branch.trim());
|
|
209
|
+
const repository = resolveRepository();
|
|
210
|
+
const { repositoryId } = await resolveRepositoryId(c);
|
|
211
|
+
const agent = resolveAgentIdentity().agent;
|
|
212
|
+
|
|
213
|
+
const prs = await getJson<any>(c, pullRequestsPath(repository), {
|
|
214
|
+
'searchCriteria.status': 'active',
|
|
215
|
+
'searchCriteria.sourceRefName': `refs/heads/${sourceBranch}`,
|
|
216
|
+
'$top': '100',
|
|
217
|
+
});
|
|
218
|
+
const candidates = (prs.value ?? []) as any[];
|
|
219
|
+
if (candidates.length === 0) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`build_run refused (H1): no open pull request in '${repository}' has source branch '${sourceBranch}' — ` +
|
|
222
|
+
'build_run may only re-queue the validation pipeline of your own open PR, on that PR\'s source branch',
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const callerId = await resolveAgentIdentityId(agent);
|
|
227
|
+
const mine = candidates.filter((pr) => pr.createdBy?.id === callerId);
|
|
228
|
+
if (mine.length === 0) {
|
|
229
|
+
const authors = [...new Set(candidates.map((pr) => pr.createdBy?.displayName ?? 'unknown'))].join(', ');
|
|
230
|
+
throw new Error(
|
|
231
|
+
`build_run refused (H1): the open pull request on '${sourceBranch}' was raised by ${authors}, not ${agent} — ` +
|
|
232
|
+
'build_run may only re-queue the validation pipeline of a PR you raised yourself',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const pr of mine) {
|
|
237
|
+
const runs = await getJson<any>(c, '/build/builds', {
|
|
238
|
+
branchName: `refs/pull/${pr.pullRequestId}/merge`,
|
|
239
|
+
repositoryId,
|
|
240
|
+
repositoryType: 'TfsGit',
|
|
241
|
+
'$top': '50',
|
|
242
|
+
});
|
|
243
|
+
if ((runs.value ?? []).some((run: any) => run.definition?.id === definitionId)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const prIds = mine.map((pr) => pr.pullRequestId).join(', ');
|
|
248
|
+
throw new Error(
|
|
249
|
+
`build_run refused (H1): build definition ${definitionId} is not a validation pipeline of your pull request(s) ${prIds} ` +
|
|
250
|
+
`(no run of it exists on refs/pull/<id>/merge) — pass the definition id of your PR's validation build, from build_list_or_get`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
163
254
|
export async function buildRun(args: BuildRunArgs): Promise<string> {
|
|
164
255
|
if (!args.id) throw new Error('id is required');
|
|
165
256
|
const definitionId = Number(args.id);
|
|
166
257
|
if (!Number.isInteger(definitionId) || definitionId < 1) throw new Error(`id must be a positive integer definition id; got '${args.id}'`);
|
|
258
|
+
if (typeof args.branch !== 'string' || args.branch.trim() === '') {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'branch is required: name the source branch of your own open PR — build_run may only re-queue that PR\'s ' +
|
|
261
|
+
'validation pipeline, on that PR\'s source branch (H1)',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
167
264
|
const c = await getClient()
|
|
168
|
-
|
|
169
|
-
|
|
265
|
+
await assertOwnPrValidationBuild(c, definitionId, args.branch);
|
|
266
|
+
const body: any = { definition: { id: definitionId }, sourceBranch: args.branch };
|
|
170
267
|
const build = await c.post<any>('/build/builds', body, { 'api-version': '7.1' });
|
|
171
268
|
return JSON.stringify({
|
|
172
269
|
id: build.id,
|