@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
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Azure DevOps work item tool logic. Exported for direct, in-process use by
|
|
2
2
|
// mcp-server.ts — there is no CLI entry point here (nothing else in the
|
|
3
3
|
// repo invoked this file as a CLI).
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { AdoClient, adoProjectUrl, resolveProject, resolveRepository } from './lib/client.ts'
|
|
8
|
+
import { evaluateFeatureTransition, evaluateWorkflowTransition, formatWorkflowRefusal } from './workflow-rules.ts'
|
|
6
9
|
|
|
7
10
|
// Work-item calls use the SAME org/project resolution as every other tool in
|
|
8
11
|
// this package (`lib/client.ts`). They used to carry their own `config` object
|
|
@@ -36,16 +39,80 @@ export type WorkItemWriteArgs = {
|
|
|
36
39
|
id?: number; type?: string; title?: string; description?: string; parentId?: number;
|
|
37
40
|
predecessors?: number[]; related?: number[]; fields?: Record<string, unknown>; state?: string; comment?: string;
|
|
38
41
|
assignedTo?: string; acceptanceCriteria?: string; priority?: number; startDate?: string; targetDate?: string;
|
|
42
|
+
/** T3: the Feature plan fields (board-rules.md § Plan fields), Feature type only. */
|
|
43
|
+
planRevision?: number; planHash?: string; baselineSha?: string;
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
const isAllowedType = (type: string | undefined): type is AllowedType => (ALLOWED_TYPES as readonly string[]).includes(type ?? '');
|
|
42
47
|
function assertAllowedType(type: string | undefined): asserts type is AllowedType {
|
|
43
48
|
if (!isAllowedType(type)) throw new Error(`type must be one of ${ALLOWED_TYPES.join(', ')}; got ${type ?? 'undefined'}`);
|
|
44
49
|
}
|
|
50
|
+
/** F5: the standing per-Epic container for Bugs found in closed Features. */
|
|
51
|
+
const FIXES_FEATURE_TITLE = 'Fixes to closed Features';
|
|
52
|
+
/** L1: "closed" for a Feature — no child may be added at or past these states. */
|
|
53
|
+
const CLOSED_FEATURE_STATES: ReadonlySet<string> = new Set(['Awaiting Release', 'Done']);
|
|
54
|
+
const L2_WARNING = 'L2 (staged refusal): a Feature needs at least one predecessor Feature; none given and other Features exist in the project — this will become a refusal';
|
|
55
|
+
const L3_WARNING = 'L3 (staged refusal): a PBI/Bug carries no predecessor links; the implementation lead picks item order — this will become a refusal';
|
|
56
|
+
|
|
57
|
+
function relationIds(item: any, rel: string): number[] {
|
|
58
|
+
const relationId = (url: string) => Number(/(\d+)\/?$/.exec(url)?.[1]);
|
|
59
|
+
return (item.relations ?? [])
|
|
60
|
+
.filter((relation: any) => relation.rel === rel)
|
|
61
|
+
.map((relation: any) => relationId(relation.url))
|
|
62
|
+
.filter((id: number) => Number.isInteger(id));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** L2: true when the project holds another Feature besides Fixes containers (excluding `excludeId`). */
|
|
66
|
+
async function projectHasNonFixesFeature(c: AdoClient, excludeId?: number): Promise<boolean> {
|
|
67
|
+
const clauses = [
|
|
68
|
+
`[System.TeamProject] = '${escapeWiql(resolveProject())}'`,
|
|
69
|
+
`[System.WorkItemType] = 'Feature'`,
|
|
70
|
+
`[System.Title] <> '${escapeWiql(FIXES_FEATURE_TITLE)}'`,
|
|
71
|
+
];
|
|
72
|
+
if (excludeId !== undefined) clauses.push(`[System.Id] <> ${excludeId}`);
|
|
73
|
+
const result = await c.post<any>('/wit/wiql', { query: `SELECT [System.Id] FROM WorkItems WHERE ${clauses.join(' AND ')}` }, { 'api-version': '7.1', '$top': '1' });
|
|
74
|
+
return (result.workItems ?? []).length > 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* L4: the predecessor rule for a Bug under a 'Fixes to closed Features'
|
|
79
|
+
* Feature. `siblingBugIds` are the other Bugs under the same Fixes Feature;
|
|
80
|
+
* `predecessorIds` are the links the Bug would carry after the write.
|
|
81
|
+
* Throws on violation.
|
|
82
|
+
*/
|
|
83
|
+
function assertFixesBugPredecessors(siblingBugIds: Set<number>, predecessorIds: number[]): void {
|
|
84
|
+
if (siblingBugIds.size === 0) {
|
|
85
|
+
if (predecessorIds.length) {
|
|
86
|
+
throw new Error(`the first Bug in a 'Fixes to closed Features' Feature carries no predecessor link; remove predecessor ${predecessorIds[0]} (L4)`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!predecessorIds.length) {
|
|
91
|
+
throw new Error(`a Bug under 'Fixes to closed Features' with sibling Bugs needs a predecessor link to one of them (L4)`);
|
|
92
|
+
}
|
|
93
|
+
if (!predecessorIds.some((id) => siblingBugIds.has(id))) {
|
|
94
|
+
throw new Error(`predecessor ${predecessorIds[0]} is not a Bug under the same 'Fixes to closed Features' Feature (L4)`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** L4: the sibling Bugs under a Fixes Feature (every child of type Bug except `excludeId`). */
|
|
99
|
+
async function fixesSiblingBugIds(c: AdoClient, fixesFeature: any, excludeId?: number): Promise<Set<number>> {
|
|
100
|
+
const childIds = relationIds(fixesFeature, 'System.LinkTypes.Hierarchy-Forward');
|
|
101
|
+
if (!childIds.length) return new Set();
|
|
102
|
+
const children = await batchFetch(c, childIds);
|
|
103
|
+
return new Set(
|
|
104
|
+
children
|
|
105
|
+
.filter((child: any) => child.fields?.['System.WorkItemType'] === 'Bug' && child.id !== excludeId)
|
|
106
|
+
.map((child: any) => child.id as number),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
45
109
|
interface WorkItemView {
|
|
46
110
|
id: number; title: string; type: string; state: string; assignedTo: string | undefined; description: string | undefined; url: string | undefined;
|
|
47
111
|
links: { parent: number | null; children: number[]; predecessors: number[]; successors: number[]; related: number[] };
|
|
48
|
-
effort: number | null; failureReason: string | null;
|
|
112
|
+
effort: number | null; failureReason: string | null; planRevision: number | null; planHash: string | null; baselineSha: string | null;
|
|
113
|
+
fields?: Record<string, unknown>; relations?: unknown[];
|
|
114
|
+
/** L2/L3 staged refusals: the write succeeded, but the board rule that will harden rides here. */
|
|
115
|
+
warnings?: string[];
|
|
49
116
|
}
|
|
50
117
|
function formatItem(wi: any, raw: boolean): WorkItemView {
|
|
51
118
|
const fields = wi.fields ?? {};
|
|
@@ -59,7 +126,10 @@ function formatItem(wi: any, raw: boolean): WorkItemView {
|
|
|
59
126
|
if (relation.rel === 'System.LinkTypes.Related') links.related.push(relationId(relation.url));
|
|
60
127
|
}
|
|
61
128
|
const effort = fields['Microsoft.VSTS.Scheduling.Effort']; const failureReason = fields['Custom.FailureReason'];
|
|
62
|
-
|
|
129
|
+
// T3: the Feature plan fields ride the standard view (null when unset);
|
|
130
|
+
// raw mode still exposes every field verbatim.
|
|
131
|
+
const planRevision = fields['Custom.PlanRevision']; const planHash = fields['Custom.PlanHash']; const baselineSha = fields['Custom.BaselineSha'];
|
|
132
|
+
const out: WorkItemView = { id: wi.id, title: fields['System.Title'], type: fields['System.WorkItemType'], state: fields['System.State'], assignedTo: fields['System.AssignedTo']?.displayName, description: fields['System.Description'], url: wi._links?.html?.href, links, effort: typeof effort === 'number' ? effort : null, failureReason: typeof failureReason === 'string' && failureReason ? failureReason : null, planRevision: typeof planRevision === 'number' ? planRevision : null, planHash: typeof planHash === 'string' && planHash ? planHash : null, baselineSha: typeof baselineSha === 'string' && baselineSha ? baselineSha : null };
|
|
63
133
|
if (raw) { out.fields = fields; out.relations = wi.relations ?? []; }
|
|
64
134
|
return out;
|
|
65
135
|
}
|
|
@@ -99,6 +169,9 @@ function buildWriteOps(args: WorkItemWriteArgs): any[] {
|
|
|
99
169
|
const ops: any[] = [];
|
|
100
170
|
const add = (field: string, value: unknown) => { if (value !== undefined) ops.push({ op: 'add', path: `/fields/${field}`, value }); };
|
|
101
171
|
add('System.Title', args.title); add('System.Description', args.description); add('System.AssignedTo', args.assignedTo); add('Microsoft.VSTS.Common.AcceptanceCriteria', args.acceptanceCriteria); add('Microsoft.VSTS.Common.Priority', args.priority); add('Microsoft.VSTS.Scheduling.StartDate', args.startDate); add('Microsoft.VSTS.Scheduling.TargetDate', args.targetDate); add('System.State', args.state); add('System.History', args.comment);
|
|
172
|
+
// T3: the named plan-field arguments (validated Feature-only by the
|
|
173
|
+
// caller) write the presumed Custom.* reference names.
|
|
174
|
+
add('Custom.PlanRevision', args.planRevision); add('Custom.PlanHash', args.planHash); add('Custom.BaselineSha', args.baselineSha);
|
|
102
175
|
for (const [field, value] of Object.entries(args.fields ?? {})) {
|
|
103
176
|
if (!field.startsWith('Custom.')) throw new Error(`fields may only set Custom.* fields; '${field}' is managed by this tool`);
|
|
104
177
|
add(field, value);
|
|
@@ -111,34 +184,89 @@ function buildWriteOps(args: WorkItemWriteArgs): any[] {
|
|
|
111
184
|
return ops;
|
|
112
185
|
}
|
|
113
186
|
|
|
187
|
+
/** T3: the plan fields exist on Features only — PBIs and Bugs carry none. */
|
|
188
|
+
function assertPlanFieldsAllowed(type: string | undefined): void {
|
|
189
|
+
if (type === 'Feature') return;
|
|
190
|
+
throw new Error('plan fields (planRevision, planHash, baselineSha) are Feature-only');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** T3 value shape: a plan revision is a positive integer; the hashes are non-empty strings. */
|
|
194
|
+
function assertPlanFieldValues(args: WorkItemWriteArgs): void {
|
|
195
|
+
if (args.planRevision !== undefined && (!Number.isInteger(args.planRevision) || (args.planRevision as number) < 1)) {
|
|
196
|
+
throw new Error('planRevision must be a positive integer');
|
|
197
|
+
}
|
|
198
|
+
if (args.planHash !== undefined && typeof args.planHash === 'string' && args.planHash.trim() === '') {
|
|
199
|
+
throw new Error('planHash must be a non-empty string');
|
|
200
|
+
}
|
|
201
|
+
if (args.baselineSha !== undefined && typeof args.baselineSha === 'string' && args.baselineSha.trim() === '') {
|
|
202
|
+
throw new Error('baselineSha must be a non-empty string');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
114
206
|
export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<string> {
|
|
115
207
|
if (args.id === undefined) {
|
|
116
208
|
assertAllowedType(args.type); if (!args.title) throw new Error('title is required for create'); if (args.parentId === undefined) throw new Error(`${args.type} requires parentId`);
|
|
209
|
+
assertPlanFieldValues(args);
|
|
117
210
|
// FENCE 1 (create): System.State via the raw fields escape hatch is
|
|
118
211
|
// fenced for the gated types — the dedicated `state` argument exists so
|
|
119
|
-
// the workflow gate can evaluate the resulting state. Features
|
|
120
|
-
//
|
|
121
|
-
// still applies
|
|
212
|
+
// the workflow gate can evaluate the resulting state. Features walk the
|
|
213
|
+
// Feature ladder instead (F3, evaluateFeatureTransition); for them the
|
|
214
|
+
// generic managed-field refusal in buildWriteOps still applies
|
|
215
|
+
// (unchanged behavior).
|
|
122
216
|
if (args.type !== 'Feature' && args.fields !== undefined && 'System.State' in args.fields) {
|
|
123
217
|
throw new Error('System.State via raw fields is fenced — pass the dedicated `state` argument so the workflow gate can evaluate it.');
|
|
124
218
|
}
|
|
125
219
|
// FENCE 2 (create): a new PBI/Bug starts at New, so a create whose
|
|
126
220
|
// `state` lands anywhere past New is already a ladder refusal — G1
|
|
127
221
|
// approval, merge automation and human cancellation all happen after
|
|
128
|
-
// create.
|
|
129
|
-
|
|
222
|
+
// create. A Feature is created only at New (F3): any other requested
|
|
223
|
+
// state is refused outright, and the POST below carries no
|
|
224
|
+
// System.State op for Features at all.
|
|
225
|
+
if (args.type === 'Feature') {
|
|
130
226
|
if (args.state !== undefined && args.state !== 'New') {
|
|
131
|
-
|
|
132
|
-
if (violations.length) throw new Error(formatWorkflowRefusal(violations))
|
|
227
|
+
throw new Error('Features are created only at New; move the Feature along its ladder after create');
|
|
133
228
|
}
|
|
229
|
+
} else if (args.state !== undefined && args.state !== 'New') {
|
|
230
|
+
const violations = evaluateWorkflowTransition(args.type, 'New', args.state)
|
|
231
|
+
if (violations.length) throw new Error(formatWorkflowRefusal(violations))
|
|
232
|
+
}
|
|
233
|
+
// T3: plan fields are Feature-only (type is known at create).
|
|
234
|
+
if (args.type !== 'Feature' && (args.planRevision !== undefined || args.planHash !== undefined || args.baselineSha !== undefined)) {
|
|
235
|
+
assertPlanFieldsAllowed(args.type);
|
|
134
236
|
}
|
|
135
|
-
|
|
237
|
+
// F3: a Feature is created only at New, so the create POST carries no System.State op.
|
|
238
|
+
const createArgs: WorkItemWriteArgs = { ...args };
|
|
239
|
+
delete createArgs.state;
|
|
240
|
+
const ops = buildWriteOps(args.type === 'Feature' ? createArgs : args);
|
|
136
241
|
const c = await getClient();
|
|
137
242
|
const parent = await c.get<any>(`/wit/workitems/${args.parentId}`, { 'api-version': '7.1' });
|
|
138
243
|
const expected = args.type === 'Feature' ? 'Epic' : 'Feature';
|
|
139
244
|
if (parent.fields?.['System.WorkItemType'] !== expected) throw new Error(`${args.type} must be parented to a ${expected}; work item ${args.parentId} is a ${parent.fields?.['System.WorkItemType']}`);
|
|
245
|
+
// L1: a child may not be added under a closed Feature (Awaiting
|
|
246
|
+
// Release or Done), on create.
|
|
247
|
+
if (expected === 'Feature' && CLOSED_FEATURE_STATES.has(parent.fields?.['System.State'])) {
|
|
248
|
+
throw new Error(`work item ${args.parentId} is a Feature at ${parent.fields?.['System.State']}: no child can be added to a closed Feature (L1)`);
|
|
249
|
+
}
|
|
250
|
+
const warnings: string[] = [];
|
|
251
|
+
if (args.type === 'Feature') {
|
|
252
|
+
// L2 (soft, staged refusal): a Feature needs at least one
|
|
253
|
+
// predecessor Feature; only the very first Feature in a project
|
|
254
|
+
// (no other Features besides Fixes containers) carries none.
|
|
255
|
+
if (!(args.predecessors ?? []).length && await projectHasNonFixesFeature(c)) warnings.push(L2_WARNING);
|
|
256
|
+
} else if (args.type === 'Bug' && parent.fields?.['System.Title'] === FIXES_FEATURE_TITLE) {
|
|
257
|
+
// L4 (hard): a Bug under 'Fixes to closed Features' needs a Related
|
|
258
|
+
// link to its Responsible Feature, in any state, and its predecessor
|
|
259
|
+
// must be another Bug in the same Fixes Feature (none for the first).
|
|
260
|
+
if (!(args.related ?? []).length) throw new Error(`a Bug under '${FIXES_FEATURE_TITLE}' needs a Related link to its Responsible Feature (L4)`);
|
|
261
|
+
assertFixesBugPredecessors(await fixesSiblingBugIds(c, parent), args.predecessors ?? []);
|
|
262
|
+
} else if ((args.predecessors ?? []).length) {
|
|
263
|
+
// L3 (soft, staged refusal): a PBI carries no predecessor links.
|
|
264
|
+
warnings.push(L3_WARNING);
|
|
265
|
+
}
|
|
140
266
|
const created = await c.post<any>(`/wit/workitems/$${encodeURIComponent(args.type)}`, ops, { 'api-version': '7.1' }, 'application/json-patch+json');
|
|
141
|
-
|
|
267
|
+
const createdView = formatItem(created, false);
|
|
268
|
+
if (warnings.length) createdView.warnings = warnings;
|
|
269
|
+
return JSON.stringify(createdView, null, 2);
|
|
142
270
|
}
|
|
143
271
|
if (args.type !== undefined) throw new Error('type cannot be changed on an existing work item');
|
|
144
272
|
// System.State arriving through the raw fields escape hatch is deferred:
|
|
@@ -153,8 +281,14 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
153
281
|
const current = await c.get<any>(`/wit/workitems/${args.id}`, { 'api-version': '7.1', $expand: 'all' });
|
|
154
282
|
assertTracked(current);
|
|
155
283
|
const currentType: AllowedType = current.fields?.['System.WorkItemType'];
|
|
156
|
-
|
|
157
|
-
//
|
|
284
|
+
assertPlanFieldValues(args);
|
|
285
|
+
// T3: plan fields are Feature-only (the type is only known after the read).
|
|
286
|
+
if (currentType !== 'Feature' && (args.planRevision !== undefined || args.planHash !== undefined || args.baselineSha !== undefined)) {
|
|
287
|
+
assertPlanFieldsAllowed(currentType);
|
|
288
|
+
}
|
|
289
|
+
// FENCE 1: System.State via raw fields on a gated type. Features keep the
|
|
290
|
+
// generic managed-field refusal instead; their ladder runs through the
|
|
291
|
+
// dedicated `state` argument (F3, evaluateFeatureTransition).
|
|
158
292
|
if (stateViaRawFields) {
|
|
159
293
|
if (currentType === 'Feature') throw new Error(`fields may only set Custom.* fields; 'System.State' is managed by this tool`);
|
|
160
294
|
throw new Error('System.State via raw fields is fenced — pass the dedicated `state` argument so the workflow gate can evaluate it.');
|
|
@@ -163,11 +297,37 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
163
297
|
// that moves System.State — a move that skips gates, reaches a human-owned
|
|
164
298
|
// state (Approved/Done/Removed) or walks off the ladder is refused. Writes
|
|
165
299
|
// that leave the state untouched are not gated.
|
|
166
|
-
|
|
167
|
-
|
|
300
|
+
const currentState = current.fields?.['System.State']
|
|
301
|
+
if (currentType === 'Feature') {
|
|
302
|
+
// F5: the standing Fixes container never walks the Feature ladder.
|
|
303
|
+
if (current.fields?.['System.Title'] === FIXES_FEATURE_TITLE && (args.state ?? currentState) !== 'In Progress') {
|
|
304
|
+
throw new Error(`fenced by feature ladder: '${FIXES_FEATURE_TITLE}' stays at In Progress; agents refuse every other target state`)
|
|
305
|
+
}
|
|
306
|
+
// F3: the Feature ladder is linear, one step at a time, never backward.
|
|
307
|
+
// F4: the surviving moves carry child/promotion preconditions, checked
|
|
308
|
+
// against the live board below.
|
|
309
|
+
const targetState = args.state ?? currentState
|
|
310
|
+
if (targetState !== currentState) {
|
|
311
|
+
const violations = evaluateFeatureTransition(currentState, targetState)
|
|
312
|
+
if (violations.length) throw new Error(formatWorkflowRefusal(violations))
|
|
313
|
+
await assertFeaturePreconditions(c, current, currentState, targetState)
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
168
316
|
const violations = evaluateWorkflowTransition(currentType, currentState, args.state ?? currentState)
|
|
169
317
|
if (violations.length) throw new Error(formatWorkflowRefusal(violations))
|
|
318
|
+
// F7 (provisional): a PBI entering Design Validation must find its
|
|
319
|
+
// parent Feature's requirements.md (or spec.md, when not yet renamed)
|
|
320
|
+
// in the current repo hashing to the Feature's Custom.PlanHash. Bugs
|
|
321
|
+
// skip the check — they have no plan hash and never reach Design
|
|
322
|
+
// Validation (F9).
|
|
323
|
+
if (currentType === 'Product Backlog Item' && (args.state ?? currentState) === 'Design Validation') {
|
|
324
|
+
await assertPlanHashMatches(c, current);
|
|
325
|
+
}
|
|
170
326
|
}
|
|
327
|
+
// L1–L4 (update): the board link rules. L2/L3 are soft — staged
|
|
328
|
+
// refusals that ride the result as warnings; L1/L4 refuse here, before
|
|
329
|
+
// any PATCH.
|
|
330
|
+
let updateParent: any | undefined;
|
|
171
331
|
if (args.parentId !== undefined) {
|
|
172
332
|
// A parent is singular. Read the existing item so the write can remove all
|
|
173
333
|
// old parent links before adding the one validated replacement.
|
|
@@ -177,6 +337,11 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
177
337
|
if (parent.fields?.['System.WorkItemType'] !== expected) {
|
|
178
338
|
throw new Error(`${type} must be parented to a ${expected}; work item ${args.parentId} is a ${parent.fields?.['System.WorkItemType']}`);
|
|
179
339
|
}
|
|
340
|
+
// L1: an update may not move a child under a closed Feature either.
|
|
341
|
+
if (expected === 'Feature' && CLOSED_FEATURE_STATES.has(parent.fields?.['System.State'])) {
|
|
342
|
+
throw new Error(`work item ${args.parentId} is a Feature at ${parent.fields?.['System.State']}: no child can be added to a closed Feature (L1)`);
|
|
343
|
+
}
|
|
344
|
+
updateParent = parent;
|
|
180
345
|
const oldParentIndexes = (current.relations ?? [])
|
|
181
346
|
.map((relation: any, index: number) => relation.rel === 'System.LinkTypes.Hierarchy-Reverse' ? index : -1)
|
|
182
347
|
.filter((index: number) => index >= 0)
|
|
@@ -188,6 +353,152 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
188
353
|
...(parentAdd ? [parentAdd] : []),
|
|
189
354
|
];
|
|
190
355
|
}
|
|
356
|
+
const warnings: string[] = [];
|
|
357
|
+
if (currentType === 'Bug') {
|
|
358
|
+
// L4 (hard, in any state): a Bug under 'Fixes to closed Features'
|
|
359
|
+
// needs its Related link to the Responsible Feature, and its
|
|
360
|
+
// predecessor must be a sibling Bug in the same Fixes Feature.
|
|
361
|
+
const parent = updateParent ?? await fetchItemParent(c, current);
|
|
362
|
+
if (parent !== undefined && parent.fields?.['System.Title'] === FIXES_FEATURE_TITLE) {
|
|
363
|
+
const relatedIds = [...relationIds(current, 'System.LinkTypes.Related'), ...(args.related ?? [])];
|
|
364
|
+
if (!relatedIds.length) {
|
|
365
|
+
throw new Error(`a Bug under '${FIXES_FEATURE_TITLE}' needs a Related link to its Responsible Feature (L4)`);
|
|
366
|
+
}
|
|
367
|
+
const predecessorIds = [...new Set([...relationIds(current, 'System.LinkTypes.Dependency-Reverse'), ...(args.predecessors ?? [])])];
|
|
368
|
+
assertFixesBugPredecessors(await fixesSiblingBugIds(c, parent, args.id), predecessorIds);
|
|
369
|
+
} else if ((args.predecessors ?? []).length) {
|
|
370
|
+
// L3 (soft): an ordinary Bug carries no predecessor links.
|
|
371
|
+
warnings.push(L3_WARNING);
|
|
372
|
+
}
|
|
373
|
+
} else if (currentType === 'Product Backlog Item') {
|
|
374
|
+
if ((args.predecessors ?? []).length) warnings.push(L3_WARNING);
|
|
375
|
+
} else if (args.predecessors !== undefined) {
|
|
376
|
+
// L2 (soft): a Feature's predecessor set after this write must be
|
|
377
|
+
// non-empty once any other Feature exists in the project.
|
|
378
|
+
const predecessorIds = [...new Set([...relationIds(current, 'System.LinkTypes.Dependency-Reverse'), ...args.predecessors])];
|
|
379
|
+
if (!predecessorIds.length && await projectHasNonFixesFeature(c, args.id)) warnings.push(L2_WARNING);
|
|
380
|
+
}
|
|
191
381
|
const updated = await c.patch<any>(`/wit/workitems/${args.id}`, ops, { 'api-version': '7.1' }, 'application/json-patch+json');
|
|
192
|
-
|
|
382
|
+
const updatedView = formatItem(updated, false);
|
|
383
|
+
if (warnings.length) updatedView.warnings = warnings;
|
|
384
|
+
return JSON.stringify(updatedView, null, 2);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** L3/L4: the parent of an item, fetched through its Hierarchy-Reverse relation (undefined when parentless). */
|
|
388
|
+
async function fetchItemParent(c: AdoClient, item: any): Promise<any | undefined> {
|
|
389
|
+
const [parentId] = relationIds(item, 'System.LinkTypes.Hierarchy-Reverse');
|
|
390
|
+
if (parentId === undefined) return undefined;
|
|
391
|
+
return c.get<any>(`/wit/workitems/${parentId}`, { 'api-version': '7.1' });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** F4: the children of a Feature, fetched live — the ladder preconditions read the board, not a cache. */
|
|
395
|
+
async function featureChildStates(c: AdoClient, item: any): Promise<Array<{ id: number; state: string }>> {
|
|
396
|
+
const relationId = (url: string) => Number(/(\d+)\/?$/.exec(url)?.[1]);
|
|
397
|
+
const ids = (item.relations ?? [])
|
|
398
|
+
.filter((relation: any) => relation.rel === 'System.LinkTypes.Hierarchy-Forward')
|
|
399
|
+
.map((relation: any) => relationId(relation.url))
|
|
400
|
+
.filter((id: number) => Number.isInteger(id));
|
|
401
|
+
if (!ids.length) return [];
|
|
402
|
+
const children = await batchFetch(c, ids);
|
|
403
|
+
return children.map((child: any) => ({ id: child.id, state: child.fields?.['System.State'] }));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* F4: the Feature ladder's preconditions, checked against the children (and,
|
|
408
|
+
* for Done, the promotion PR) after the pure ladder allowed the move:
|
|
409
|
+
*
|
|
410
|
+
* - New → In Progress: at least one child has left New.
|
|
411
|
+
* - In Progress → Awaiting Release: every child is Done or Removed.
|
|
412
|
+
* - Awaiting Release → Done: the integration → main promotion PR is merged
|
|
413
|
+
* (G2 has passed for the code this Feature ships).
|
|
414
|
+
*/
|
|
415
|
+
async function assertFeaturePreconditions(c: AdoClient, item: any, fromState: string, toState: string): Promise<void> {
|
|
416
|
+
if (fromState === 'New' && toState === 'In Progress') {
|
|
417
|
+
const children = await featureChildStates(c, item);
|
|
418
|
+
if (!children.length) {
|
|
419
|
+
throw new Error('a Feature moves New → In Progress only once at least one child has left New; this Feature has no children');
|
|
420
|
+
}
|
|
421
|
+
if (!children.some((child) => child.state !== 'New')) {
|
|
422
|
+
throw new Error('a Feature moves New → In Progress only once at least one child has left New; every child is still at New');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (fromState === 'In Progress' && toState === 'Awaiting Release') {
|
|
426
|
+
const children = await featureChildStates(c, item);
|
|
427
|
+
if (!children.length) {
|
|
428
|
+
throw new Error('a Feature moves In Progress → Awaiting Release only when every child is Done or Removed; this Feature has no children');
|
|
429
|
+
}
|
|
430
|
+
const open = children.find((child) => child.state !== 'Done' && child.state !== 'Removed');
|
|
431
|
+
if (open) {
|
|
432
|
+
throw new Error(`a Feature moves In Progress → Awaiting Release only when every child is Done or Removed; child ${open.id} is at ${open.state}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (fromState === 'Awaiting Release' && toState === 'Done') {
|
|
436
|
+
const repository = resolveRepository();
|
|
437
|
+
const result = await c.get<any>(`/git/repositories/${repository}/pullRequests`, {
|
|
438
|
+
'api-version': '7.1',
|
|
439
|
+
'searchCriteria.status': 'completed',
|
|
440
|
+
'searchCriteria.sourceRefName': 'refs/heads/integration',
|
|
441
|
+
'searchCriteria.targetRefName': 'refs/heads/main',
|
|
442
|
+
});
|
|
443
|
+
if (!(result.value ?? []).length) {
|
|
444
|
+
throw new Error('Awaiting Release → Done needs the integration → main promotion PR merged; none found');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* F7 (provisional — review after the first real runs): the plan hash is
|
|
451
|
+
* what G1 approved. Before an item enters Design Validation, the Feature's
|
|
452
|
+
* requirements.md (or spec.md, when not yet renamed) in the current repo
|
|
453
|
+
* must hash (sha256) to the Feature's Custom.PlanHash. A Feature without a
|
|
454
|
+
* plan hash is not checked.
|
|
455
|
+
*/
|
|
456
|
+
async function assertPlanHashMatches(c: AdoClient, item: any): Promise<void> {
|
|
457
|
+
const parent = await fetchItemParent(c, item);
|
|
458
|
+
if (parent === undefined) return;
|
|
459
|
+
const planHash = parent.fields?.['Custom.PlanHash'];
|
|
460
|
+
if (typeof planHash !== 'string' || !planHash.trim()) return;
|
|
461
|
+
const featureId = parent.id as number;
|
|
462
|
+
const planFile = findPlanFile(process.cwd(), featureId);
|
|
463
|
+
if (planFile === undefined) {
|
|
464
|
+
throw new Error(`F7: no requirements.md or spec.md found for Feature ${featureId} under plans/features/ — cannot verify Custom.PlanHash before Design Validation`);
|
|
465
|
+
}
|
|
466
|
+
const digest = createHash('sha256').update(readFileSync(planFile)).digest('hex');
|
|
467
|
+
if (digest !== planHash.trim().toLowerCase()) {
|
|
468
|
+
throw new Error(`F7: ${planFile} hashes to ${digest}, which does not hash to the Feature's Custom.PlanHash (${planHash}) — the approved plan and the repo disagree; re-validate before Design Validation`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** The Feature's plan file: plans/features/az<id>-<slug>/requirements.md, with spec.md as the not-yet-renamed fallback. */
|
|
473
|
+
function findPlanFile(cwd: string, featureId: number): string | undefined {
|
|
474
|
+
const featuresDir = join(cwd, 'plans', 'features');
|
|
475
|
+
if (!existsSync(featuresDir)) return undefined;
|
|
476
|
+
for (const entry of readdirSync(featuresDir)) {
|
|
477
|
+
if (!entry.startsWith(`az${featureId}-`)) continue;
|
|
478
|
+
for (const name of ['requirements.md', 'spec.md']) {
|
|
479
|
+
const candidate = join(featuresDir, entry, name);
|
|
480
|
+
if (existsSync(candidate)) return candidate;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export type WorkItemCommentsReadArgs = { id: number };
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* T1: read a work item's comments (Work Item Tracking Comments – List,
|
|
490
|
+
* GET .../_apis/wit/workItems/{id}/comments?api-version=7.1). The board's
|
|
491
|
+
* evidence contract (board-rules.md § Comments and evidence) writes
|
|
492
|
+
* evidence to System.History; --raw on workitem_list_or_get shows only the
|
|
493
|
+
* latest value, so this is the read path for everything a run wrote.
|
|
494
|
+
*/
|
|
495
|
+
export async function workitemCommentsRead(args: WorkItemCommentsReadArgs): Promise<string> {
|
|
496
|
+
const c = await getClient();
|
|
497
|
+
const result = await c.get<any>(`/wit/workItems/${args.id}/comments`, { 'api-version': '7.1' });
|
|
498
|
+
const comments = (result.comments ?? []).map((comment: any) => ({
|
|
499
|
+
author: comment.createdBy?.displayName ?? null,
|
|
500
|
+
createdDate: comment.createdDate ?? null,
|
|
501
|
+
text: comment.text ?? '',
|
|
502
|
+
}));
|
|
503
|
+
return JSON.stringify({ id: args.id, count: comments.length, comments }, null, 2);
|
|
193
504
|
}
|
package/packages/azdo/src/cli.ts
CHANGED
|
@@ -45,6 +45,7 @@ const commands: readonly Command[] = [
|
|
|
45
45
|
{ flag: '--no-auto-complete', key: 'autoComplete', type: 'boolean' },
|
|
46
46
|
{ flag: '--merge-strategy', key: 'mergeStrategy', type: 'string', values: ['Squash'] },
|
|
47
47
|
{ flag: '--delete-source-branch', key: 'deleteSourceBranch', type: 'boolean' },
|
|
48
|
+
{ flag: '--attachment-content', key: 'attachmentContent', type: 'string' },
|
|
48
49
|
{ flag: '--repository', key: 'repository', type: 'string' },
|
|
49
50
|
],
|
|
50
51
|
validate: requirePullRequestWrite,
|
|
@@ -103,21 +104,60 @@ const commands: readonly Command[] = [
|
|
|
103
104
|
{ flag: '--priority', key: 'priority', type: 'integer' },
|
|
104
105
|
{ flag: '--start-date', key: 'startDate', type: 'date' },
|
|
105
106
|
{ flag: '--target-date', key: 'targetDate', type: 'date' },
|
|
107
|
+
{ flag: '--plan-revision', key: 'planRevision', type: 'integer' },
|
|
108
|
+
{ flag: '--plan-hash', key: 'planHash', type: 'string' },
|
|
109
|
+
{ flag: '--baseline-sha', key: 'baselineSha', type: 'string' },
|
|
106
110
|
],
|
|
107
111
|
validate: (args) => { if (args.id === undefined) requireKeys(args, ['type', 'title', 'parentId']); },
|
|
108
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: 'workitem-comments-read',
|
|
115
|
+
tool: 'workitem_comments_read',
|
|
116
|
+
options: [{ flag: '--id', key: 'id', type: 'integer' }],
|
|
117
|
+
validate: (args) => requireKeys(args, ['id']),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'workitem-attachments',
|
|
121
|
+
tool: 'workitem_attachments',
|
|
122
|
+
options: [
|
|
123
|
+
{ flag: '--id', key: 'id', type: 'integer' },
|
|
124
|
+
{ flag: '--action', key: 'action', type: 'string', values: ['read', 'upload', 'delete'] },
|
|
125
|
+
{ flag: '--file-name', key: 'fileName', type: 'string' },
|
|
126
|
+
{ flag: '--content', key: 'content', type: 'string' },
|
|
127
|
+
],
|
|
128
|
+
validate: (args) => {
|
|
129
|
+
requireKeys(args, ['id', 'action']);
|
|
130
|
+
if (args.action === 'upload' || args.action === 'delete') requireKeys(args, ['fileName']);
|
|
131
|
+
if (args.action === 'upload') requireKeys(args, ['content']);
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'activity-add',
|
|
136
|
+
tool: 'activity_add',
|
|
137
|
+
options: [{ flag: '--note', key: 'note', type: 'string' }],
|
|
138
|
+
validate: (args) => requireKeys(args, ['note']),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'activity-list',
|
|
142
|
+
tool: 'activity_list',
|
|
143
|
+
options: [],
|
|
144
|
+
},
|
|
109
145
|
];
|
|
110
146
|
|
|
111
147
|
const help = `Usage: adl mcp cli <command> [options]
|
|
112
148
|
|
|
113
149
|
Commands:
|
|
114
150
|
pr-list-or-get [--id ID] [--status active|completed|abandoned|all] [--repository REPOSITORY]
|
|
115
|
-
pr-create-or-update --action create|update [--pull-request-id ID] [--title TITLE] [--description DESCRIPTION] [--auto-complete|--no-auto-complete] [--merge-strategy Squash] [--delete-source-branch] [--repository REPOSITORY]
|
|
151
|
+
pr-create-or-update --action create|update [--pull-request-id ID] [--title TITLE] [--description DESCRIPTION] [--auto-complete|--no-auto-complete] [--merge-strategy Squash] [--delete-source-branch] [--attachment-content TEXT] [--repository REPOSITORY]
|
|
116
152
|
pr-review --pull-request-id ID [--comment TEXT] [--vote approve|approve-with-suggestions|reset|wait-for-author|reject] [--target-branch BRANCH] [--repository REPOSITORY]
|
|
117
153
|
build-list-or-get [--build-id ID] [--branch BRANCH] [--top N] [--tail N]
|
|
118
154
|
build-run --id ID [--branch BRANCH]
|
|
119
155
|
workitem-list-or-get [--id ID] [--ids IDS] [--type TYPE] [--state STATE] [--assigned-to IDENTITY] [--limit N] [--raw]
|
|
120
|
-
workitem-create-or-update [--id ID] [--type 'Feature'|'Product Backlog Item'|'Bug'] [--title TITLE] [--description DESCRIPTION] [--parent-id ID] [--predecessors ID[,ID...]] [--related ID[,ID...]] [--fields JSON_OBJECT] [--state STATE] [--comment TEXT] [--assigned-to IDENTITY] [--acceptance-criteria TEXT] [--priority N] [--start-date YYYY-MM-DD] [--target-date YYYY-MM-DD]
|
|
156
|
+
workitem-create-or-update [--id ID] [--type 'Feature'|'Product Backlog Item'|'Bug'] [--title TITLE] [--description DESCRIPTION] [--parent-id ID] [--predecessors ID[,ID...]] [--related ID[,ID...]] [--fields JSON_OBJECT] [--state STATE] [--comment TEXT] [--assigned-to IDENTITY] [--acceptance-criteria TEXT] [--priority N] [--start-date YYYY-MM-DD] [--target-date YYYY-MM-DD] [--plan-revision N] [--plan-hash SHA256] [--baseline-sha SHA]
|
|
157
|
+
workitem-comments-read --id ID
|
|
158
|
+
workitem-attachments --id ID --action read|upload|delete [--file-name NAME] [--content TEXT]
|
|
159
|
+
activity-add --note TEXT
|
|
160
|
+
activity-list
|
|
121
161
|
`;
|
|
122
162
|
|
|
123
163
|
function requireKeys(args: ToolArguments, keys: string[]): void {
|
|
@@ -147,7 +147,7 @@ export interface AgentIdentityResolution {
|
|
|
147
147
|
* - `env.ADL_HUMAN_AZDO === '1'`: the EXACT string `"1"`, not `"true"`,
|
|
148
148
|
* not `"yes"`, not trimmed whitespace -- Tarun exports it from his own
|
|
149
149
|
* `.zshrc`. An agent must never set it (packages/agent-settings'
|
|
150
|
-
* GLOBAL-AGENTS.md, "Agent identity"; the hook's denial of an agent
|
|
150
|
+
* TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md, "Agent identity"; the hook's denial of an agent
|
|
151
151
|
* setting it is packages/agent-hooks work, not this module's).
|
|
152
152
|
*
|
|
153
153
|
* No fallback, no trimming, no case-insensitivity: a near-miss value is
|
|
@@ -205,7 +205,13 @@ export class AdoClient {
|
|
|
205
205
|
redirect: 'error',
|
|
206
206
|
method,
|
|
207
207
|
headers,
|
|
208
|
-
|
|
208
|
+
// JSON bodies are stringified; an octet-stream upload (T2
|
|
209
|
+
// attachments) is sent exactly as supplied.
|
|
210
|
+
body: options.body === undefined
|
|
211
|
+
? undefined
|
|
212
|
+
: contentType === 'application/octet-stream'
|
|
213
|
+
? (options.body as string | Uint8Array)
|
|
214
|
+
: JSON.stringify(options.body),
|
|
209
215
|
});
|
|
210
216
|
|
|
211
217
|
if (!resp.ok) {
|
|
@@ -2,10 +2,13 @@ import { BRANCH_PREFIXES } from './commit-types.ts'
|
|
|
2
2
|
|
|
3
3
|
// A PR title never carries the pipeline-reserved `release`, so its types are the branch prefixes.
|
|
4
4
|
const TITLE_PATTERN = new RegExp(`^(${BRANCH_PREFIXES.join('|')})(\\([a-z0-9][a-z0-9-]*\\))?: .+$`)
|
|
5
|
-
|
|
5
|
+
// P3 (azdo-git-process.md § PR body): every PR carries the same five sections; a planning PR may write the exact text "none (docs-only)" in the last two.
|
|
6
|
+
const REQUIRED_SECTIONS = ['Summary', 'Why', 'Changes', 'Test plan', 'Security impact considered']
|
|
7
|
+
const DOCS_ONLY_SECTIONS = new Set(['Test plan', 'Security impact considered'])
|
|
8
|
+
export const DOCS_ONLY = 'none (docs-only)'
|
|
6
9
|
|
|
7
|
-
export function validatePullRequestMetadata(title: string, description: string): string[] {
|
|
8
|
-
return [...validatePullRequestTitle(title), ...validatePullRequestDescription(description)]
|
|
10
|
+
export function validatePullRequestMetadata(title: string, description: string, docsOnlySections = false): string[] {
|
|
11
|
+
return [...validatePullRequestTitle(title), ...validatePullRequestDescription(description, { docsOnlySections })]
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export function validatePullRequestTitle(title: string): string[] {
|
|
@@ -20,11 +23,17 @@ export function validatePullRequestTitle(title: string): string[] {
|
|
|
20
23
|
return errors
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
export function validatePullRequestDescription(description: string): string[] {
|
|
26
|
+
export function validatePullRequestDescription(description: string, options: { docsOnlySections?: boolean } = {}): string[] {
|
|
24
27
|
const errors: string[] = []
|
|
25
28
|
for (const section of REQUIRED_SECTIONS) {
|
|
26
29
|
const match = description.match(new RegExp(`(?:^|\\n)## ${section}[ \\t]*\\n([\\s\\S]*?)(?=\\n## |$)`, 'i'))
|
|
27
|
-
|
|
30
|
+
const text = match?.[1]?.trim()
|
|
31
|
+
if (!text) {
|
|
32
|
+
errors.push(`description requires a non-empty ## ${section} section`)
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
// P3: "none (docs-only)" is special only in the last two sections of a planning PR; anywhere else it is ordinary body text.
|
|
36
|
+
if (options.docsOnlySections && DOCS_ONLY_SECTIONS.has(section) && text === DOCS_ONLY) continue
|
|
28
37
|
}
|
|
29
38
|
return errors
|
|
30
39
|
}
|