ai-dev-requirements 0.2.0 → 0.3.0

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/dist/index.cjs CHANGED
@@ -188,7 +188,7 @@ function loadConfig(startDir) {
188
188
  }
189
189
  //#endregion
190
190
  //#region package.json
191
- var version = "0.2.0";
191
+ var version = "0.3.0";
192
192
  //#endregion
193
193
  //#region ../../src/utils/map-status.ts
194
194
  const ONES_STATUS_MAP = {
@@ -257,6 +257,39 @@ function workItemKindLabel(kind) {
257
257
  }
258
258
  }
259
259
  //#endregion
260
+ //#region ../../src/utils/requirement-decomposition.ts
261
+ function canonicalize(value) {
262
+ if (Array.isArray(value)) return value.map(canonicalize);
263
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, canonicalize(nested)]));
264
+ return value;
265
+ }
266
+ function stableHash(value) {
267
+ return (0, node_crypto.createHash)("sha256").update(JSON.stringify(canonicalize(value))).digest("hex");
268
+ }
269
+ function compareNullableDate$1(left, right) {
270
+ if (left === right) return 0;
271
+ if (left === null) return 1;
272
+ if (right === null) return -1;
273
+ return left.localeCompare(right);
274
+ }
275
+ function sortRequirementTasks(tasks) {
276
+ return [...tasks].sort((left, right) => compareNullableDate$1(left.planStartDate, right.planStartDate) || compareNullableDate$1(left.planEndDate, right.planEndDate) || left.displayId.localeCompare(right.displayId));
277
+ }
278
+ function buildRequirementDecompositionBaseline(requirement, tasks, metadata = {}) {
279
+ return {
280
+ requirementVersion: metadata.version ?? null,
281
+ requirementUpdatedAt: metadata.updatedAt ?? null,
282
+ requirementHash: stableHash(requirement),
283
+ relatedTasksHash: stableHash(tasks)
284
+ };
285
+ }
286
+ function buildRequirementDecompositionPlanHash(input) {
287
+ return stableHash(input);
288
+ }
289
+ function isSameRequirementBaseline(left, right) {
290
+ return left.requirementVersion === right.requirementVersion && left.requirementUpdatedAt === right.requirementUpdatedAt && left.requirementHash === right.requirementHash && left.relatedTasksHash === right.relatedTasksHash;
291
+ }
292
+ //#endregion
260
293
  //#region ../../src/adapters/base.ts
261
294
  /**
262
295
  * Abstract base class for source adapters.
@@ -297,8 +330,12 @@ const TASK_DETAIL_QUERY = `
297
330
  project { uuid name }
298
331
  parent { uuid number issueType { uuid name } }
299
332
  relatedTasks {
300
- uuid number name
333
+ key uuid number name
334
+ description
335
+ descriptionText
336
+ desc_rich: description
301
337
  issueType { uuid name }
338
+ subIssueType { uuid name detailType }
302
339
  status { uuid name category }
303
340
  assign { uuid name }
304
341
  }
@@ -344,7 +381,8 @@ const SEARCH_TASKS_QUERY = `
344
381
  status { uuid name category }
345
382
  priority { value }
346
383
  assign { uuid name }
347
- project { uuid name }
384
+ project { uuid name identifier }
385
+ parent { uuid number issueType { uuid name } }
348
386
  }
349
387
  }
350
388
  }
@@ -693,6 +731,135 @@ function htmlToPlainText(html) {
693
731
  function getTaskDetailText(task) {
694
732
  return task.descriptionText?.trim() || htmlToPlainText(task.desc_rich ?? task.description ?? "");
695
733
  }
734
+ function firstString(record, keys) {
735
+ for (const key of keys) {
736
+ const value = record[key];
737
+ if (typeof value === "string" && value.trim()) return value.trim();
738
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
739
+ }
740
+ return null;
741
+ }
742
+ function taskInfoFieldValue(record, fieldUuid) {
743
+ const direct = record[fieldUuid];
744
+ if (typeof direct === "string" && direct.trim()) return direct.trim();
745
+ const collections = [
746
+ record.field_values,
747
+ record.fieldValues,
748
+ record.fields
749
+ ];
750
+ for (const collection of collections) if (Array.isArray(collection)) for (const entry of collection) {
751
+ if (!isRecord(entry)) continue;
752
+ if (firstString(entry, [
753
+ "field_uuid",
754
+ "fieldUuid",
755
+ "uuid"
756
+ ]) !== fieldUuid) continue;
757
+ const value = firstString(entry, [
758
+ "date_value",
759
+ "dateValue",
760
+ "value",
761
+ "field_value",
762
+ "fieldValue"
763
+ ]);
764
+ if (value) return value;
765
+ }
766
+ else if (isRecord(collection)) {
767
+ const entry = collection[fieldUuid];
768
+ if (typeof entry === "string" && entry.trim()) return entry.trim();
769
+ if (isRecord(entry)) {
770
+ const value = firstString(entry, [
771
+ "date_value",
772
+ "dateValue",
773
+ "value",
774
+ "field_value",
775
+ "fieldValue"
776
+ ]);
777
+ if (value) return value;
778
+ }
779
+ }
780
+ return null;
781
+ }
782
+ function taskInfoDate(record, kind) {
783
+ let value;
784
+ if (kind === "start") value = firstString(record, [
785
+ "planStartDate",
786
+ "plan_start_date",
787
+ "plan_start"
788
+ ]) ?? taskInfoFieldValue(record, "field027");
789
+ else value = firstString(record, [
790
+ "planEndDate",
791
+ "plan_end_date",
792
+ "plan_end"
793
+ ]) ?? taskInfoFieldValue(record, "field028");
794
+ if (!value) return null;
795
+ if (isValidOnesDate(value)) return value;
796
+ const unixSeconds = Number(value);
797
+ if (!Number.isFinite(unixSeconds) || unixSeconds <= 0) return null;
798
+ return (/* @__PURE__ */ new Date(unixSeconds * 1e3)).toISOString().slice(0, 10);
799
+ }
800
+ const ONES_MANHOUR_UNITS_PER_HOUR = 1e5;
801
+ function taskInfoHours(record, keys) {
802
+ for (const key of keys) {
803
+ const value = record[key];
804
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) continue;
805
+ return value / ONES_MANHOUR_UNITS_PER_HOUR;
806
+ }
807
+ return null;
808
+ }
809
+ function inferredParentDisplayId(task, info) {
810
+ const explicit = firstString(info, ["parent_display_id", "parentDisplayId"]);
811
+ if (explicit) return explicit;
812
+ return task.name.trim().match(/^([A-Z][A-Z0-9]*-\d+)\b/i)?.[1]?.toUpperCase() ?? null;
813
+ }
814
+ function compareNullableDate(left, right) {
815
+ if (left === right) return 0;
816
+ if (left === null) return 1;
817
+ if (right === null) return -1;
818
+ return left.localeCompare(right);
819
+ }
820
+ async function mapWithConcurrency(items, concurrency, mapper) {
821
+ const results = [];
822
+ let cursor = 0;
823
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
824
+ while (cursor < items.length) {
825
+ const index = cursor;
826
+ cursor += 1;
827
+ results[index] = await mapper(items[index], index);
828
+ }
829
+ });
830
+ await Promise.all(workers);
831
+ return results;
832
+ }
833
+ function taskInfoDetail(record, fallback) {
834
+ const text = firstString(record, ["descriptionText", "description_text"]);
835
+ if (text) return text;
836
+ const rich = firstString(record, [
837
+ "desc_rich",
838
+ "description",
839
+ "desc"
840
+ ]);
841
+ return rich ? htmlToPlainText(rich) : getTaskDetailText(fallback);
842
+ }
843
+ function taskDisplayId(info, task, fallbackIdentifier) {
844
+ const explicit = firstString(info, ["displayId", "display_id"]);
845
+ if (explicit) return explicit;
846
+ return fallbackIdentifier ? `${fallbackIdentifier}-${task.number}` : `#${task.number}`;
847
+ }
848
+ function extractHtmlImageReferences(html) {
849
+ return Array.from(html.matchAll(/<img\b[^>]*>/gi), (match) => {
850
+ const tag = match[0];
851
+ const srcMatch = tag.match(/\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
852
+ const resourceMatch = tag.match(/\bdata-uuid\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
853
+ return {
854
+ tag,
855
+ src: (srcMatch?.[1] ?? srcMatch?.[2] ?? "").replace(/&amp;/gi, "&").trim(),
856
+ resourceUuid: (resourceMatch?.[1] ?? resourceMatch?.[2] ?? "").trim()
857
+ };
858
+ });
859
+ }
860
+ function containsInlineTaskImages(task) {
861
+ return [task.description, task.desc_rich].some((value) => typeof value === "string" && /<img\b/i.test(value)) || /\[(?:image|图片)\]/i.test(task.descriptionText ?? "");
862
+ }
696
863
  function isRecord(value) {
697
864
  return value !== null && typeof value === "object" && !Array.isArray(value);
698
865
  }
@@ -1131,7 +1298,11 @@ var OnesAdapter = class extends BaseAdapter {
1131
1298
  return response.json();
1132
1299
  }
1133
1300
  async fetchRelatedActivities(taskKey) {
1134
- return (await this.onesql(RELATED_ACTIVITIES_QUERY, { key: taskKey }, "Task")).data?.task?.relatedActivities ?? [];
1301
+ try {
1302
+ return (await this.onesql(RELATED_ACTIVITIES_QUERY, { key: taskKey }, "Task")).data?.task?.relatedActivities ?? [];
1303
+ } catch {
1304
+ return [];
1305
+ }
1135
1306
  }
1136
1307
  async searchTaskByNumber(taskNumber) {
1137
1308
  const session = await this.login();
@@ -1316,30 +1487,92 @@ var OnesAdapter = class extends BaseAdapter {
1316
1487
  return null;
1317
1488
  }
1318
1489
  }
1490
+ getAttachmentResourceUuid(image) {
1491
+ if (image.src) try {
1492
+ const source = new URL(image.src, this.config.apiBase);
1493
+ if (source.origin === new URL(this.config.apiBase).origin) {
1494
+ const match = source.pathname.match(/\/res\/attachment\/([^/]+)$/);
1495
+ const resourceUuid = match?.[1] ? decodeOnesPathIdentifier(match[1]) : null;
1496
+ if (resourceUuid) return resourceUuid;
1497
+ }
1498
+ } catch {}
1499
+ return image.resourceUuid;
1500
+ }
1319
1501
  /**
1320
1502
  * Replace stale image URLs in HTML with fresh signed URLs from the attachment API.
1321
- * Extracts data-uuid from <img> tags and resolves fresh URLs in parallel.
1503
+ * Prefer the resource identifier from the attachment URL because ONES data-uuid
1504
+ * can identify the editor node instead of the underlying attachment.
1322
1505
  */
1323
- async refreshImageUrls(html) {
1506
+ async refreshImageUrls(html, freshUrlCache = /* @__PURE__ */ new Map()) {
1324
1507
  if (!html) return html;
1325
- const matches = Array.from(html.matchAll(/<img\s[^>]*data-uuid="([^"]+)"[^>]*>/g));
1326
- if (matches.length === 0) return html;
1327
- const replacements = await Promise.all(matches.map(async (match) => {
1328
- const dataUuid = match[1];
1329
- const freshUrl = await this.getAttachmentUrl(dataUuid);
1508
+ const images = extractHtmlImageReferences(html).flatMap((image) => {
1509
+ const resourceUuid = this.getAttachmentResourceUuid(image);
1510
+ return resourceUuid ? [{
1511
+ image,
1512
+ resourceUuid
1513
+ }] : [];
1514
+ });
1515
+ if (images.length === 0) return html;
1516
+ const replacements = await Promise.all(images.map(async ({ image, resourceUuid }) => {
1517
+ let freshUrl = freshUrlCache.get(resourceUuid);
1518
+ if (!freshUrl) {
1519
+ freshUrl = this.getAttachmentUrl(resourceUuid);
1520
+ freshUrlCache.set(resourceUuid, freshUrl);
1521
+ }
1330
1522
  return {
1331
- fullMatch: match[0],
1332
- dataUuid,
1333
- freshUrl
1523
+ fullMatch: image.tag,
1524
+ freshUrl: await freshUrl
1334
1525
  };
1335
1526
  }));
1336
1527
  let result = html;
1337
- for (const { fullMatch, freshUrl } of replacements) if (freshUrl) {
1338
- const updatedImg = fullMatch.replace(/src="[^"]*"/, `src="${freshUrl}"`);
1528
+ for (const { fullMatch, freshUrl } of replacements) {
1529
+ if (!freshUrl) continue;
1530
+ const updatedImg = /\bsrc\s*=/i.test(fullMatch) ? fullMatch.replace(/\bsrc\s*=\s*(?:"[^"]*"|'[^']*')/i, `src="${freshUrl}"`) : fullMatch.replace(/<img\b/i, `<img src="${freshUrl}"`);
1339
1531
  result = result.replace(fullMatch, updatedImg);
1340
1532
  }
1341
1533
  return result;
1342
1534
  }
1535
+ async getFreshTaskDescriptions(task) {
1536
+ const taskInfo = await this.fetchTaskInfo(task.uuid);
1537
+ const rawDescription = typeof taskInfo.desc === "string" ? taskInfo.desc : task.description ?? "";
1538
+ const rawDescriptionRich = typeof taskInfo.desc_rich === "string" ? taskInfo.desc_rich : task.desc_rich ?? task.description ?? "";
1539
+ const freshUrlCache = /* @__PURE__ */ new Map();
1540
+ const [description, descriptionRich] = await Promise.all([this.refreshImageUrls(rawDescription, freshUrlCache), this.refreshImageUrls(rawDescriptionRich, freshUrlCache)]);
1541
+ return {
1542
+ description,
1543
+ descriptionRich
1544
+ };
1545
+ }
1546
+ async getTaskImageAttachments(task) {
1547
+ const { description, descriptionRich } = await this.getFreshTaskDescriptions(task);
1548
+ const images = [...extractHtmlImageReferences(descriptionRich), ...extractHtmlImageReferences(description)];
1549
+ const seen = /* @__PURE__ */ new Set();
1550
+ const attachments = [];
1551
+ for (const image of images) {
1552
+ if (!image.src) continue;
1553
+ let url;
1554
+ try {
1555
+ url = new URL(image.src, this.config.apiBase).toString();
1556
+ } catch {
1557
+ continue;
1558
+ }
1559
+ if (this.classifyRemoteImageUrl(url) === "untrusted") continue;
1560
+ const identity = image.resourceUuid || url;
1561
+ if (seen.has(identity)) continue;
1562
+ seen.add(identity);
1563
+ const pathname = new URL(url).pathname;
1564
+ const pathName = attachmentNameFromPath(pathname);
1565
+ const name = pathName && pathName !== "/" ? pathName : `image-${attachments.length + 1}.png`;
1566
+ attachments.push({
1567
+ id: image.resourceUuid || `${task.uuid}-image-${attachments.length + 1}`,
1568
+ name,
1569
+ url,
1570
+ mimeType: mimeTypeFromFileName(pathname),
1571
+ size: 0
1572
+ });
1573
+ }
1574
+ return attachments;
1575
+ }
1343
1576
  /**
1344
1577
  * Fetch wiki page content via REST API.
1345
1578
  * Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
@@ -1459,7 +1692,7 @@ var OnesAdapter = class extends BaseAdapter {
1459
1692
  title: `Wiki ${wikiUuid}`,
1460
1693
  uuid: wikiUuid
1461
1694
  });
1462
- const wikiContents = await Promise.all([...wikiRefs.values()].map(async (wiki) => {
1695
+ const [wikiContents, taskImageAttachments] = await Promise.all([Promise.all([...wikiRefs.values()].map(async (wiki) => {
1463
1696
  const rendered = await this.fetchWikiContent(wiki.uuid);
1464
1697
  return {
1465
1698
  title: wiki.title,
@@ -1467,7 +1700,7 @@ var OnesAdapter = class extends BaseAdapter {
1467
1700
  content: rendered.content,
1468
1701
  attachments: rendered.attachments
1469
1702
  };
1470
- }));
1703
+ })), containsInlineTaskImages(task) ? this.getTaskImageAttachments(task) : Promise.resolve([])]);
1471
1704
  const parts = [];
1472
1705
  parts.push(`# #${task.number} ${task.name}`);
1473
1706
  parts.push("");
@@ -1527,7 +1760,7 @@ var OnesAdapter = class extends BaseAdapter {
1527
1760
  parts.push(detailText);
1528
1761
  }
1529
1762
  const wikiAttachments = wikiContents.flatMap((wiki) => wiki.attachments);
1530
- const req = toRequirement(task, parts.join("\n"), wikiAttachments);
1763
+ const req = toRequirement(task, parts.join("\n"), [...wikiAttachments, ...taskImageAttachments]);
1531
1764
  req.raw = {
1532
1765
  ...req.raw,
1533
1766
  relatedActivities,
@@ -1620,13 +1853,13 @@ var OnesAdapter = class extends BaseAdapter {
1620
1853
  filterGroup: [filter],
1621
1854
  search: null,
1622
1855
  pagination: {
1623
- limit: pageSize * page,
1856
+ limit: intent === "all_tasks" ? 1e3 : pageSize * page,
1624
1857
  preciseCount: false
1625
1858
  },
1626
1859
  limit: 1e3
1627
1860
  }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? [];
1628
1861
  if (intent === "all_bugs") tasks = tasks.filter((task) => classifyOnesWorkItem(task.issueType, task.subIssueType) === "defect").filter((task) => isOpenOrInProgressBug(task)).sort((a, b) => getBugStatusPriority(a) - getBugStatusPriority(b));
1629
- if (intent === "all_tasks") tasks = tasks.filter((task) => classifyOnesWorkItem(task.issueType, task.subIssueType) === "task");
1862
+ if (intent === "all_tasks") tasks = tasks.filter((task) => classifyOnesWorkItem(task.issueType, task.subIssueType) === "task").filter((task) => task.status?.category === "to_do" || task.status?.category === "in_progress");
1630
1863
  if (assigneeUuid) tasks = tasks.filter((task) => task.assign?.uuid === assigneeUuid);
1631
1864
  if (intent === "keyword" && params.query) {
1632
1865
  const keyword = params.query.trim();
@@ -1644,6 +1877,142 @@ var OnesAdapter = class extends BaseAdapter {
1644
1877
  pageSize
1645
1878
  };
1646
1879
  }
1880
+ async listPendingWorkItems() {
1881
+ const items = await mapWithConcurrency(((await this.graphql(SEARCH_TASKS_QUERY, {
1882
+ groupBy: { tasks: {} },
1883
+ groupOrderBy: null,
1884
+ orderBy: {
1885
+ position: "ASC",
1886
+ createTime: "DESC"
1887
+ },
1888
+ filterGroup: [{
1889
+ assign_in: ["${currentUser}"],
1890
+ status_notIn: DEFAULT_STATUS_NOT_IN
1891
+ }],
1892
+ search: null,
1893
+ pagination: {
1894
+ limit: 1e3,
1895
+ preciseCount: false
1896
+ },
1897
+ limit: 1e3
1898
+ }, "group-task-data")).data?.buckets?.flatMap((bucket) => bucket.tasks ?? []) ?? []).filter((task) => task.status?.category === "to_do" || task.status?.category === "in_progress").filter((task) => {
1899
+ const kind = classifyOnesWorkItem(task.issueType, task.subIssueType);
1900
+ return kind === "requirement" || kind === "task";
1901
+ }), 6, async (task) => {
1902
+ const info = await this.fetchTaskInfo(task.uuid);
1903
+ const partial = Object.keys(info).length === 0;
1904
+ const kind = classifyOnesWorkItem(task.issueType, task.subIssueType);
1905
+ const statusCategory = task.status.category === "in_progress" ? "in_progress" : "to_do";
1906
+ const fallbackIdentifier = task.project?.identifier?.toUpperCase() ?? null;
1907
+ return {
1908
+ uuid: task.uuid,
1909
+ displayId: taskDisplayId(info, task, fallbackIdentifier),
1910
+ kind: kind === "requirement" ? "requirement" : "task",
1911
+ title: firstString(info, ["summary", "name"]) ?? task.name,
1912
+ statusName: task.status.name,
1913
+ statusCategory,
1914
+ assigneeName: task.assign?.name ?? null,
1915
+ projectName: task.project?.name ?? null,
1916
+ parentUuid: firstString(info, ["parent_uuid", "parentUuid"]) ?? task.parent?.uuid ?? null,
1917
+ parentDisplayId: kind === "task" ? inferredParentDisplayId(task, info) : null,
1918
+ actualHours: taskInfoHours(info, [
1919
+ "total_manhour",
1920
+ "totalManhour",
1921
+ "actual_manhour"
1922
+ ]),
1923
+ remainingHours: taskInfoHours(info, ["remaining_manhour", "remainingManhour"]),
1924
+ estimatedHours: taskInfoHours(info, [
1925
+ "assess_manhour",
1926
+ "assessManhour",
1927
+ "estimated_manhour"
1928
+ ]),
1929
+ planStartDate: taskInfoDate(info, "start"),
1930
+ planEndDate: taskInfoDate(info, "end"),
1931
+ partial,
1932
+ warnings: partial ? ["ONES task detail GET returned no data"] : []
1933
+ };
1934
+ });
1935
+ items.sort((left, right) => compareNullableDate(left.planStartDate, right.planStartDate) || compareNullableDate(left.planEndDate, right.planEndDate) || left.displayId.localeCompare(right.displayId));
1936
+ return {
1937
+ items,
1938
+ total: items.length,
1939
+ partialCount: items.filter((item) => item.partial).length,
1940
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1941
+ };
1942
+ }
1943
+ async getRequirementDecompositionContext(params) {
1944
+ const workItem = await this.getRequirement({ id: params.requirementId });
1945
+ if (workItem.raw.workItemKind !== "requirement") {
1946
+ const kind = typeof workItem.raw.workItemKind === "string" ? workItem.raw.workItemKind : workItem.type;
1947
+ throw new Error(`ONES: "${params.requirementId}" is ${kind}, not a requirement. Only requirements can be decomposed.`);
1948
+ }
1949
+ const raw = workItem.raw;
1950
+ if (!Number.isInteger(raw.number)) throw new TypeError("ONES: Standalone wiki pages cannot be decomposed into requirement tasks");
1951
+ const parsedDisplayId = parseDisplayId(params.requirementId);
1952
+ const requirementInfo = await this.fetchTaskInfo(workItem.id);
1953
+ const projectIdentifier = parsedDisplayId?.identifier.toUpperCase() ?? firstString(requirementInfo, ["projectIdentifier", "project_identifier"]);
1954
+ const displayId = firstString(requirementInfo, ["displayId", "display_id"]) ?? (projectIdentifier ? `${projectIdentifier}-${raw.number}` : `#${raw.number}`);
1955
+ const relatedTasks = (raw.relatedTasks ?? []).filter((task) => classifyOnesWorkItem(task.issueType, task.subIssueType) === "task");
1956
+ const relatedInfos = await Promise.all(relatedTasks.map((task) => this.fetchTaskInfo(task.uuid)));
1957
+ const tasks = sortRequirementTasks(relatedTasks.map((task, index) => {
1958
+ const info = relatedInfos[index] ?? {};
1959
+ const statusCategory = task.status?.category ?? "unknown";
1960
+ return {
1961
+ uuid: task.uuid,
1962
+ displayId: taskDisplayId(info, task, projectIdentifier),
1963
+ name: task.name,
1964
+ detail: taskInfoDetail(info, task),
1965
+ statusName: task.status?.name ?? "Unknown",
1966
+ statusCategory,
1967
+ pending: statusCategory === "to_do" || statusCategory === "in_progress",
1968
+ assigneeName: task.assign?.name ?? null,
1969
+ assigneeUuid: task.assign?.uuid ?? null,
1970
+ planStartDate: taskInfoDate(info, "start"),
1971
+ planEndDate: taskInfoDate(info, "end")
1972
+ };
1973
+ }));
1974
+ const requirement = {
1975
+ workItemKind: "requirement",
1976
+ uuid: workItem.id,
1977
+ displayId,
1978
+ name: raw.name ?? workItem.title,
1979
+ detail: typeof workItem.raw.sourceDescription === "string" ? workItem.raw.sourceDescription : workItem.description,
1980
+ issueTypeName: raw.subIssueType?.name ?? raw.issueType?.name ?? "需求",
1981
+ statusName: raw.status?.name ?? workItem.status,
1982
+ statusCategory: raw.status?.category ?? workItem.status,
1983
+ projectUuid: raw.project?.uuid ?? null,
1984
+ projectName: raw.project?.name ?? null,
1985
+ assigneeUuid: raw.assign?.uuid ?? null,
1986
+ assigneeName: raw.assign?.name ?? workItem.assignee
1987
+ };
1988
+ const baseline = buildRequirementDecompositionBaseline(requirement, tasks, {
1989
+ version: firstString(requirementInfo, [
1990
+ "version",
1991
+ "version_uuid",
1992
+ "versionUuid"
1993
+ ]),
1994
+ updatedAt: firstString(requirementInfo, [
1995
+ "updatedAt",
1996
+ "updated_at",
1997
+ "updateTime",
1998
+ "update_time"
1999
+ ])
2000
+ });
2001
+ return {
2002
+ decompositionRelation: {
2003
+ verified: false,
2004
+ uuid: null,
2005
+ name: null
2006
+ },
2007
+ requirement,
2008
+ tasks,
2009
+ pendingTasks: tasks.filter((task) => task.pending),
2010
+ baseline
2011
+ };
2012
+ }
2013
+ async createRequirementDecomposition(_params) {
2014
+ throw new Error("ONES: Requirement task creation is unavailable because the production create/relationship API contract has not been confirmed. No write request was sent.");
2015
+ }
1647
2016
  async addManhour(params) {
1648
2017
  const description = params.description.trim();
1649
2018
  if (!description) throw new Error("ONES: description is required");
@@ -1735,35 +2104,13 @@ var OnesAdapter = class extends BaseAdapter {
1735
2104
  }));
1736
2105
  }
1737
2106
  async getIssueDetail(params) {
1738
- let issueKey;
1739
- const numMatch = params.issueId.match(/^#?(\d+)$/);
1740
- if (numMatch) {
1741
- const taskNumber = Number.parseInt(numMatch[1], 10);
1742
- const found = ((await this.graphql(SEARCH_TASKS_QUERY, {
1743
- groupBy: { tasks: {} },
1744
- groupOrderBy: null,
1745
- orderBy: { createTime: "DESC" },
1746
- filterGroup: [{ number_in: [taskNumber] }],
1747
- search: null,
1748
- pagination: {
1749
- limit: 10,
1750
- preciseCount: false
1751
- },
1752
- limit: 10
1753
- }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((t) => t.number === taskNumber);
1754
- if (!found) throw new Error(`ONES: Issue #${taskNumber} not found in current team`);
1755
- issueKey = `task-${found.uuid}`;
1756
- } else issueKey = params.issueId.startsWith("task-") ? params.issueId : `task-${params.issueId}`;
2107
+ const { key: issueKey } = await this.resolveTaskRef(params.issueId);
1757
2108
  const task = (await this.graphql(ISSUE_DETAIL_QUERY, { key: issueKey }, "Task")).data?.task;
1758
2109
  if (!task) throw new Error(`ONES: Issue "${issueKey}" not found`);
1759
2110
  const kind = classifyOnesWorkItem(task.issueType, task.subIssueType);
1760
2111
  if (kind === "unknown") throw new Error(`ONES: Unable to classify "${params.issueId}" before get_issue_detail`);
1761
2112
  if (kind === "requirement" || kind === "task") throw unsupportedWorkItemToolError(params.issueId, kind, "get_issue_detail", "get_work_item");
1762
- const taskInfo = await this.fetchTaskInfo(task.uuid);
1763
- const rawDescription = taskInfo.desc ?? task.description ?? "";
1764
- const rawDescRich = taskInfo.desc_rich ?? task.desc_rich ?? "";
1765
- const freshDescription = await this.refreshImageUrls(rawDescription);
1766
- const freshDescRich = await this.refreshImageUrls(rawDescRich);
2113
+ const { description: freshDescription, descriptionRich: freshDescRich } = await this.getFreshTaskDescriptions(task);
1767
2114
  return {
1768
2115
  key: task.key,
1769
2116
  uuid: task.uuid,
@@ -2356,7 +2703,7 @@ async function downloadTrustedImages(urls, options) {
2356
2703
  //#endregion
2357
2704
  //#region ../../src/tools/get-issue-detail.ts
2358
2705
  const GetIssueDetailSchema = zod_v4.z.object({
2359
- issueId: zod_v4.z.string().describe("The issue task ID or key (e.g. \"mock-issue-uuid\" or \"task-mock-issue-uuid\")"),
2706
+ issueId: zod_v4.z.string().describe("ONES defect UUID, task key, number, or display ID (for example \"DEMO-2001\")"),
2360
2707
  source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
2361
2708
  });
2362
2709
  /**
@@ -2565,6 +2912,66 @@ function formatWorkItem(req) {
2565
2912
  return lines.join("\n");
2566
2913
  }
2567
2914
  //#endregion
2915
+ //#region ../../src/tools/list-pending-work-items.ts
2916
+ const ListPendingWorkItemsSchema = zod_v4.z.object({ source: zod_v4.z.string().optional().describe("Source to read. If omitted, uses the default source.") });
2917
+ function resolveAdapter$1(source, adapters, defaultSource) {
2918
+ const sourceType = source ?? defaultSource;
2919
+ if (!sourceType) throw new Error("No source specified and no default source configured");
2920
+ const adapter = adapters.get(sourceType);
2921
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
2922
+ return adapter;
2923
+ }
2924
+ function sanitizeItem(item) {
2925
+ return {
2926
+ ...item,
2927
+ displayId: sanitizeExternalInline(item.displayId),
2928
+ title: sanitizeExternalInline(item.title),
2929
+ statusName: sanitizeExternalInline(item.statusName),
2930
+ assigneeName: item.assigneeName ? sanitizeExternalInline(item.assigneeName) : null,
2931
+ projectName: item.projectName ? sanitizeExternalInline(item.projectName) : null,
2932
+ parentDisplayId: item.parentDisplayId ? sanitizeExternalInline(item.parentDisplayId) : null,
2933
+ warnings: item.warnings.map(sanitizeExternalInline)
2934
+ };
2935
+ }
2936
+ function formatHours(value) {
2937
+ if (value === null) return "—";
2938
+ return `${Number.isInteger(value) ? value : value.toFixed(1)}h`;
2939
+ }
2940
+ function escapeTable(value) {
2941
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
2942
+ }
2943
+ function formatResult(result) {
2944
+ const lines = [
2945
+ "# Pending ONES Work Items",
2946
+ "",
2947
+ `- Total: ${result.total}`,
2948
+ `- Partial rows: ${result.partialCount}`,
2949
+ `- Fetched at: ${result.fetchedAt}`,
2950
+ "- Scope: current assignee; requirements and tasks; status is not started or in progress; defects excluded.",
2951
+ "",
2952
+ UNTRUSTED_SOURCE_NOTICE,
2953
+ "",
2954
+ "| Display ID | Type | Title | Status | Actual | Remaining | Estimate | Plan Start | Plan End |",
2955
+ "| --- | --- | --- | --- | ---: | ---: | ---: | --- | --- |"
2956
+ ];
2957
+ for (const item of result.items) lines.push(`| ${escapeTable(item.displayId)} | ${item.kind} | ${escapeTable(item.title)} | ${escapeTable(item.statusName)} | ${formatHours(item.actualHours)} | ${formatHours(item.remainingHours)} | ${formatHours(item.estimatedHours)} | ${item.planStartDate ?? "—"} | ${item.planEndDate ?? "—"} |`);
2958
+ return lines.join("\n");
2959
+ }
2960
+ async function handleListPendingWorkItems(input, adapters, defaultSource) {
2961
+ const result = await resolveAdapter$1(input.source, adapters, defaultSource).listPendingWorkItems();
2962
+ const safeResult = {
2963
+ ...result,
2964
+ items: result.items.map(sanitizeItem)
2965
+ };
2966
+ return {
2967
+ content: [{
2968
+ type: "text",
2969
+ text: formatResult(safeResult)
2970
+ }],
2971
+ structuredContent: safeResult
2972
+ };
2973
+ }
2974
+ //#endregion
2568
2975
  //#region ../../src/tools/list-sources.ts
2569
2976
  async function handleListSources(adapters, config) {
2570
2977
  const lines = ["# Configured Sources", ""];
@@ -2588,6 +2995,274 @@ async function handleListSources(adapters, config) {
2588
2995
  }] };
2589
2996
  }
2590
2997
  //#endregion
2998
+ //#region ../../src/tools/requirement-decomposition.ts
2999
+ const APPROVAL_TTL_MS = 1800 * 1e3;
3000
+ const MAX_CREATE_OPERATIONS = 10;
3001
+ function isValidDate(value) {
3002
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
3003
+ const [year, month, day] = value.split("-").map(Number);
3004
+ const date = new Date(Date.UTC(year, month - 1, day));
3005
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
3006
+ }
3007
+ const DateSchema$1 = zod_v4.z.string().refine(isValidDate, "Expected a valid YYYY-MM-DD date");
3008
+ function unicodeLength(value) {
3009
+ return Array.from(value).length;
3010
+ }
3011
+ const ShortContentSchema = zod_v4.z.string().trim().min(1).refine((value) => unicodeLength(value) <= 20, "shortContent must not exceed 20 Unicode characters");
3012
+ const RequirementTaskProposalSchema = zod_v4.z.object({
3013
+ shortContent: ShortContentSchema.describe("Concise task content without the requirement display ID; at most 20 Unicode characters."),
3014
+ detail: zod_v4.z.string().trim().min(1).describe("Concrete task detail and completion boundary."),
3015
+ assigneeUuid: zod_v4.z.string().trim().min(1).optional(),
3016
+ priorityUuid: zod_v4.z.string().trim().min(1).optional(),
3017
+ complexityUuid: zod_v4.z.string().trim().min(1).optional(),
3018
+ splitTypeUuid: zod_v4.z.string().trim().min(1).optional(),
3019
+ productUuid: zod_v4.z.string().trim().min(1).optional(),
3020
+ moduleUuid: zod_v4.z.string().trim().min(1).optional(),
3021
+ estimatedHours: zod_v4.z.number().positive().finite().optional(),
3022
+ planStartDate: DateSchema$1.optional(),
3023
+ planEndDate: DateSchema$1.optional()
3024
+ }).refine((value) => !value.planStartDate || !value.planEndDate || value.planStartDate <= value.planEndDate, { message: "planEndDate must be the same as or later than planStartDate" });
3025
+ const InspectRequirementDecompositionSchema = zod_v4.z.object({
3026
+ requirementId: zod_v4.z.string().trim().min(1).describe("ONES requirement UUID, number, or display ID."),
3027
+ source: zod_v4.z.string().optional().describe("Source to inspect. If omitted, uses the default source.")
3028
+ });
3029
+ const PrepareRequirementDecompositionSchema = zod_v4.z.object({
3030
+ requirementId: zod_v4.z.string().trim().min(1).describe("ONES requirement UUID, number, or display ID."),
3031
+ tasks: zod_v4.z.array(RequirementTaskProposalSchema).min(1).max(MAX_CREATE_OPERATIONS),
3032
+ source: zod_v4.z.string().optional().describe("Source to prepare against. If omitted, uses the default source.")
3033
+ });
3034
+ const ApplyRequirementDecompositionSchema = zod_v4.z.object({
3035
+ approvalToken: zod_v4.z.string().trim().min(1),
3036
+ planHash: zod_v4.z.string().regex(/^[a-f0-9]{64}$/),
3037
+ confirmed: zod_v4.z.literal(true).describe("Must be true only after the user confirms the exact prepared operations."),
3038
+ source: zod_v4.z.string().optional().describe("Source to write to. Must match the prepared plan source.")
3039
+ });
3040
+ var RequirementDecompositionApprovalStore = class {
3041
+ approvals = /* @__PURE__ */ new Map();
3042
+ now;
3043
+ ttlMs;
3044
+ constructor(options = {}) {
3045
+ this.now = options.now ?? Date.now;
3046
+ this.ttlMs = options.ttlMs ?? APPROVAL_TTL_MS;
3047
+ }
3048
+ create(record) {
3049
+ const now = this.now();
3050
+ for (const [token, approval] of this.approvals) {
3051
+ const expired = approval.expiresAt <= now;
3052
+ const superseded = approval.source === record.source && approval.requirementUuid === record.requirementUuid;
3053
+ if (expired || superseded) this.approvals.delete(token);
3054
+ }
3055
+ const token = (0, node_crypto.randomBytes)(24).toString("hex");
3056
+ const expiresAt = now + this.ttlMs;
3057
+ this.approvals.set(token, {
3058
+ ...record,
3059
+ expiresAt
3060
+ });
3061
+ return {
3062
+ token,
3063
+ expiresAt
3064
+ };
3065
+ }
3066
+ /** Atomically remove and return an approval before any asynchronous work. */
3067
+ take(token) {
3068
+ const record = this.approvals.get(token);
3069
+ if (!record) return null;
3070
+ this.approvals.delete(token);
3071
+ if (record.expiresAt <= this.now()) return null;
3072
+ return record;
3073
+ }
3074
+ };
3075
+ function resolveAdapter(source, adapters, defaultSource) {
3076
+ const sourceType = source ?? defaultSource;
3077
+ if (!sourceType) throw new Error("No source specified and no default source configured");
3078
+ const adapter = adapters.get(sourceType);
3079
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
3080
+ return {
3081
+ sourceType,
3082
+ adapter
3083
+ };
3084
+ }
3085
+ function sanitizedContext(context) {
3086
+ const requirement = {
3087
+ ...context.requirement,
3088
+ displayId: sanitizeExternalInline(context.requirement.displayId),
3089
+ name: sanitizeExternalInline(context.requirement.name),
3090
+ detail: sanitizeExternalText(context.requirement.detail),
3091
+ issueTypeName: sanitizeExternalInline(context.requirement.issueTypeName),
3092
+ statusName: sanitizeExternalInline(context.requirement.statusName),
3093
+ statusCategory: sanitizeExternalInline(context.requirement.statusCategory),
3094
+ projectName: context.requirement.projectName ? sanitizeExternalInline(context.requirement.projectName) : null,
3095
+ assigneeName: context.requirement.assigneeName ? sanitizeExternalInline(context.requirement.assigneeName) : null
3096
+ };
3097
+ const sanitizeTask = (task) => ({
3098
+ ...task,
3099
+ displayId: sanitizeExternalInline(task.displayId),
3100
+ name: sanitizeExternalInline(task.name),
3101
+ detail: sanitizeExternalText(task.detail),
3102
+ statusName: sanitizeExternalInline(task.statusName),
3103
+ statusCategory: sanitizeExternalInline(task.statusCategory),
3104
+ assigneeName: task.assigneeName ? sanitizeExternalInline(task.assigneeName) : null
3105
+ });
3106
+ const tasks = sortRequirementTasks(context.tasks.map(sanitizeTask));
3107
+ const pendingUuids = new Set(context.pendingTasks.map((task) => task.uuid));
3108
+ return {
3109
+ decompositionRelation: context.decompositionRelation,
3110
+ requirement,
3111
+ tasks,
3112
+ pendingTasks: tasks.filter((task) => pendingUuids.has(task.uuid)),
3113
+ baseline: context.baseline
3114
+ };
3115
+ }
3116
+ function formatInspection(context) {
3117
+ const lines = [
3118
+ `# ${context.requirement.displayId} ${context.requirement.name}`,
3119
+ "",
3120
+ `- **Type**: ${context.requirement.issueTypeName}`,
3121
+ `- **Status**: ${context.requirement.statusName} (${context.requirement.statusCategory})`,
3122
+ `- **Decomposition relation verified**: ${context.decompositionRelation.verified ? "yes" : "no"}`,
3123
+ `- **Related task candidates**: ${context.tasks.length}`,
3124
+ `- **Pending related task candidates**: ${context.pendingTasks.length}`,
3125
+ "- **Implementation order**: use pending tasks only; they are sorted by planned start, planned end, then Display ID, with unset dates last.",
3126
+ "- **Change safety**: compare requirement detail with every task name/detail before coding; warn on meaningful divergence and block affected work on a major mismatch.",
3127
+ "",
3128
+ "## Untrusted ONES Requirement Detail",
3129
+ "",
3130
+ UNTRUSTED_SOURCE_NOTICE,
3131
+ "",
3132
+ context.requirement.detail || "(No requirement detail)",
3133
+ "",
3134
+ context.decompositionRelation.verified ? "## Existing Requirement Decomposition" : "## Related Task Candidates (relationship unverified)",
3135
+ ""
3136
+ ];
3137
+ if (context.tasks.length === 0) lines.push(context.decompositionRelation.verified ? "No existing requirement decomposition tasks." : "No related task candidates were returned; the decomposition relationship is still unverified.");
3138
+ else for (const task of context.tasks) {
3139
+ lines.push(`### ${task.displayId} ${task.name}`);
3140
+ lines.push(`- Status: ${task.statusName} (${task.statusCategory})`);
3141
+ lines.push(`- Plan: ${task.planStartDate ?? "unset"} → ${task.planEndDate ?? "unset"}`);
3142
+ lines.push(`- Assignee: ${task.assigneeName ?? "Unassigned"}`);
3143
+ lines.push("");
3144
+ lines.push(task.detail || "(No task detail)");
3145
+ lines.push("");
3146
+ }
3147
+ return lines.join("\n");
3148
+ }
3149
+ function normalizedShortContent(value) {
3150
+ return value.trim().replace(/\s+/g, " ");
3151
+ }
3152
+ function buildOperations(displayId, tasks) {
3153
+ const seen = /* @__PURE__ */ new Set();
3154
+ return tasks.map((task) => {
3155
+ const shortContent = normalizedShortContent(task.shortContent);
3156
+ if (new RegExp(`^${displayId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(shortContent)) throw new Error("shortContent must not repeat the requirement display ID");
3157
+ const identity = shortContent.toLocaleLowerCase();
3158
+ if (seen.has(identity)) throw new Error(`Duplicate decomposition task shortContent: "${shortContent}"`);
3159
+ seen.add(identity);
3160
+ return {
3161
+ operation: "create",
3162
+ title: `${displayId} ${shortContent}`,
3163
+ shortContent,
3164
+ detail: task.detail,
3165
+ ...task.assigneeUuid ? { assigneeUuid: task.assigneeUuid } : {},
3166
+ ...task.priorityUuid ? { priorityUuid: task.priorityUuid } : {},
3167
+ ...task.complexityUuid ? { complexityUuid: task.complexityUuid } : {},
3168
+ ...task.splitTypeUuid ? { splitTypeUuid: task.splitTypeUuid } : {},
3169
+ ...task.productUuid ? { productUuid: task.productUuid } : {},
3170
+ ...task.moduleUuid ? { moduleUuid: task.moduleUuid } : {},
3171
+ ...task.estimatedHours !== void 0 ? { estimatedHours: task.estimatedHours } : {},
3172
+ ...task.planStartDate ? { planStartDate: task.planStartDate } : {},
3173
+ ...task.planEndDate ? { planEndDate: task.planEndDate } : {}
3174
+ };
3175
+ });
3176
+ }
3177
+ async function handleInspectRequirementDecomposition(input, adapters, defaultSource) {
3178
+ const { adapter } = resolveAdapter(input.source, adapters, defaultSource);
3179
+ const context = sanitizedContext(await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId }));
3180
+ return {
3181
+ content: [{
3182
+ type: "text",
3183
+ text: formatInspection(context)
3184
+ }],
3185
+ structuredContent: context
3186
+ };
3187
+ }
3188
+ async function handlePrepareRequirementDecomposition(input, adapters, approvals, defaultSource) {
3189
+ const { sourceType, adapter } = resolveAdapter(input.source, adapters, defaultSource);
3190
+ const context = await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId });
3191
+ if (context.requirement.workItemKind !== "requirement") throw new Error("Only requirements can be decomposed");
3192
+ if (!context.decompositionRelation.verified || !context.decompositionRelation.uuid) throw new Error("The \"requirement decomposition task\" relationship could not be verified from the read response. No plan or write was prepared.");
3193
+ if (context.requirement.statusCategory !== "to_do" && context.requirement.statusCategory !== "in_progress") throw new Error(`Requirement ${context.requirement.displayId} is not pending (${context.requirement.statusName})`);
3194
+ if (context.tasks.length > 0) throw new Error(`Requirement ${context.requirement.displayId} already has ${context.tasks.length} decomposition task(s). Inspect them; additions or edits require a separate explicit workflow.`);
3195
+ const operations = buildOperations(context.requirement.displayId, input.tasks);
3196
+ const planHash = buildRequirementDecompositionPlanHash({
3197
+ requirementUuid: context.requirement.uuid,
3198
+ decompositionRelation: context.decompositionRelation,
3199
+ baseline: context.baseline,
3200
+ operations
3201
+ });
3202
+ const approval = approvals.create({
3203
+ source: sourceType,
3204
+ requirementId: input.requirementId,
3205
+ requirementUuid: context.requirement.uuid,
3206
+ decompositionRelation: context.decompositionRelation,
3207
+ baseline: context.baseline,
3208
+ operations,
3209
+ planHash
3210
+ });
3211
+ const plan = {
3212
+ requirement: sanitizedContext(context).requirement,
3213
+ decompositionRelation: context.decompositionRelation,
3214
+ operations,
3215
+ baseline: context.baseline,
3216
+ planHash,
3217
+ approvalToken: approval.token,
3218
+ expiresAt: new Date(approval.expiresAt).toISOString()
3219
+ };
3220
+ return {
3221
+ content: [{
3222
+ type: "text",
3223
+ text: [
3224
+ `Prepared ${operations.length} create operation(s) for ${plan.requirement.displayId}.`,
3225
+ "No ONES create or edit request was sent.",
3226
+ "Show the exact operations to the user. Call apply_requirement_decomposition only after explicit confirmation."
3227
+ ].join("\n")
3228
+ }],
3229
+ structuredContent: plan
3230
+ };
3231
+ }
3232
+ async function handleApplyRequirementDecomposition(input, adapters, approvals, options) {
3233
+ if (input.confirmed !== true) throw new Error("Explicit confirmation is required before applying a decomposition");
3234
+ if (!options.writesEnabled) throw new Error("Requirement decomposition writes are disabled. Enable both ONES_ENABLE_WRITES=true and the source requirementDecompositionWrites option only in an approved production deployment.");
3235
+ const record = approvals.take(input.approvalToken);
3236
+ if (!record) throw new Error("Approval token is invalid, expired, or already used. Prepare the decomposition again.");
3237
+ if ((input.source ?? options.defaultSource) !== record.source) throw new Error("Approval token source does not match the requested source");
3238
+ if (input.planHash !== record.planHash) throw new Error("Plan hash does not match the approved decomposition");
3239
+ const { adapter } = resolveAdapter(record.source, adapters, options.defaultSource);
3240
+ const current = await adapter.getRequirementDecompositionContext({ requirementId: record.requirementId });
3241
+ if (current.requirement.uuid !== record.requirementUuid || !isSameRequirementBaseline(current.baseline, record.baseline)) throw new Error("Requirement or related tasks changed after preparation. Prepare and confirm a new decomposition.");
3242
+ if (!current.decompositionRelation.verified || current.decompositionRelation.uuid !== record.decompositionRelation.uuid) throw new Error("The requirement decomposition relationship changed or is no longer verified. Prepare and confirm again.");
3243
+ if (current.tasks.length > 0) throw new Error("Requirement now has decomposition tasks. No create request was sent.");
3244
+ if (buildRequirementDecompositionPlanHash({
3245
+ requirementUuid: record.requirementUuid,
3246
+ decompositionRelation: record.decompositionRelation,
3247
+ baseline: record.baseline,
3248
+ operations: record.operations
3249
+ }) !== record.planHash) throw new Error("Stored decomposition plan failed integrity validation");
3250
+ const result = await adapter.createRequirementDecomposition({
3251
+ requirementUuid: record.requirementUuid,
3252
+ decompositionRelation: record.decompositionRelation,
3253
+ baseline: record.baseline,
3254
+ planHash: record.planHash,
3255
+ operations: record.operations
3256
+ });
3257
+ return {
3258
+ content: [{
3259
+ type: "text",
3260
+ text: `Created ${result.createdTasks.length} requirement decomposition task(s).`
3261
+ }],
3262
+ structuredContent: result
3263
+ };
3264
+ }
3265
+ //#endregion
2591
3266
  //#region ../../src/tools/search-requirements.ts
2592
3267
  const SearchRequirementsSchema = zod_v4.z.object({
2593
3268
  query: zod_v4.z.string().describe("Search keywords"),
@@ -2684,6 +3359,11 @@ function createRequirementsServer(config, adapterOverrides) {
2684
3359
  adapters.set(source.type, adapter);
2685
3360
  }
2686
3361
  const defaultSource = config.config.defaultSource;
3362
+ const decompositionApprovals = new RequirementDecompositionApprovalStore();
3363
+ const decompositionWritesEnabled = (sourceType) => {
3364
+ if (process.env.ONES_ENABLE_WRITES !== "true" || !sourceType) return false;
3365
+ return config.sources.find((candidate) => candidate.type === sourceType)?.config.options?.requirementDecompositionWrites === true;
3366
+ };
2687
3367
  const server = new _modelcontextprotocol_server.McpServer({
2688
3368
  name: "ai-dev-requirements",
2689
3369
  version
@@ -2732,6 +3412,21 @@ function createRequirementsServer(config, adapterOverrides) {
2732
3412
  return toolError(err);
2733
3413
  }
2734
3414
  });
3415
+ server.registerTool("list_pending_work_items", {
3416
+ title: "List Pending Work Items",
3417
+ description: "List the current assignee's not-started and in-progress ONES requirements and tasks with actual, remaining, and estimated hours plus planned dates. Defects are excluded. Read-only.",
3418
+ inputSchema: ListPendingWorkItemsSchema,
3419
+ annotations: {
3420
+ readOnlyHint: true,
3421
+ openWorldHint: true
3422
+ }
3423
+ }, async (params) => {
3424
+ try {
3425
+ return await handleListPendingWorkItems(params, adapters, defaultSource);
3426
+ } catch (err) {
3427
+ return toolError(err);
3428
+ }
3429
+ });
2735
3430
  server.registerTool("get_related_issues", {
2736
3431
  title: "Get Related Issues",
2737
3432
  description: "Get pending defects related to a requirement or task. Rejects a defect ID; use get_issue_detail instead.",
@@ -2793,6 +3488,57 @@ function createRequirementsServer(config, adapterOverrides) {
2793
3488
  return toolError(err);
2794
3489
  }
2795
3490
  });
3491
+ server.registerTool("inspect_requirement_decomposition", {
3492
+ title: "Inspect Requirement Decomposition",
3493
+ description: "Read a requirement and related task candidates with task detail, status, sorted plan dates, and explicit decomposition-relation verification. When the relation is unverified, candidates are not claimed to be decomposition tasks. Rejects tasks and defects. Never creates or edits ONES data.",
3494
+ inputSchema: InspectRequirementDecompositionSchema,
3495
+ annotations: {
3496
+ readOnlyHint: true,
3497
+ openWorldHint: true
3498
+ }
3499
+ }, async (params) => {
3500
+ try {
3501
+ return await handleInspectRequirementDecomposition(params, adapters, defaultSource);
3502
+ } catch (err) {
3503
+ return toolError(err);
3504
+ }
3505
+ });
3506
+ server.registerTool("prepare_requirement_decomposition", {
3507
+ title: "Prepare Requirement Decomposition",
3508
+ description: "Validate a structured decomposition for a pending requirement with no existing decomposition tasks, then return the exact create operations and a one-time approval token. Does not write to ONES.",
3509
+ inputSchema: PrepareRequirementDecompositionSchema,
3510
+ annotations: {
3511
+ readOnlyHint: true,
3512
+ openWorldHint: true
3513
+ }
3514
+ }, async (params) => {
3515
+ try {
3516
+ return await handlePrepareRequirementDecomposition(params, adapters, decompositionApprovals, defaultSource);
3517
+ } catch (err) {
3518
+ return toolError(err);
3519
+ }
3520
+ });
3521
+ server.registerTool("apply_requirement_decomposition", {
3522
+ title: "Apply Requirement Decomposition",
3523
+ description: "Create the exact previously prepared requirement tasks only after explicit user confirmation. Rechecks requirement/task hashes and uses a one-time token. Disabled by default.",
3524
+ inputSchema: ApplyRequirementDecompositionSchema,
3525
+ annotations: {
3526
+ readOnlyHint: false,
3527
+ destructiveHint: false,
3528
+ idempotentHint: false,
3529
+ openWorldHint: true
3530
+ }
3531
+ }, async (params) => {
3532
+ try {
3533
+ const sourceType = params.source ?? defaultSource;
3534
+ return await handleApplyRequirementDecomposition(params, adapters, decompositionApprovals, {
3535
+ defaultSource,
3536
+ writesEnabled: decompositionWritesEnabled(sourceType)
3537
+ });
3538
+ } catch (err) {
3539
+ return toolError(err);
3540
+ }
3541
+ });
2796
3542
  server.registerTool("add_manhour", {
2797
3543
  title: "Add Manhour",
2798
3544
  description: "Add a work-hour record to a ONES task, bug, or requirement. Supports task key, uuid, number, or displayId.",