@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.37 → 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/package.json +1 -1
- package/packages/agent-hooks/src/main.ts +23 -2
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +12 -8
- 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 -26
- package/packages/agent-hooks/src/rules/impl/git-target.ts +5 -0
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +3 -1
- package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +230 -11
- 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 -11
- 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/finalize-feature/SKILL.md +3 -3
- package/packages/agent-skills/assets/implement-feature/SKILL.md +17 -20
- package/packages/agent-skills/assets/implement-feature/references/briefs.md +3 -3
- package/packages/agent-skills/assets/implement-item/SKILL.md +3 -3
- package/packages/agent-skills/assets/mechanics/SKILL.md +1 -1
- package/packages/agent-skills/assets/plan-feature/SKILL.md +10 -10
- package/packages/agent-skills/assets/prepare-promotion/SKILL.md +3 -4
- package/packages/agent-skills/assets/report-bug/SKILL.md +2 -2
- package/packages/agent-skills/assets/review-change/SKILL.md +3 -3
- package/packages/agent-skills/assets/review-plan/SKILL.md +4 -4
- package/packages/agent-skills/assets/triage-workitems/SKILL.md +4 -4
- package/packages/agent-skills/assets/validate-design/SKILL.md +6 -12
- package/packages/agent-skills/assets/verify-item/SKILL.md +5 -5
- package/packages/agent-skills/assets/wayfinder/SKILL.md +1 -1
- package/packages/agent-skills/assets/workitems-process/SKILL.md +19 -21
- package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +36 -3
- package/packages/agent-skills/assets/workitems-process/references/evidence.md +4 -4
- package/packages/agent-skills/assets/write-feature/SKILL.md +4 -5
- package/packages/agent-skills/dist/deploy.d.ts +45 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +167 -3
- 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 +241 -3
- 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/src/azdo-attachments.ts +2 -1
- package/packages/azdo/src/azdo-pr.ts +182 -6
- package/packages/azdo/src/azdo-wit.ts +290 -41
- package/packages/azdo/src/cli.ts +79 -12
- package/packages/azdo/src/lib/client.ts +88 -16
- package/packages/azdo/src/mcp-server.ts +75 -10
- package/packages/azdo/src/tool-dispatch.ts +61 -26
- package/packages/fleet/src/install-adl-package.ts +4 -3
- package/packages/repo-controls/assets/promote.ts +1 -0
- 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.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
|
@@ -35,12 +35,49 @@ export function resetClientForTests(): void {
|
|
|
35
35
|
const ALLOWED_TYPES = ['Feature', 'Product Backlog Item', 'Bug'] as const;
|
|
36
36
|
type AllowedType = (typeof ALLOWED_TYPES)[number];
|
|
37
37
|
export type WorkItemListOrGetArgs = { id?: number; ids?: number[]; type?: string; state?: string; assignedTo?: string; limit?: number; raw?: boolean };
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
/** The per-type write surface (azdo-type-split plan). One arg shape per type; the
|
|
40
|
+
* three sibling tool names — `workitem_create_or_update_feature` /
|
|
41
|
+
* `_pbi` / `_bug` — are dispatched to `workitemCreateOrUpdateFeature` /
|
|
42
|
+
* `Pbi` / `Bug` (see tool-dispatch.ts). The blanket
|
|
43
|
+
* `workitem_create_or_update` is gone. */
|
|
44
|
+
export type WorkItemFeatureWriteArgs = {
|
|
45
|
+
id?: number; title?: string; description?: string; parentId?: number;
|
|
40
46
|
predecessors?: number[]; related?: number[]; fields?: Record<string, unknown>; state?: string; comment?: string;
|
|
41
47
|
assignedTo?: string; acceptanceCriteria?: string; priority?: number; startDate?: string; targetDate?: string;
|
|
42
|
-
/** T3: the Feature plan fields (board-rules.md § Plan fields)
|
|
48
|
+
/** T3: the Feature plan fields (board-rules.md § Plan fields). Required on
|
|
49
|
+
* the New → In Progress move; a Feature created at New does not carry them
|
|
50
|
+
* until it walks its ladder. */
|
|
43
51
|
planRevision?: number; planHash?: string; baselineSha?: string;
|
|
52
|
+
/** az698: drop a single predecessor or related link by id (idempotent on a missing link). Update path only. */
|
|
53
|
+
removePredecessor?: number; removeRelated?: number;
|
|
54
|
+
};
|
|
55
|
+
export type WorkItemPbiWriteArgs = {
|
|
56
|
+
id?: number; title?: string; description?: string; parentId?: number;
|
|
57
|
+
predecessors?: number[]; related?: number[]; fields?: Record<string, unknown>; state?: string; comment?: string;
|
|
58
|
+
assignedTo?: string; acceptanceCriteria?: string; priority?: number; effort?: number; startDate?: string; targetDate?: string;
|
|
59
|
+
/** az698: drop a single predecessor or related link by id. */
|
|
60
|
+
removePredecessor?: number; removeRelated?: number;
|
|
61
|
+
};
|
|
62
|
+
export type WorkItemBugWriteArgs = {
|
|
63
|
+
id?: number; title?: string; description?: string; parentId?: number;
|
|
64
|
+
predecessors?: number[]; related?: number[]; fields?: Record<string, unknown>; state?: string; comment?: string;
|
|
65
|
+
assignedTo?: string; acceptanceCriteria?: string; priority?: number; effort?: number; startDate?: string; targetDate?: string;
|
|
66
|
+
/** az698: drop a single predecessor or related link by id. */
|
|
67
|
+
removePredecessor?: number; removeRelated?: number;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** The merged shape all three per-type arg shapes satisfy — what
|
|
71
|
+
* `tool-dispatch.ts` builds once and hands to whichever type-named tool was
|
|
72
|
+
* called. The board type is never an argument: it is the tool name. */
|
|
73
|
+
export type WorkItemWriteArgs = WorkItemFeatureWriteArgs & WorkItemPbiWriteArgs & WorkItemBugWriteArgs;
|
|
74
|
+
|
|
75
|
+
/** The tool-name suffix each board type writes through — `Product Backlog Item`
|
|
76
|
+
* is the one type whose board name is not its tool name. */
|
|
77
|
+
const TOOL_SUFFIX: Record<AllowedType, 'feature' | 'pbi' | 'bug'> = {
|
|
78
|
+
Feature: 'feature',
|
|
79
|
+
'Product Backlog Item': 'pbi',
|
|
80
|
+
Bug: 'bug',
|
|
44
81
|
};
|
|
45
82
|
|
|
46
83
|
const isAllowedType = (type: string | undefined): type is AllowedType => (ALLOWED_TYPES as readonly string[]).includes(type ?? '');
|
|
@@ -110,6 +147,10 @@ interface WorkItemView {
|
|
|
110
147
|
id: number; title: string; type: string; state: string; assignedTo: string | undefined; description: string | undefined; url: string | undefined;
|
|
111
148
|
links: { parent: number | null; children: number[]; predecessors: number[]; successors: number[]; related: number[] };
|
|
112
149
|
effort: number | null; failureReason: string | null; planRevision: number | null; planHash: string | null; baselineSha: string | null;
|
|
150
|
+
/** Priority is the literal set {1, 2, 3, 4} (1=Very High, 2=High, 3=Medium, 4=Low). null when unset or not one of the four (azdo-type-split plan). */
|
|
151
|
+
priority: number | null;
|
|
152
|
+
/** Acceptance Criteria; null when unset. Required on a Feature's New → In Progress move; optional for PBI / Bug. */
|
|
153
|
+
acceptanceCriteria: string | null;
|
|
113
154
|
fields?: Record<string, unknown>; relations?: unknown[];
|
|
114
155
|
/** L2/L3 staged refusals: the write succeeded, but the board rule that will harden rides here. */
|
|
115
156
|
warnings?: string[];
|
|
@@ -129,7 +170,28 @@ function formatItem(wi: any, raw: boolean): WorkItemView {
|
|
|
129
170
|
// T3: the Feature plan fields ride the standard view (null when unset);
|
|
130
171
|
// raw mode still exposes every field verbatim.
|
|
131
172
|
const planRevision = fields['Custom.PlanRevision']; const planHash = fields['Custom.PlanHash']; const baselineSha = fields['Custom.BaselineSha'];
|
|
132
|
-
|
|
173
|
+
// Priority is the literal set {1, 2, 3, 4}; anything else reads as null (mirrors effort's lax null rule).
|
|
174
|
+
const rawPriority = fields['Microsoft.VSTS.Common.Priority'];
|
|
175
|
+
const priority = typeof rawPriority === 'number' && [1, 2, 3, 4].includes(rawPriority) ? rawPriority : null;
|
|
176
|
+
const ac = fields['Microsoft.VSTS.Common.AcceptanceCriteria'];
|
|
177
|
+
const acceptanceCriteria = typeof ac === 'string' && ac ? ac : null;
|
|
178
|
+
const out: WorkItemView = {
|
|
179
|
+
id: wi.id,
|
|
180
|
+
title: fields['System.Title'],
|
|
181
|
+
type: fields['System.WorkItemType'],
|
|
182
|
+
state: fields['System.State'],
|
|
183
|
+
assignedTo: fields['System.AssignedTo']?.displayName,
|
|
184
|
+
description: fields['System.Description'],
|
|
185
|
+
url: wi._links?.html?.href,
|
|
186
|
+
links,
|
|
187
|
+
effort: typeof effort === 'number' ? effort : null,
|
|
188
|
+
failureReason: typeof failureReason === 'string' && failureReason ? failureReason : null,
|
|
189
|
+
planRevision: typeof planRevision === 'number' ? planRevision : null,
|
|
190
|
+
planHash: typeof planHash === 'string' && planHash ? planHash : null,
|
|
191
|
+
baselineSha: typeof baselineSha === 'string' && baselineSha ? baselineSha : null,
|
|
192
|
+
priority,
|
|
193
|
+
acceptanceCriteria,
|
|
194
|
+
};
|
|
133
195
|
if (raw) { out.fields = fields; out.relations = wi.relations ?? []; }
|
|
134
196
|
return out;
|
|
135
197
|
}
|
|
@@ -165,12 +227,18 @@ export async function workitemListOrGet(args: WorkItemListOrGetArgs = {}): Promi
|
|
|
165
227
|
return JSON.stringify({ items: items.map((item) => formatItem(item, args.raw ?? false)), count: selected.length, truncated: ids.length > limit, defaultsApplied: args.state === undefined }, null, 2);
|
|
166
228
|
}
|
|
167
229
|
|
|
168
|
-
|
|
230
|
+
/** The per-type write arg shapes (azdo-type-split plan) give `buildWriteOps`
|
|
231
|
+
* a narrowed surface by construction. The `fields` escape hatch still
|
|
232
|
+
* cannot land managed fields — see the `Custom.*` check below.
|
|
233
|
+
* planRevision / planHash / baselineSha remain Feature-only at the type
|
|
234
|
+
* level (PBI/Bug args don't carry them at all); effort rides Feature +
|
|
235
|
+
* PBI + Bug. */
|
|
236
|
+
function buildWriteOps(args: { title?: string; description?: string; assignedTo?: string; acceptanceCriteria?: string; priority?: number; effort?: number; startDate?: string; targetDate?: string; state?: string; comment?: string; planRevision?: number; planHash?: string; baselineSha?: string; fields?: Record<string, unknown>; parentId?: number; predecessors?: number[]; related?: number[]; }): any[] {
|
|
169
237
|
const ops: any[] = [];
|
|
170
238
|
const add = (field: string, value: unknown) => { if (value !== undefined) ops.push({ op: 'add', path: `/fields/${field}`, value }); };
|
|
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
|
|
173
|
-
// caller
|
|
239
|
+
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.Effort', args.effort); add('Microsoft.VSTS.Scheduling.StartDate', args.startDate); add('Microsoft.VSTS.Scheduling.TargetDate', args.targetDate); add('System.State', args.state); add('System.History', args.comment);
|
|
240
|
+
// T3: the named plan-field arguments are Feature-only; the type-specific
|
|
241
|
+
// caller is responsible for never passing them on PBI/Bug.
|
|
174
242
|
add('Custom.PlanRevision', args.planRevision); add('Custom.PlanHash', args.planHash); add('Custom.BaselineSha', args.baselineSha);
|
|
175
243
|
for (const [field, value] of Object.entries(args.fields ?? {})) {
|
|
176
244
|
if (!field.startsWith('Custom.')) throw new Error(`fields may only set Custom.* fields; '${field}' is managed by this tool`);
|
|
@@ -184,14 +252,17 @@ function buildWriteOps(args: WorkItemWriteArgs): any[] {
|
|
|
184
252
|
return ops;
|
|
185
253
|
}
|
|
186
254
|
|
|
187
|
-
/**
|
|
188
|
-
|
|
189
|
-
|
|
255
|
+
/**
|
|
256
|
+
* T3: the plan fields exist on Features only — PBIs and Bugs carry none.
|
|
257
|
+
* Both call sites below already guard `type !== 'Feature'` before calling, so
|
|
258
|
+
* this refuses unconditionally; the caller owns the type test.
|
|
259
|
+
*/
|
|
260
|
+
function assertPlanFieldsAllowed(): void {
|
|
190
261
|
throw new Error('plan fields (planRevision, planHash, baselineSha) are Feature-only');
|
|
191
262
|
}
|
|
192
263
|
|
|
193
264
|
/** T3 value shape: a plan revision is a positive integer; the hashes are non-empty strings. */
|
|
194
|
-
function assertPlanFieldValues(args:
|
|
265
|
+
function assertPlanFieldValues(args: { planRevision?: number; planHash?: string; baselineSha?: string }): void {
|
|
195
266
|
if (args.planRevision !== undefined && (!Number.isInteger(args.planRevision) || (args.planRevision as number) < 1)) {
|
|
196
267
|
throw new Error('planRevision must be a positive integer');
|
|
197
268
|
}
|
|
@@ -203,9 +274,139 @@ function assertPlanFieldValues(args: WorkItemWriteArgs): void {
|
|
|
203
274
|
}
|
|
204
275
|
}
|
|
205
276
|
|
|
206
|
-
|
|
277
|
+
/** Priority is the literal set {1, 2, 3, 4} — 1=Very High, 2=High, 3=Medium, 4=Low. Not any integer. */
|
|
278
|
+
function assertPriority(priority: number): void {
|
|
279
|
+
if (!Number.isInteger(priority) || ![1, 2, 3, 4].includes(priority)) {
|
|
280
|
+
throw new Error('priority must be one of 1 (Very High), 2 (High), 3 (Medium), 4 (Low)');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Effort is days, finite, ≥ 0. Floats accepted. */
|
|
285
|
+
function assertEffort(effort: number): void {
|
|
286
|
+
if (!Number.isFinite(effort) || effort < 0) {
|
|
287
|
+
throw new Error('effort must be a non-negative number (days; floats accepted)');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Every heading in `headings` must appear in `description` as an H2 section.
|
|
292
|
+
* Headings may sit at the start of the description, on their own line, or
|
|
293
|
+
* after a blank line — the gate only checks presence, not prose quality
|
|
294
|
+
* (azdo-type-split plan § Out of Scope). */
|
|
295
|
+
function assertDescriptionHasHeadings(description: string, headings: readonly string[], shapeName: string): void {
|
|
296
|
+
for (const heading of headings) {
|
|
297
|
+
// The heading is present if any line of the description equals `## <heading>`
|
|
298
|
+
// (a heading at the start of a line) — case-sensitive, exact. Matches
|
|
299
|
+
// the heading at end of description with or without a trailing newline.
|
|
300
|
+
const atLineStart = new RegExp(`(^|\\n)\\s*##\\s+${heading.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\b`);
|
|
301
|
+
if (!atLineStart.test(description)) {
|
|
302
|
+
throw new Error(`description must carry the '${heading}' section (${shapeName})`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The board file each type's description is written from, and the H2 sections
|
|
308
|
+
* that file carries — a Feature from intent.md, a PBI and a Bug from their
|
|
309
|
+
* item file. */
|
|
310
|
+
const DESCRIPTION_SHAPE: Record<AllowedType, { file: string; headings: readonly string[] }> = {
|
|
311
|
+
Feature: { file: 'intent.md', headings: ['Why', 'Outcome', 'Constraints', 'Non-goals'] },
|
|
312
|
+
'Product Backlog Item': { file: 'item file', headings: ['What it delivers', 'Verification'] },
|
|
313
|
+
Bug: {
|
|
314
|
+
file: 'bug item file',
|
|
315
|
+
headings: ['Current behaviour', 'Expected behaviour', 'Behaviour that must not change', 'Responsible Feature', 'Verification'],
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/** What the description is missing: nothing, or the sections it must carry.
|
|
320
|
+
* Returns the wording for a refusal rather than throwing, so the caller can
|
|
321
|
+
* list every missing field in one message. */
|
|
322
|
+
function missingDescription(type: AllowedType, description: string | undefined): string | undefined {
|
|
323
|
+
if (description === undefined || description.trim() === '') return 'non-empty';
|
|
324
|
+
const { file, headings } = DESCRIPTION_SHAPE[type];
|
|
325
|
+
try {
|
|
326
|
+
assertDescriptionHasHeadings(description, headings, `${file} shape`);
|
|
327
|
+
return undefined;
|
|
328
|
+
} catch {
|
|
329
|
+
return `carrying ${headings.map((heading) => `## ${heading}`).join(', ')} (${file})`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Every create lands at New: a Feature walks its own ladder only after it
|
|
334
|
+
* exists, and a PBI/Bug starts at New (G1 approval, merge automation and
|
|
335
|
+
* human cancellation all happen later). */
|
|
336
|
+
function assertCreateState(type: AllowedType, state: string | undefined): void {
|
|
337
|
+
if (state === undefined || state === 'New') return;
|
|
338
|
+
if (type === 'Feature') {
|
|
339
|
+
throw new Error('Features are created only at New; move the Feature along its ladder after create');
|
|
340
|
+
}
|
|
341
|
+
const violations = evaluateWorkflowTransition(type, 'New', state);
|
|
342
|
+
if (violations.length) throw new Error(formatWorkflowRefusal(violations));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Per-type mandatory fields (Tarun, 2026-10-15), create path:
|
|
346
|
+
* - Feature: title, parentId (an Epic), priority, description (intent.md shape).
|
|
347
|
+
* - PBI / Bug: title, parentId (a Feature), description (item file shape), effort.
|
|
348
|
+
* Runs before any network call and lists every missing field in one message. */
|
|
349
|
+
function assertMandatoryOnCreate(type: AllowedType, args: WorkItemWriteArgs): void {
|
|
350
|
+
const missing: string[] = [];
|
|
351
|
+
if (args.title === undefined || args.title.trim() === '') missing.push('title');
|
|
352
|
+
if (args.parentId === undefined) missing.push('parentId');
|
|
353
|
+
else if (!Number.isInteger(args.parentId) || args.parentId < 1) missing.push('parentId (a positive integer)');
|
|
354
|
+
const missingShape = missingDescription(type, args.description);
|
|
355
|
+
if (missingShape !== undefined) missing.push(`description (${missingShape})`);
|
|
356
|
+
if (type === 'Feature') {
|
|
357
|
+
if (args.priority === undefined) missing.push('priority (1=Very High, 2=High, 3=Medium, 4=Low)');
|
|
358
|
+
else assertPriority(args.priority);
|
|
359
|
+
} else if (args.effort === undefined) {
|
|
360
|
+
missing.push('effort (days)');
|
|
361
|
+
} else {
|
|
362
|
+
assertEffort(args.effort);
|
|
363
|
+
}
|
|
364
|
+
if (missing.length) {
|
|
365
|
+
throw new Error(`a ${type} create requires ${missing.join(', ')}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Per-type mandatory fields on the update path (Tarun, 2026-10-15). Every
|
|
370
|
+
* update carries a comment. A Feature at New must re-send its create fields;
|
|
371
|
+
* the New → In Progress move additionally requires the acceptance criteria
|
|
372
|
+
* itemised per requirements.md and the three plan fields. A PBI or Bug carries
|
|
373
|
+
* nothing beyond the comment once it exists. */
|
|
374
|
+
function assertMandatoryOnUpdate(type: AllowedType, currentState: string, targetState: string, args: WorkItemWriteArgs): void {
|
|
375
|
+
assertUpdateComment(args.comment);
|
|
376
|
+
if (type !== 'Feature' || currentState !== 'New') return;
|
|
377
|
+
const missing: string[] = [];
|
|
378
|
+
if (args.title === undefined || args.title.trim() === '') missing.push('title');
|
|
379
|
+
if (args.parentId === undefined) missing.push('parentId');
|
|
380
|
+
if (args.priority === undefined) missing.push('priority');
|
|
381
|
+
else assertPriority(args.priority);
|
|
382
|
+
const missingShape = missingDescription(type, args.description);
|
|
383
|
+
if (missingShape !== undefined) missing.push(`description (${missingShape})`);
|
|
384
|
+
if (targetState === 'In Progress') {
|
|
385
|
+
if (args.acceptanceCriteria === undefined || args.acceptanceCriteria.trim() === '') missing.push('acceptanceCriteria');
|
|
386
|
+
if (args.planRevision === undefined) missing.push('planRevision');
|
|
387
|
+
if (args.planHash === undefined || args.planHash.trim() === '') missing.push('planHash');
|
|
388
|
+
if (args.baselineSha === undefined || args.baselineSha.trim() === '') missing.push('baselineSha');
|
|
389
|
+
}
|
|
390
|
+
if (missing.length) {
|
|
391
|
+
throw new Error(`a Feature at New requires ${missing.join(', ')} on this update`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** `comment` is mandatory on every update; on create, it's optional.
|
|
396
|
+
* Trims; throws on empty/whitespace. */
|
|
397
|
+
function assertUpdateComment(comment: string | undefined): void {
|
|
398
|
+
if (typeof comment !== 'string' || comment.trim() === '') {
|
|
399
|
+
throw new Error('comment is required on every update (mandatory evidence)');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** The one write implementation behind the three per-type tools. `type` is the
|
|
404
|
+
* tool's own name, never an argument — a Feature write can no longer land on a
|
|
405
|
+
* PBI by passing the wrong `type`. */
|
|
406
|
+
async function writeWorkItem(type: AllowedType, args: WorkItemWriteArgs): Promise<string> {
|
|
207
407
|
if (args.id === undefined) {
|
|
208
|
-
|
|
408
|
+
// Per-type mandatory fields (Tarun, 2026-10-15) run before any network.
|
|
409
|
+
assertMandatoryOnCreate(type, args);
|
|
209
410
|
assertPlanFieldValues(args);
|
|
210
411
|
// FENCE 1 (create): System.State via the raw fields escape hatch is
|
|
211
412
|
// fenced for the gated types — the dedicated `state` argument exists so
|
|
@@ -213,7 +414,7 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
213
414
|
// Feature ladder instead (F3, evaluateFeatureTransition); for them the
|
|
214
415
|
// generic managed-field refusal in buildWriteOps still applies
|
|
215
416
|
// (unchanged behavior).
|
|
216
|
-
if (
|
|
417
|
+
if (type !== 'Feature' && args.fields !== undefined && 'System.State' in args.fields) {
|
|
217
418
|
throw new Error('System.State via raw fields is fenced — pass the dedicated `state` argument so the workflow gate can evaluate it.');
|
|
218
419
|
}
|
|
219
420
|
// FENCE 2 (create): a new PBI/Bug starts at New, so a create whose
|
|
@@ -222,38 +423,31 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
222
423
|
// create. A Feature is created only at New (F3): any other requested
|
|
223
424
|
// state is refused outright, and the POST below carries no
|
|
224
425
|
// System.State op for Features at all.
|
|
225
|
-
|
|
226
|
-
if (args.state !== undefined && args.state !== 'New') {
|
|
227
|
-
throw new Error('Features are created only at New; move the Feature along its ladder after create');
|
|
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
|
-
}
|
|
426
|
+
assertCreateState(type, args.state);
|
|
233
427
|
// T3: plan fields are Feature-only (type is known at create).
|
|
234
|
-
if (
|
|
235
|
-
assertPlanFieldsAllowed(
|
|
428
|
+
if (type !== 'Feature' && (args.planRevision !== undefined || args.planHash !== undefined || args.baselineSha !== undefined)) {
|
|
429
|
+
assertPlanFieldsAllowed();
|
|
236
430
|
}
|
|
237
431
|
// F3: a Feature is created only at New, so the create POST carries no System.State op.
|
|
238
432
|
const createArgs: WorkItemWriteArgs = { ...args };
|
|
239
433
|
delete createArgs.state;
|
|
240
|
-
const ops = buildWriteOps(
|
|
434
|
+
const ops = buildWriteOps(type === 'Feature' ? createArgs : args);
|
|
241
435
|
const c = await getClient();
|
|
242
436
|
const parent = await c.get<any>(`/wit/workitems/${args.parentId}`, { 'api-version': '7.1' });
|
|
243
|
-
const expected =
|
|
244
|
-
if (parent.fields?.['System.WorkItemType'] !== expected) throw new Error(`${
|
|
437
|
+
const expected = type === 'Feature' ? 'Epic' : 'Feature';
|
|
438
|
+
if (parent.fields?.['System.WorkItemType'] !== expected) throw new Error(`${type} must be parented to a ${expected}; work item ${args.parentId} is a ${parent.fields?.['System.WorkItemType']}`);
|
|
245
439
|
// L1: a child may not be added under a closed Feature (Awaiting
|
|
246
440
|
// Release or Done), on create.
|
|
247
441
|
if (expected === 'Feature' && CLOSED_FEATURE_STATES.has(parent.fields?.['System.State'])) {
|
|
248
442
|
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
443
|
}
|
|
250
444
|
const warnings: string[] = [];
|
|
251
|
-
if (
|
|
445
|
+
if (type === 'Feature') {
|
|
252
446
|
// L2 (soft, staged refusal): a Feature needs at least one
|
|
253
447
|
// predecessor Feature; only the very first Feature in a project
|
|
254
448
|
// (no other Features besides Fixes containers) carries none.
|
|
255
449
|
if (!(args.predecessors ?? []).length && await projectHasNonFixesFeature(c)) warnings.push(L2_WARNING);
|
|
256
|
-
} else if (
|
|
450
|
+
} else if (type === 'Bug' && parent.fields?.['System.Title'] === FIXES_FEATURE_TITLE) {
|
|
257
451
|
// L4 (hard): a Bug under 'Fixes to closed Features' needs a Related
|
|
258
452
|
// link to its Responsible Feature, in any state, and its predecessor
|
|
259
453
|
// must be another Bug in the same Fixes Feature (none for the first).
|
|
@@ -263,12 +457,11 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
263
457
|
// L3 (soft, staged refusal): a PBI carries no predecessor links.
|
|
264
458
|
warnings.push(L3_WARNING);
|
|
265
459
|
}
|
|
266
|
-
const created = await c.post<any>(`/wit/workitems/$${encodeURIComponent(
|
|
460
|
+
const created = await c.post<any>(`/wit/workitems/$${encodeURIComponent(type)}`, ops, { 'api-version': '7.1' }, 'application/json-patch+json');
|
|
267
461
|
const createdView = formatItem(created, false);
|
|
268
462
|
if (warnings.length) createdView.warnings = warnings;
|
|
269
463
|
return JSON.stringify(createdView, null, 2);
|
|
270
464
|
}
|
|
271
|
-
if (args.type !== undefined) throw new Error('type cannot be changed on an existing work item');
|
|
272
465
|
// System.State arriving through the raw fields escape hatch is deferred:
|
|
273
466
|
// its refusal is type-dependent (FENCE 1 below) and the type is only known
|
|
274
467
|
// after the read. Every OTHER managed field is still refused inside
|
|
@@ -276,15 +469,23 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
276
469
|
const fields = args.fields ?? {};
|
|
277
470
|
const stateViaRawFields = 'System.State' in fields;
|
|
278
471
|
const fieldsForOps = stateViaRawFields ? Object.fromEntries(Object.entries(fields).filter(([name]) => name !== 'System.State')) : fields;
|
|
279
|
-
let ops = buildWriteOps({ ...args, fields: fieldsForOps }); if (!ops.length && !stateViaRawFields) throw new Error('nothing to write: no fields, state, comment, or relations were supplied');
|
|
472
|
+
let ops = buildWriteOps({ ...args, fields: fieldsForOps }); if (!ops.length && !stateViaRawFields && args.removePredecessor === undefined && args.removeRelated === undefined) throw new Error('nothing to write: no fields, state, comment, or relations were supplied');
|
|
280
473
|
const c = await getClient();
|
|
281
474
|
const current = await c.get<any>(`/wit/workitems/${args.id}`, { 'api-version': '7.1', $expand: 'all' });
|
|
282
475
|
assertTracked(current);
|
|
283
476
|
const currentType: AllowedType = current.fields?.['System.WorkItemType'];
|
|
477
|
+
// The tool name IS the type: writing a PBI through `..._feature` is refused
|
|
478
|
+
// by name rather than silently landing on the item.
|
|
479
|
+
if (currentType !== type) {
|
|
480
|
+
throw new Error(`work item ${args.id} is a ${currentType}; write it with the workitem_create_or_update_${TOOL_SUFFIX[currentType]} tool`);
|
|
481
|
+
}
|
|
482
|
+
const currentState = current.fields?.['System.State']
|
|
483
|
+
// Per-type mandatory fields on an update (Tarun, 2026-10-15).
|
|
484
|
+
assertMandatoryOnUpdate(type, currentState, args.state ?? currentState, args);
|
|
284
485
|
assertPlanFieldValues(args);
|
|
285
486
|
// T3: plan fields are Feature-only (the type is only known after the read).
|
|
286
487
|
if (currentType !== 'Feature' && (args.planRevision !== undefined || args.planHash !== undefined || args.baselineSha !== undefined)) {
|
|
287
|
-
assertPlanFieldsAllowed(
|
|
488
|
+
assertPlanFieldsAllowed();
|
|
288
489
|
}
|
|
289
490
|
// FENCE 1: System.State via raw fields on a gated type. Features keep the
|
|
290
491
|
// generic managed-field refusal instead; their ladder runs through the
|
|
@@ -297,7 +498,6 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
297
498
|
// that moves System.State — a move that skips gates, reaches a human-owned
|
|
298
499
|
// state (Approved/Done/Removed) or walks off the ladder is refused. Writes
|
|
299
500
|
// that leave the state untouched are not gated.
|
|
300
|
-
const currentState = current.fields?.['System.State']
|
|
301
501
|
if (currentType === 'Feature') {
|
|
302
502
|
// F5: the standing Fixes container never walks the Feature ladder.
|
|
303
503
|
if (current.fields?.['System.Title'] === FIXES_FEATURE_TITLE && (args.state ?? currentState) !== 'In Progress') {
|
|
@@ -346,13 +546,31 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
346
546
|
.map((relation: any, index: number) => relation.rel === 'System.LinkTypes.Hierarchy-Reverse' ? index : -1)
|
|
347
547
|
.filter((index: number) => index >= 0)
|
|
348
548
|
.sort((left: number, right: number) => right - left);
|
|
349
|
-
|
|
549
|
+
// buildWriteOps emitted the Hierarchy-Reverse add for every parentId, so the find always hits.
|
|
550
|
+
const parentAdd = ops.find((operation) => operation.path === '/relations/-' && operation.value?.rel === 'System.LinkTypes.Hierarchy-Reverse')!;
|
|
350
551
|
ops = [
|
|
351
552
|
...ops.filter((operation) => operation !== parentAdd),
|
|
352
553
|
...oldParentIndexes.map((index: number) => ({ op: 'remove', path: `/relations/${index}` })),
|
|
353
|
-
|
|
554
|
+
parentAdd,
|
|
354
555
|
];
|
|
355
556
|
}
|
|
557
|
+
// az698: --remove-predecessor / --remove-related drop a single relation
|
|
558
|
+
// link by id. The relation index comes from the LIVE read of `current.relations`
|
|
559
|
+
// (mirrors the parent-remove pattern above), and the op is idempotent: a
|
|
560
|
+
// remove on an id that is not present is a no-op (zero ops added), and the
|
|
561
|
+
// PATCH still lands for the rest of the write. L1–L4 re-validation below
|
|
562
|
+
// uses the post-write set (current ∪ added − removed) for the L4 check.
|
|
563
|
+
for (const [rel, removeId] of [
|
|
564
|
+
['System.LinkTypes.Dependency-Reverse', args.removePredecessor],
|
|
565
|
+
['System.LinkTypes.Related', args.removeRelated],
|
|
566
|
+
] as const) {
|
|
567
|
+
if (removeId === undefined) continue;
|
|
568
|
+
const indexes = (current.relations ?? [])
|
|
569
|
+
.map((relation: any, index: number) => relation.rel === rel && Number(/(\d+)\/?$/.exec(relation.url ?? '')?.[1]) === removeId ? index : -1)
|
|
570
|
+
.filter((index: number) => index >= 0)
|
|
571
|
+
.sort((left: number, right: number) => right - left);
|
|
572
|
+
for (const index of indexes) ops.push({ op: 'remove', path: `/relations/${index}` });
|
|
573
|
+
}
|
|
356
574
|
const warnings: string[] = [];
|
|
357
575
|
if (currentType === 'Bug') {
|
|
358
576
|
// L4 (hard, in any state): a Bug under 'Fixes to closed Features'
|
|
@@ -360,8 +578,14 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
360
578
|
// predecessor must be a sibling Bug in the same Fixes Feature.
|
|
361
579
|
const parent = updateParent ?? await fetchItemParent(c, current);
|
|
362
580
|
if (parent !== undefined && parent.fields?.['System.Title'] === FIXES_FEATURE_TITLE) {
|
|
363
|
-
|
|
364
|
-
|
|
581
|
+
// az698: the post-write Related set is current ∪ added − removed. A
|
|
582
|
+
// --remove-related that would clear the last Related link to the
|
|
583
|
+
// Responsible Feature is refused here, before any PATCH, with the
|
|
584
|
+
// same L4 wording the add path uses.
|
|
585
|
+
const relatedAfter = new Set(relationIds(current, 'System.LinkTypes.Related'));
|
|
586
|
+
for (const id of args.related ?? []) relatedAfter.add(id);
|
|
587
|
+
if (args.removeRelated !== undefined) relatedAfter.delete(args.removeRelated);
|
|
588
|
+
if (!relatedAfter.size) {
|
|
365
589
|
throw new Error(`a Bug under '${FIXES_FEATURE_TITLE}' needs a Related link to its Responsible Feature (L4)`);
|
|
366
590
|
}
|
|
367
591
|
const predecessorIds = [...new Set([...relationIds(current, 'System.LinkTypes.Dependency-Reverse'), ...(args.predecessors ?? [])])];
|
|
@@ -375,8 +599,11 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
375
599
|
} else if (args.predecessors !== undefined) {
|
|
376
600
|
// L2 (soft): a Feature's predecessor set after this write must be
|
|
377
601
|
// non-empty once any other Feature exists in the project.
|
|
378
|
-
|
|
379
|
-
|
|
602
|
+
// az698: a --remove-predecessor is part of the post-write set.
|
|
603
|
+
const predecessorAfter = new Set(relationIds(current, 'System.LinkTypes.Dependency-Reverse'));
|
|
604
|
+
for (const id of args.predecessors) predecessorAfter.add(id);
|
|
605
|
+
if (args.removePredecessor !== undefined) predecessorAfter.delete(args.removePredecessor);
|
|
606
|
+
if (!predecessorAfter.size && await projectHasNonFixesFeature(c, args.id)) warnings.push(L2_WARNING);
|
|
380
607
|
}
|
|
381
608
|
const updated = await c.patch<any>(`/wit/workitems/${args.id}`, ops, { 'api-version': '7.1' }, 'application/json-patch+json');
|
|
382
609
|
const updatedView = formatItem(updated, false);
|
|
@@ -384,6 +611,28 @@ export async function workitemCreateOrUpdate(args: WorkItemWriteArgs): Promise<s
|
|
|
384
611
|
return JSON.stringify(updatedView, null, 2);
|
|
385
612
|
}
|
|
386
613
|
|
|
614
|
+
/** Create or update a Feature under its Epic (Tarun's per-type write surface).
|
|
615
|
+
* Mandatory: title, parentId, priority, description (intent.md shape) on
|
|
616
|
+
* create and while the Feature is at New; acceptanceCriteria plus the three
|
|
617
|
+
* plan fields on the New → In Progress move; a comment on every update. */
|
|
618
|
+
export function workitemCreateOrUpdateFeature(args: WorkItemFeatureWriteArgs): Promise<string> {
|
|
619
|
+
return writeWorkItem('Feature', args);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Create or update a PBI under its Feature. Mandatory on create: title,
|
|
623
|
+
* parentId, description (item/*.md shape), effort in days. A comment is
|
|
624
|
+
* mandatory on every update. */
|
|
625
|
+
export function workitemCreateOrUpdatePbi(args: WorkItemPbiWriteArgs): Promise<string> {
|
|
626
|
+
return writeWorkItem('Product Backlog Item', args);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Create or update a Bug under its Feature (usually a Fixes Feature).
|
|
630
|
+
* Mandatory on create: title, parentId, description (item/*.md shape), effort
|
|
631
|
+
* in days. A comment is mandatory on every update. */
|
|
632
|
+
export function workitemCreateOrUpdateBug(args: WorkItemBugWriteArgs): Promise<string> {
|
|
633
|
+
return writeWorkItem('Bug', args);
|
|
634
|
+
}
|
|
635
|
+
|
|
387
636
|
/** L3/L4: the parent of an item, fetched through its Hierarchy-Reverse relation (undefined when parentless). */
|
|
388
637
|
async function fetchItemParent(c: AdoClient, item: any): Promise<any | undefined> {
|
|
389
638
|
const [parentId] = relationIds(item, 'System.LinkTypes.Hierarchy-Reverse');
|