@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.
Files changed (128) hide show
  1. package/bin/adl +2 -2
  2. package/bin/reconcile +3 -11
  3. package/bin/setup +15 -15
  4. package/package.json +1 -1
  5. package/packages/agent-hooks/deploy-profile.json +1 -1
  6. package/packages/agent-hooks/package.json +1 -1
  7. package/packages/agent-hooks/src/main.ts +23 -2
  8. package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
  9. package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
  10. package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
  11. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +22 -12
  12. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
  13. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
  14. package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +11 -5
  15. package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +18 -5
  16. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +30 -25
  17. package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
  18. package/packages/agent-hooks/src/rules/impl/git-target.ts +5 -0
  19. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +4 -2
  20. package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +230 -11
  21. package/packages/agent-hooks/src/rules/registry.ts +11 -2
  22. package/packages/agent-hooks/src/rules/shell/git-identity.ts +1 -1
  23. package/packages/agent-hooks/src/rules/shell/invocation.ts +14 -2
  24. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +10 -7
  25. package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +2 -2
  26. package/packages/agent-settings/package.json +1 -1
  27. package/packages/agent-settings/src/deploy/agents-deploy.ts +19 -1
  28. package/packages/agent-settings/src/deploy/index.ts +1 -0
  29. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +4 -6
  30. package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
  31. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
  32. package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
  33. package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
  34. package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
  35. package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
  36. package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
  37. package/packages/agent-skills/assets/implement-feature/SKILL.md +150 -0
  38. package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
  39. package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
  40. package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
  41. package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
  42. package/packages/agent-skills/assets/prepare-promotion/SKILL.md +90 -0
  43. package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
  44. package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
  45. package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
  46. package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
  47. package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
  48. package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
  49. package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
  50. package/packages/agent-skills/assets/validate-design/SKILL.md +64 -0
  51. package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
  52. package/packages/agent-skills/assets/wayfinder/SKILL.md +1 -1
  53. package/packages/agent-skills/assets/workitems-process/SKILL.md +145 -0
  54. package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +227 -0
  55. package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
  56. package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
  57. package/packages/agent-skills/assets/write-feature/SKILL.md +94 -0
  58. package/packages/agent-skills/dist/deploy.d.ts +52 -0
  59. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  60. package/packages/agent-skills/dist/deploy.js +218 -7
  61. package/packages/agent-skills/dist/deploy.js.map +1 -1
  62. package/packages/agent-skills/dist/index.d.ts +1 -1
  63. package/packages/agent-skills/dist/index.d.ts.map +1 -1
  64. package/packages/agent-skills/dist/index.js +1 -1
  65. package/packages/agent-skills/dist/index.js.map +1 -1
  66. package/packages/agent-skills/package.json +1 -1
  67. package/packages/agent-skills/src/deploy.ts +288 -6
  68. package/packages/agent-skills/src/index.ts +10 -1
  69. package/packages/azdo/README.md +13 -4
  70. package/packages/azdo/bunfig.toml +6 -0
  71. package/packages/azdo/package.json +1 -1
  72. package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
  73. package/packages/azdo/src/activity-log.ts +118 -0
  74. package/packages/azdo/src/azdo-attachments.ts +109 -0
  75. package/packages/azdo/src/azdo-pl.ts +100 -3
  76. package/packages/azdo/src/azdo-pr.ts +376 -17
  77. package/packages/azdo/src/azdo-wit.ts +594 -34
  78. package/packages/azdo/src/cli.ts +119 -12
  79. package/packages/azdo/src/lib/client.ts +95 -17
  80. package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
  81. package/packages/azdo/src/mcp-server.ts +121 -8
  82. package/packages/azdo/src/tool-dispatch.ts +107 -23
  83. package/packages/azdo/src/workflow-ladder.csv +125 -107
  84. package/packages/azdo/src/workflow-rules.ts +254 -97
  85. package/packages/fleet/src/install-adl-package.ts +4 -3
  86. package/packages/repo-controls/README.md +12 -4
  87. package/packages/repo-controls/assets/promote.ts +1 -0
  88. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
  89. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
  90. package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +5 -5
  91. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
  92. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
  93. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
  94. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
  95. 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
  96. 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
  97. 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
  98. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +1 -1
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +1 -1
  100. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +1 -1
  101. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +1 -1
  102. 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
  103. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +1 -1
  104. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +1 -1
  105. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +1 -1
  106. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +1 -1
  107. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
  108. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
  109. 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
  110. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
  111. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
  112. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
  113. 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
  114. 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
  115. 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
  116. 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
  117. 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
  118. 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
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
  124. package/packages/repo-controls/package.json +1 -1
  125. package/packages/repo-controls/src/config.ts +21 -2
  126. package/packages/repo-controls/src/doctor.ts +9 -14
  127. package/packages/repo-controls/src/findings-ratchet.ts +12 -0
  128. package/packages/repo-controls/src/hook.ts +61 -4
@@ -3,10 +3,11 @@
3
3
  // repo invoked this file as a CLI; see mcp-server.ts's runTool() history).
4
4
  import {
5
5
  validatePullRequestDescription,
6
- validatePullRequestMetadata,
7
6
  validatePullRequestTitle,
8
7
  } from './lib/validate-pr-metadata.ts'
9
8
  import { AdoClient, PR_TARGET_BRANCH, adoProjectUrl, currentBranch, resolveRepository } from './lib/client.ts';
9
+ import { uploadAttachment } from './azdo-attachments.ts';
10
+ import { evaluateWorkflowTransition, formatWorkflowRefusal } from './workflow-rules.ts';
10
11
 
11
12
  let client: AdoClient | undefined
12
13
 
@@ -21,8 +22,8 @@ export function resetClientForTests(): void {
21
22
  }
22
23
 
23
24
  /** The human-facing PR link, built from the same org/project root the API call uses. */
24
- export function pullRequestWebUrl(repository: string, pullRequestId: number | string): string {
25
- return `${adoProjectUrl()}/_git/${repository}/pullrequest/${pullRequestId}`;
25
+ export function pullRequestWebUrl(repository: string, pullRequestId: number | string, cwd?: string): string {
26
+ return `${adoProjectUrl(cwd)}/_git/${repository}/pullrequest/${pullRequestId}`;
26
27
  }
27
28
 
28
29
  /**
@@ -34,17 +35,22 @@ export function pullRequestWebUrl(repository: string, pullRequestId: number | st
34
35
  * the call succeeded, the tool reported success, and the PR stayed unarmed.
35
36
  * That silent no-op is why this is a named function with its own test rather
36
37
  * than an object literal at the call site.
38
+ *
39
+ * P2: `completionOptions.transitionWorkItems` is set by PR kind — true only
40
+ * for implementation and Fixes Bug PRs, where ADO moves the linked items to
41
+ * Done when the PR merges into integration (planning and ordinary PRs move
42
+ * nothing on merge).
37
43
  */
38
- export function autoCompleteBody(identityId: string): {
44
+ export function autoCompleteBody(identityId: string, transitionWorkItems = false): {
39
45
  autoCompleteSetBy: { id: string }
40
- completionOptions: { mergeStrategy: string; deleteSourceBranch: boolean }
46
+ completionOptions: { mergeStrategy: string; deleteSourceBranch: boolean; transitionWorkItems: boolean }
41
47
  } {
42
48
  if (!identityId) {
43
49
  throw new Error('cannot arm auto-complete: no identity to attribute it to')
44
50
  }
45
51
  return {
46
52
  autoCompleteSetBy: { id: identityId },
47
- completionOptions: { mergeStrategy: 'squash', deleteSourceBranch: true },
53
+ completionOptions: { mergeStrategy: 'squash', deleteSourceBranch: true, transitionWorkItems },
48
54
  }
49
55
  }
50
56
 
@@ -121,6 +127,20 @@ export async function prListOrGet(args: PrListOrGetArgs): Promise<string> {
121
127
  pullRequestsPath(repository, args.id),
122
128
  { 'api-version': '7.1' }
123
129
  );
130
+ // Embed the relation inline (Bug 706) so callers can verify the
131
+ // work-items set on the single-PR view without a second request. The
132
+ // PR's own response carries `_links.workItems.href` but no inline
133
+ // array; this follow-on reads the sub-resource and overlays the ids.
134
+ // The list branch below intentionally keeps the small PR shape
135
+ // (no inline array); only the per-PR fetch path embeds it.
136
+ const workItems = await c.get<{ value?: Array<{ id?: number }> }>(
137
+ pullRequestsPath(repository, args.id) + '/workItems',
138
+ { 'api-version': '7.1' }
139
+ );
140
+ pr.workItems = (workItems.value ?? [])
141
+ .map((entry) => Number(entry.id))
142
+ .filter((id) => Number.isInteger(id))
143
+ .sort((a, b) => a - b);
124
144
  return JSON.stringify(pr, null, 2);
125
145
  }
126
146
 
@@ -147,8 +167,11 @@ export async function prListOrGet(args: PrListOrGetArgs): Promise<string> {
147
167
  return JSON.stringify(prs, null, 2);
148
168
  }
149
169
 
150
- function assertValidCreateMetadata(title: string, description: string) {
151
- const metadataErrors = validatePullRequestMetadata(title, description)
170
+ function assertValidCreateMetadata(title: string, description: string, docsOnlySections: boolean) {
171
+ const metadataErrors = [
172
+ ...validatePullRequestTitle(title),
173
+ ...validatePullRequestDescription(description, { docsOnlySections }),
174
+ ]
152
175
  if (metadataErrors.length === 0) return
153
176
  throw new Error(metadataErrors.map((error) => `pr-policy: FAIL — ${error}`).join('\n'))
154
177
  }
@@ -169,6 +192,12 @@ export type PrCreateOrUpdateArgs = {
169
192
  autoComplete?: boolean;
170
193
  mergeStrategy?: string;
171
194
  deleteSourceBranch?: boolean;
195
+ /** P4: the content of the kind's mandatory Feature attachment (planning/implementation PRs only). */
196
+ attachmentContent?: string;
197
+ /** P5: positive integer work-item ids to add to the PR's work-items relation on update. */
198
+ addWorkItems?: number[];
199
+ /** P5: positive integer work-item ids to remove from the PR's work-items relation on update. */
200
+ removeWorkItems?: number[];
172
201
  /** Overrides `AZURE_DEVOPS_REPO` for this call. */
173
202
  repository?: string;
174
203
  /** Rejected removed routing input; PRs always use the checked-out branch. */
@@ -177,15 +206,272 @@ export type PrCreateOrUpdateArgs = {
177
206
  targetBranch?: never;
178
207
  };
179
208
 
209
+ /**
210
+ * P5: the public signature of the post-create work-items update.
211
+ *
212
+ * The shape per the doc the human pasted (2026-10-14): a separate
213
+ * `/workitems` sub-endpoint does not exist; the PR's own endpoint does
214
+ * not honour `workItems` as an updatable field. The real route is the
215
+ * work-item's `relations` array, mutated via JSON-Patch on the work item
216
+ * itself. Each `--add-work-items` id gets an `ArtifactLink` of rel
217
+ * `Pull Request` pointing at the PR; the PR's `workItems` sub-endpoint
218
+ * (a READ) then lists the same ids, which is the live verification.
219
+ *
220
+ * PATCH /_apis/wit/workitems/{workItemId}?api-version=7.1
221
+ * Content-Type: application/json-patch+json
222
+ * Body: [{ op: "add", path: "/relations/-", value: { rel: "ArtifactLink",
223
+ * url: "vstfs:///Git/PullRequestId/{projectId}%2F{repoId}%2F{prId}",
224
+ * attributes: { name: "Pull Request" } } }]
225
+ *
226
+ * The function validates the inputs (no id in both lists; positive integers;
227
+ * dedupe), and for each add issues a PATCH on the work item. A `remove`
228
+ * issues a JSON-Patch `remove` on the matching `ArtifactLink` (matched by
229
+ * the work item's `relations` array; the `url` field is the only stable
230
+ * identifier). Returns the merged id set the PR's workItems sub-endpoint
231
+ * reports back. The `pr_create_or_update` tool's update path calls this
232
+ * when at least one of `--add-work-items` / `--remove-work-items` is
233
+ * supplied.
234
+ */
235
+ export type UpdatePrWorkItemLinksArgs = {
236
+ add: number[];
237
+ remove: number[];
238
+ };
239
+
240
+ export async function updatePrWorkItemLinks(
241
+ c: AdoClient,
242
+ prId: number,
243
+ args: UpdatePrWorkItemLinksArgs,
244
+ ): Promise<number[]> {
245
+ const additions = dedupeIntegers(args.add);
246
+ const removals = dedupeIntegers(args.remove);
247
+ const overlap = additions.find((id) => removals.includes(id));
248
+ if (overlap !== undefined) {
249
+ throw new Error(`work-item ${overlap} is in both add and remove; an id can only appear on one side of the PATCH (P5 FENCE)`);
250
+ }
251
+ if (!Number.isInteger(prId) || prId <= 0) {
252
+ throw new Error(`prId must be a positive integer`);
253
+ }
254
+ // Resolve the project + repository GUIDs at call time from ADO. The vstfs
255
+ // URL is `vstfs:///Git/PullRequestId/{projectId}%2F{repoId}%2F{prId}` —
256
+ // both GUIDs are required and are not on the PR's own response. The
257
+ // canonical Azure DevOps REST shape returns both on
258
+ // `GET /{org}/{project}/_apis/git/repositories/{repoName}`. We cache the
259
+ // resolution so the loop below does not call it per work item.
260
+ const repository = resolveRepository();
261
+ const repoDoc = await c.get<{ id?: string; project?: { id?: string } }>(
262
+ `/git/repositories/${repository}`,
263
+ { 'api-version': '7.1' },
264
+ );
265
+ const repoId = repoDoc.id;
266
+ const projectId = repoDoc.project?.id;
267
+ if (!repoId || !projectId) {
268
+ throw new Error(`updatePrWorkItemLinks could not resolve repository GUIDs; GET .../repositories/${repository} returned id=${repoId}, project.id=${projectId}`);
269
+ }
270
+ const prLinkUrl = `vstfs:///Git/PullRequestId/${projectId}%2F${repoId}%2F${prId}`;
271
+ for (const workItemId of additions) {
272
+ const current = await c.get<{ relations?: Array<{ rel?: string; url?: string }> }>(
273
+ `/wit/workitems/${workItemId}`,
274
+ { 'api-version': '7.1', $expand: 'relations' },
275
+ );
276
+ const relations = current.relations ?? [];
277
+ const alreadyLinked = relations.some(
278
+ (rel) => rel.rel === 'ArtifactLink' && rel.url === prLinkUrl,
279
+ );
280
+ if (alreadyLinked) continue;
281
+ await c.patch<unknown>(
282
+ `/wit/workitems/${workItemId}`,
283
+ [
284
+ {
285
+ op: 'add',
286
+ path: '/relations/-',
287
+ value: {
288
+ rel: 'ArtifactLink',
289
+ url: prLinkUrl,
290
+ attributes: { name: 'Pull Request' },
291
+ },
292
+ },
293
+ ],
294
+ { 'api-version': '7.1' },
295
+ 'application/json-patch+json',
296
+ );
297
+ }
298
+ if (removals.length > 0) {
299
+ // For each work item, fetch the current relations, find the one whose
300
+ // url matches the PR's vstfs URL, and remove it via JSON-Patch.
301
+ for (const workItemId of removals) {
302
+ const current = await c.get<{ relations?: Array<{ rel?: string; url?: string }> }>(
303
+ `/wit/workitems/${workItemId}`,
304
+ { 'api-version': '7.1', $expand: 'relations' },
305
+ );
306
+ const relations = current.relations ?? [];
307
+ const targetIndex = relations.findIndex((r) => r.url === prLinkUrl);
308
+ if (targetIndex === -1) continue;
309
+ await c.patch<unknown>(
310
+ `/wit/workitems/${workItemId}`,
311
+ [{ op: 'remove', path: `/relations/${targetIndex}` }],
312
+ { 'api-version': '7.1' },
313
+ 'application/json-patch+json',
314
+ );
315
+ }
316
+ }
317
+ // Read the PR's workItems sub-endpoint as the verification surface. This
318
+ // is the only server endpoint that lists the relation; the PR's own
319
+ // response does not carry workItems. The path uses the repository NAME
320
+ // (the package convention for REST URLs — see `pullRequestsPath`), not
321
+ // the GUID that the vstfs URL above embeds.
322
+ const verify = await c.get<{ value?: Array<{ id?: number }> }>(
323
+ pullRequestsPath(repository, prId) + '/workItems',
324
+ { 'api-version': '7.1' },
325
+ );
326
+ return (verify.value ?? []).map((entry) => Number(entry.id)).filter((id) => Number.isInteger(id)).sort((a, b) => a - b);
327
+ }
328
+
329
+ function dedupeIntegers(list: readonly number[]): number[] {
330
+ const seen = new Set<number>();
331
+ const out: number[] = [];
332
+ for (const value of list) {
333
+ if (!Number.isInteger(value) || value <= 0) {
334
+ throw new Error(`work-item ids must be positive integers`);
335
+ }
336
+ if (seen.has(value)) continue;
337
+ seen.add(value);
338
+ out.push(value);
339
+ }
340
+ return out;
341
+ }
342
+
180
343
  export function assertPrSourceBranch(source: string | undefined): asserts source is string {
181
344
  if (!source) throw new Error('could not determine the current branch; run from the repository checkout');
182
345
  if (source === 'HEAD') throw new Error('this checkout is in detached HEAD state; check out a branch first');
183
- if (source === PR_TARGET_BRANCH) throw new Error(`already on ${PR_TARGET_BRANCH}; create a feature branch first`);
184
- if (source === 'main') throw new Error(`a pull request is never opened from main; open a branch into ${PR_TARGET_BRANCH}`);
346
+ const target = PR_TARGET_BRANCH;
347
+ if (source === target) throw new Error(`already on ${target}; create a feature branch first`);
348
+ if (source === 'main') throw new Error(`a pull request is never opened from main; open a branch into ${target}`);
349
+ }
350
+
351
+ /**
352
+ * P1: the PR kind comes from the source branch naming a work item
353
+ * (azdo-git-process.md § PRs). `docs/az<id>-<slug>-plan` is a planning PR;
354
+ * `fix/az<bug id>-<slug>` is a Fixes Bug implementation PR naming one Bug;
355
+ * any other `<type>/az<id>-<slug>` branch names a Feature and is an
356
+ * implementation PR. Anything else is ordinary. The kind can still fall
357
+ * back to ordinary when the board holds no item in a wireable state.
358
+ */
359
+ export type PrKind = 'planning' | 'implementation' | 'ordinary';
360
+
361
+ export interface PrKindTarget {
362
+ kind: PrKind;
363
+ /** The work item the branch names (undefined for ordinary). */
364
+ itemId?: number;
365
+ itemType?: 'Feature' | 'Bug';
366
+ }
367
+
368
+ const PLANNING_BRANCH = /^docs\/az(\d+)-[a-z0-9-]+-plan$/;
369
+ const FIXES_BUG_BRANCH = /^fix\/az(\d+)-[a-z0-9-]+$/;
370
+ const FEATURE_BRANCH = /^(?:feat|chore|docs|perf|refactor)\/az(\d+)-[a-z0-9-]+$/;
371
+
372
+ export function derivePrKind(sourceBranch: string): PrKindTarget {
373
+ const planning = PLANNING_BRANCH.exec(sourceBranch);
374
+ if (planning) return { kind: 'planning', itemId: Number(planning[1]), itemType: 'Feature' };
375
+ const fixes = FIXES_BUG_BRANCH.exec(sourceBranch);
376
+ if (fixes) return { kind: 'implementation', itemId: Number(fixes[1]), itemType: 'Bug' };
377
+ const feature = FEATURE_BRANCH.exec(sourceBranch);
378
+ if (feature) return { kind: 'implementation', itemId: Number(feature[1]), itemType: 'Feature' };
379
+ return { kind: 'ordinary' };
380
+ }
381
+
382
+ /** The board states P1 wires, per kind. Done is never wired (terminal); neither are the parked/removed states. */
383
+ const PLANNING_WIREABLE: ReadonlySet<string> = new Set(['In Planning']);
384
+ const PBI_IMPLEMENTATION_WIREABLE: ReadonlySet<string> = new Set(['Approved', 'Design Validation', 'In Implementation', 'In Testing', 'In Review', 'Updating Docs']);
385
+ const BUG_IMPLEMENTATION_WIREABLE: ReadonlySet<string> = new Set(['Approved', 'In Reproduction', 'In Fixing', 'In Testing', 'In Review', 'Updating Docs']);
386
+
387
+ /** P2's raise-time move targets: planning items → Awaiting Approval; loop items → Updating Docs. */
388
+ const PBI_LOOP: ReadonlySet<string> = new Set(['In Implementation', 'In Testing', 'In Review', 'Updating Docs']);
389
+ const BUG_LOOP: ReadonlySet<string> = new Set(['In Fixing', 'In Testing', 'In Review', 'Updating Docs']);
390
+
391
+ export interface WiredItem {
392
+ id: number;
393
+ type: 'Product Backlog Item' | 'Bug';
394
+ state: string;
395
+ }
396
+
397
+ function relationIds(item: any, rel: string): number[] {
398
+ const relationId = (url: string) => Number(/(\d+)\/?$/.exec(url)?.[1]);
399
+ return (item.relations ?? [])
400
+ .filter((relation: any) => relation.rel === rel)
401
+ .map((relation: any) => relationId(relation.url))
402
+ .filter((id: number) => Number.isInteger(id));
403
+ }
404
+
405
+ /**
406
+ * P1: the items to wire, read from the board. Planning wires the Feature's
407
+ * in-scope items at In Planning; implementation wires the Feature's items
408
+ * (or the one Bug) at Approved or later. An empty result demotes the PR to
409
+ * ordinary — no links, no state moves, no attachment.
410
+ */
411
+ export async function resolveWiredItems(c: AdoClient, target: PrKindTarget): Promise<WiredItem[]> {
412
+ if (target.itemId === undefined || target.itemType === undefined) return [];
413
+ const item = await c.get<any>(`/wit/workitems/${target.itemId}`, { 'api-version': '7.1', $expand: 'all' });
414
+ if (target.itemType === 'Bug') {
415
+ const state = item.fields?.['System.State'];
416
+ return BUG_IMPLEMENTATION_WIREABLE.has(state) ? [{ id: item.id, type: 'Bug', state }] : [];
417
+ }
418
+ const wireable = target.kind === 'planning' ? PLANNING_WIREABLE : PBI_IMPLEMENTATION_WIREABLE;
419
+ const childIds = relationIds(item, 'System.LinkTypes.Hierarchy-Forward');
420
+ if (!childIds.length) return [];
421
+ const batch = await c.post<any>('/wit/workitemsbatch', { ids: childIds, $expand: 'all' }, { 'api-version': '7.1' });
422
+ return (batch.value ?? [])
423
+ .filter((child: any) => ['Product Backlog Item', 'Bug'].includes(child.fields?.['System.WorkItemType']))
424
+ .filter((child: any) => wireable.has(child.fields?.['System.State']))
425
+ .map((child: any) => ({ id: child.id, type: child.fields['System.WorkItemType'], state: child.fields['System.State'] }));
426
+ }
427
+
428
+ /** P2's raise-time target state for one wired item, or undefined when the item stays where it is. */
429
+ export function raiseMoveTarget(kind: PrKind, item: WiredItem): string | undefined {
430
+ if (kind === 'planning') return 'Awaiting Approval';
431
+ if (kind === 'implementation') {
432
+ const loop = item.type === 'Bug' ? BUG_LOOP : PBI_LOOP;
433
+ return loop.has(item.state) && item.state !== 'Updating Docs' ? 'Updating Docs' : undefined;
434
+ }
435
+ return undefined;
436
+ }
437
+
438
+ /**
439
+ * P2: move the linked items themselves when the PR is raised. Every move is
440
+ * pre-checked against the workflow fence (the same rule an agent write
441
+ * faces) so a PR is never raised half-wired.
442
+ */
443
+ async function moveWiredItems(c: AdoClient, kind: PrKind, items: WiredItem[]): Promise<void> {
444
+ for (const item of items) {
445
+ const to = raiseMoveTarget(kind, item);
446
+ if (to === undefined) continue;
447
+ const violations = evaluateWorkflowTransition(item.type, item.state, to);
448
+ if (violations.length) throw new Error(formatWorkflowRefusal(violations));
449
+ await c.patch<any>(
450
+ `/wit/workitems/${item.id}`,
451
+ [{ op: 'add', path: '/fields/System.State', value: to }],
452
+ { 'api-version': '7.1' },
453
+ 'application/json-patch+json',
454
+ );
455
+ }
456
+ }
457
+
458
+ /**
459
+ * P4: the work item the mandatory attachment hangs off — the named Feature,
460
+ * or, for a Fixes Bug PR, its Fixes Feature (board-rules.md § Attachments).
461
+ * The file name is completed by the caller once the PR id exists.
462
+ */
463
+ async function attachmentTargetWorkItemId(c: AdoClient, target: PrKindTarget): Promise<number> {
464
+ if (target.itemType === 'Bug') {
465
+ const bug = await c.get<any>(`/wit/workitems/${target.itemId}`, { 'api-version': '7.1', $expand: 'all' });
466
+ const [parentId] = relationIds(bug, 'System.LinkTypes.Hierarchy-Reverse');
467
+ if (parentId === undefined) throw new Error(`Fixes Bug ${target.itemId} has no parent Fixes Feature to attach the implementation report to (P4)`);
468
+ return parentId;
469
+ }
470
+ return target.itemId!;
185
471
  }
186
472
 
187
473
  /** The exact next steps returned after PR creation, including live-stage failure polling. */
188
- export function prCreateResult(repository: string, pullRequestId: number, title: string, status: string, autoComplete: boolean): Record<string, unknown> {
474
+ export function prCreateResult(repository: string, pullRequestId: number, title: string, status: string, autoComplete: boolean, kind: PrKind = 'ordinary', wiredItems: number[] = []): Record<string, unknown> {
189
475
  const prRef = `refs/pull/${pullRequestId}/merge`;
190
476
  return {
191
477
  id: pullRequestId,
@@ -193,6 +479,8 @@ export function prCreateResult(repository: string, pullRequestId: number, title:
193
479
  status,
194
480
  url: pullRequestWebUrl(repository, pullRequestId),
195
481
  autoComplete,
482
+ kind,
483
+ workItems: wiredItems,
196
484
  summary: 'Creating this pull request started a validation build.',
197
485
  poll: [
198
486
  `1. build_list_or_get({ branch: '${prRef}' }) — take builds[0].id.`,
@@ -209,7 +497,9 @@ export async function prCreateOrUpdate(args: PrCreateOrUpdateArgs): Promise<stri
209
497
  const repository = resolveRepository(args.repository);
210
498
 
211
499
  if (args.id) {
212
- const { id, title, description, autoComplete, mergeStrategy, deleteSourceBranch } = args;
500
+ const { id, title, description, autoComplete, mergeStrategy, deleteSourceBranch, addWorkItems, removeWorkItems } = args;
501
+ const prId = Number(id);
502
+ const hasWorkItemChange = (addWorkItems?.length ?? 0) > 0 || (removeWorkItems?.length ?? 0) > 0;
213
503
 
214
504
  assertValidUpdateMetadata(title, description)
215
505
 
@@ -235,10 +525,31 @@ export async function prCreateOrUpdate(args: PrCreateOrUpdateArgs): Promise<stri
235
525
  Object.assign(body, autoCompleteBody(existing.createdBy?.id ?? ''))
236
526
  }
237
527
 
238
- if (Object.keys(body).length === 0) {
528
+ if (Object.keys(body).length === 0 && !hasWorkItemChange) {
239
529
  throw new Error('update requires title, description, or completion options');
240
530
  }
241
531
 
532
+ if (hasWorkItemChange) {
533
+ // P5: post-create mutability of the PR's work-items relation. Runs even
534
+ // when no metadata changes are present, so an ordinary PR whose source
535
+ // branch name does not match any of the three branch-name regexes still
536
+ // has a way to be wired. The flag path replaces workItemRefs at create
537
+ // for ordinary PRs (and complements it for implementation/planning).
538
+ const linked = await updatePrWorkItemLinks(c, prId, {
539
+ add: addWorkItems ?? [],
540
+ remove: removeWorkItems ?? [],
541
+ });
542
+ if (Object.keys(body).length === 0) {
543
+ return JSON.stringify({ pullRequestId: prId, workItems: linked, summary: 'work-items relation updated; no PR metadata changed' }, null, 2);
544
+ }
545
+ const pr = await c.patch<any>(
546
+ pullRequestsPath(repository, id),
547
+ body,
548
+ { 'api-version': '7.1' }
549
+ );
550
+ return JSON.stringify({ ...pr, workItems: linked, summary: 'PR metadata and work-items relation updated' }, null, 2);
551
+ }
552
+
242
553
  const pr = await c.patch<any>(
243
554
  pullRequestsPath(repository, id),
244
555
  body,
@@ -258,9 +569,42 @@ export async function prCreateOrUpdate(args: PrCreateOrUpdateArgs): Promise<stri
258
569
  assertPrSourceBranch(source);
259
570
  if (!title) throw new Error('title is required for create');
260
571
 
261
- assertValidCreateMetadata(title, description)
262
-
263
572
  const c = await getClient()
573
+
574
+ // P1: derive the kind from the source branch AND the board. No item in a
575
+ // wireable state demotes the PR to ordinary — nothing links, moves or
576
+ // attaches (the intent.md PR is the standing example).
577
+ const kindTarget = derivePrKind(source);
578
+ const wired = kindTarget.kind === 'ordinary' ? [] : await resolveWiredItems(c, kindTarget);
579
+ const kind: PrKind = wired.length ? kindTarget.kind : 'ordinary';
580
+
581
+ // P3: a planning PR may write "none (docs-only)" in the last two sections.
582
+ assertValidCreateMetadata(title, description, kind === 'planning')
583
+
584
+ // P4: the agent supplies the mandatory attachment's content; a planning or
585
+ // implementation PR refuses to raise without it. The planning attachment's
586
+ // file name comes from the Feature's plan fields and is validated here,
587
+ // before any PR exists.
588
+ let planningAttachment: { workItemId: number; fileName: string } | undefined;
589
+ let implAttachmentTarget: number | undefined;
590
+ if (kind !== 'ordinary') {
591
+ if (args.attachmentContent === undefined || args.attachmentContent.trim() === '') {
592
+ const article = kind === 'implementation' ? 'an' : 'a';
593
+ throw new Error(`${article} ${kind} PR requires attachmentContent — the mandatory ${kind === 'planning' ? 'plan-r<N>-<sha7>.md plan attachment' : 'impl-report-pr<id>.md implementation report'} (P4); refusing to raise the PR without it`)
594
+ }
595
+ if (kind === 'planning') {
596
+ const feature = await c.get<any>(`/wit/workitems/${kindTarget.itemId}`, { 'api-version': '7.1' });
597
+ const revision = feature.fields?.['Custom.PlanRevision'];
598
+ const hash = feature.fields?.['Custom.PlanHash'];
599
+ if (typeof revision !== 'number' || typeof hash !== 'string' || !hash) {
600
+ throw new Error('a planning PR needs the Feature\'s Custom.PlanRevision and Custom.PlanHash to name the mandatory plan attachment (P4)')
601
+ }
602
+ planningAttachment = { workItemId: kindTarget.itemId!, fileName: `plan-r${revision}-${hash.slice(0, 7)}.md` };
603
+ } else {
604
+ implAttachmentTarget = await attachmentTargetWorkItemId(c, kindTarget);
605
+ }
606
+ }
607
+
264
608
  await assertBranchReadyForPr(c, repository, source, target)
265
609
  const body: any = {
266
610
  sourceRefName: `refs/heads/${source}`,
@@ -268,6 +612,9 @@ export async function prCreateOrUpdate(args: PrCreateOrUpdateArgs): Promise<stri
268
612
  title,
269
613
  description,
270
614
  };
615
+ // P1: link the wired items at create, as `az repos pr create --work-items`
616
+ // does through workItemRefs on the create body.
617
+ if (wired.length) body.workItemRefs = wired.map((item) => ({ id: item.id }));
271
618
 
272
619
  const pr = await c.post<any>(
273
620
  pullRequestsPath(repository),
@@ -275,15 +622,27 @@ export async function prCreateOrUpdate(args: PrCreateOrUpdateArgs): Promise<stri
275
622
  { 'api-version': '7.1' }
276
623
  );
277
624
 
625
+ // P2: the tool moves the linked items itself at raise — planning items to
626
+ // Awaiting Approval, loop items to Updating Docs. Implementation PRs also
627
+ // arm transitionWorkItems so ADO lands the linked items on Done at merge.
628
+ await moveWiredItems(c, kind, wired);
629
+
630
+ // P4: attach the mandatory Feature attachment now that the PR id exists.
631
+ if (planningAttachment) {
632
+ await uploadAttachment(c, planningAttachment.workItemId, planningAttachment.fileName, args.attachmentContent!);
633
+ } else if (implAttachmentTarget !== undefined) {
634
+ await uploadAttachment(c, implAttachmentTarget, `impl-report-pr${pr.pullRequestId}.md`, args.attachmentContent!);
635
+ }
636
+
278
637
  if (arm) {
279
638
  // The PR we just created was created BY us, so its `createdBy` is the
280
639
  // authenticated identity — no extra lookup needed to attribute the arming.
281
640
  await c.patch<any>(
282
641
  pullRequestsPath(repository, pr.pullRequestId),
283
- autoCompleteBody(pr.createdBy?.id ?? ''),
642
+ autoCompleteBody(pr.createdBy?.id ?? '', kind === 'implementation'),
284
643
  { 'api-version': '7.1' }
285
644
  );
286
645
  }
287
646
 
288
- return JSON.stringify(prCreateResult(repository, pr.pullRequestId, pr.title, pr.status, arm), null, 2);
647
+ return JSON.stringify(prCreateResult(repository, pr.pullRequestId, pr.title, pr.status, arm, kind, wired.map((item) => item.id)), null, 2);
289
648
  }