@mytegroupinc/myte-core 0.0.49 → 0.0.50

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/README.md CHANGED
@@ -38,6 +38,12 @@ This package exists so the public wrapper can stay small and versioned cleanly.
38
38
  inserts cannot shift later pages. It remains compatible with older
39
39
  offset-only API versions and rejects duplicate cross-page records instead of
40
40
  committing an ambiguous local snapshot.
41
+ - `feedback-sync` keeps stable comment IDs and nests every comment attachment
42
+ under its originating comment. Readable text, Markdown, and DOCX attachment
43
+ context is materialized under
44
+ `MyteCommandCenter/feedback-sync/comment-attachments`; binary formats remain
45
+ visible as metadata-only context. Raw S3/object-storage URLs are not written
46
+ into local sync artifacts.
41
47
  - `feedback status|edit|assign|archive|refine` writes reviewable local YAML artifacts.
42
48
  - `feedback submit|revise|comment` are live writes that require `--confirm-write --approval-artifact <path>`.
43
49
  - `feedback review|move|undo|apply` are governed writes with backend permission checks but no approval artifact flags.
@@ -145,12 +151,28 @@ their values and does not disable TLS verification. Captive portals and
145
151
  intercepting gateways are reported as non-JSON/HTML transport failures rather
146
152
  than as malformed Myte API responses.
147
153
 
148
- Feedback comment support:
149
-
150
- - `feedback-sync` includes existing feedback comment turns in local context.
151
- - Feedback-specific comment creation exists in the web backend at `/api/feedbacks/<feedback_id>/comments`, protected by JWT/project assignment.
154
+ Feedback comment support:
155
+
156
+ - `feedback-sync` includes existing feedback comments with `comment_id`,
157
+ sender, content, timestamps, and attachments nested under the originating
158
+ comment.
159
+ - Readable text, Markdown, and DOCX comment attachments are extracted by the
160
+ backend under attachment safety limits and written to
161
+ `MyteCommandCenter/feedback-sync/comment-attachments/<feedback>/<comment>/`.
162
+ The local YAML points to the file. Unsupported binary formats remain listed
163
+ with `context_status: metadata_only`. Extraction is capped at 20 readable
164
+ comment attachments per API page by default; additional readable files stay
165
+ linked to their comments with `context_status: deferred`. Use
166
+ `--comment-attachment-limit <0-100>` to change that bounded limit.
167
+ - `--no-with-prd-text` disables readable PRD and comment-attachment extraction
168
+ while retaining comment and attachment metadata.
169
+ - Feedback-specific comment creation exists in the web backend at `/api/feedbacks/<feedback_id>/comments`, protected by JWT/project assignment.
152
170
  - Project-key comment creation is available through `myte feedback comment --feedback-id <id> --body "..." --confirm-write --approval-artifact ./comment.md` or `--body-file ./comment.md --confirm-write --approval-artifact ./comment.md`.
153
- - The project-key feedback comment endpoint is text-only in this slice and caps content at 500,000 characters. Use the web UI when comment attachments are required.
171
+ - The project-key feedback comment endpoint is text-only in this slice and caps content at 500,000 characters. Use the web UI when comment attachments are required.
172
+ - `feedback apply --force` is an owner/elevated emergency override for a stale
173
+ local artifact. It requires the artifact to contain a non-empty reason and
174
+ returns the persisted force/warning audit context; normal edits should use
175
+ submit followed by owner/elevated review.
154
176
 
155
177
  ## Mission Action Map
156
178
 
@@ -241,16 +263,42 @@ Agent rule: never guess `suggestion_id`. If `mission-ops.yml` is missing or stal
241
263
  | `feedback history` | Lists audited feedback board/refinement events. | Read-only; backend checks project access. |
242
264
  | `feedback validate` | Sends an artifact to backend validation without mutation. | Useful before submit or owner-direct apply. |
243
265
  | `feedback apply` | Applies an artifact through the owner-direct/emergency path. | No approval artifact required; not the normal collaborator flow; normal flow is `submit` -> `review`. |
266
+
267
+ Canonical feedback lifecycle states are `frozen`, `todo`, `in_progress`, `in_review`, `completed`, `deployed`, `rejected`, and `archived`.
268
+
269
+ ### Feedback Permission Matrix
270
+
271
+ | Project role | Feedback permissions |
272
+ | --- | --- |
273
+ | Project Owner | Full Feedback lifecycle: view/sync/comment, draft/validate/submit, direct apply, approve/request changes/reject/cancel reviews, assignment, governed board states, archive, version history, and diffs. |
274
+ | Elevated collaborator (`owner_delegate`) | Same operational Feedback authority as the Project Owner, including direct apply, approval, assignment, governed board states, and archive. Project-level privilege grant/revoke and unrelated destructive infrastructure operations remain real-owner controls. |
275
+ | Builder collaborator | View/sync/comment, create and validate local edits, submit/revise/cancel their review requests, self-assign, and move active work through `todo`, `in_progress`, `in_review`, and `completed`. Cannot approve, directly apply, unassign others, archive, reject, freeze, or deploy Feedback. |
276
+ | Client collaborator | View/sync and participate in Feedback discussion. Does not receive builder edit, submit, assignment, or board-operation capabilities. |
277
+ | Organisation owner not assigned to the project | View access only; no project Feedback mutation authority. |
278
+
279
+ Stale local artifacts fail validation/apply with `snapshot_mismatch`. Re-sync,
280
+ review the newer server state, and create a fresh artifact; `force` is not part
281
+ of the normal collaborator workflow.
244
282
 
245
- Canonical feedback lifecycle states are `frozen`, `todo`, `in_progress`, `in_review`, `completed`, `deployed`, `rejected`, and `archived`.
246
-
247
- Reusable live smoke harness after deploy:
248
-
249
- ```bash
250
- node ./scripts/feedback-live-full-harness.js --confirm-live --base-url https://api.myte.dev/api
251
- ```
252
-
253
- The harness creates five disposable `TEST` feedback items from markdown, verifies batch review and batch board movement, batch archives the disposable items, verifies normal `feedback-sync` excludes archived items, and verifies the project-key surface does not expose Feedback unarchive/restore.
283
+ Reusable live smoke harness after deploy:
284
+
285
+ ```bash
286
+ node ./scripts/feedback-live-full-harness.js --confirm-live --base-url https://api.myte.dev/api
287
+ ```
288
+
289
+ For strict owner/elevated/builder permission certification, set distinct
290
+ `MYTE_OWNER_API_KEY`, `MYTE_DELEGATE_API_KEY`, and
291
+ `MYTE_COLLABORATOR_API_KEY` values scoped to the same test project and add
292
+ `--require-role-matrix`. To certify a real web-created comment attachment,
293
+ provide an ephemeral `MYTE_WEB_ACCESS_TOKEN` for an assigned test user and add
294
+ `--require-comment-attachment-sync`.
295
+
296
+ The harness creates five disposable `TEST` feedback items plus one
297
+ three-document Feedback item. It verifies comment sync, optional comment
298
+ attachment materialization, stale-artifact rejection and fresh re-sync,
299
+ request-changes/revise, owner and elevated approvals, regular collaborator
300
+ denials, batch review and board movement, per-document refinement/version diff,
301
+ exact-ID archival, archived sync exclusion, and project-key unarchive denial.
254
302
 
255
303
  ## Direct Project API Surface
256
304
 
package/cli.js CHANGED
@@ -135,8 +135,8 @@ function splitCommand(argv) {
135
135
  return { command: "query", rest: argv };
136
136
  }
137
137
 
138
- function parseArgs(argv) {
139
- const parsed = { _: [], fetch: true };
138
+ function parseArgs(argv) {
139
+ const parsed = { _: [] };
140
140
  const aliases = { q: "query", d: "with-diff", c: "context", h: "help" };
141
141
  const setValue = (key, value) => {
142
142
  const finalKey = aliases[key] || key;
@@ -328,12 +328,14 @@ function printHelp() {
328
328
  " - Optional: --target-contact-id <id> (repeatable) or --target-contact-ids <id1,id2>",
329
329
  " - If the project has no linked client contacts, the backend falls back to the project owner for internal projects",
330
330
  "",
331
- "feedback-sync contract:",
331
+ "feedback-sync contract:",
332
332
  " - Runs from any workspace where you want local MyteCommandCenter data written",
333
333
  " - Syncs non-archived feedback by default so local Command Center data stays focused on active visible work",
334
334
  " - Archived feedback is intentionally excluded from normal sync; returning it to active work is web UI only",
335
- " - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
336
- " - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
335
+ " - Writes project feedback metadata and comment turns, stable comment ids, and nested attachment metadata into MyteCommandCenter/data/feedback.yml",
336
+ " - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
337
+ " - Stores readable text/Markdown/DOCX comment attachment context under MyteCommandCenter/feedback-sync/comment-attachments and never writes raw storage URLs",
338
+ " - Non-readable comment attachments remain nested under the correct comment as metadata_only context",
337
339
  " - Reads existing feedback comments and can create text-only feedback-specific comments through the project-key API",
338
340
  "",
339
341
  "feedback review contract:",
@@ -428,7 +430,8 @@ function printHelp() {
428
430
  " --due-date <date> Feedback due date target for edit artifacts",
429
431
  " --review-note <text> Review note stored in feedback refinement history",
430
432
  " --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
431
- " --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
433
+ " --no-with-prd-text Skip PRD and readable comment-attachment extraction; keep feedback/comment/attachment metadata",
434
+ " --comment-attachment-limit <n> Cap readable comment-attachment extraction per sync page (0-100; default: 20)",
432
435
  " --mission-ids <ids> Comma-separated mission business ids for run-qaqc, mission status, or mission archive (quote multi-id values on PowerShell)",
433
436
  " --reason <text> Optional reason for governed mission archive or feedback changes",
434
437
  " --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
@@ -436,7 +439,7 @@ function printHelp() {
436
439
  " --approval-artifact Local .md/.markdown/.yml/.yaml/.json approval artifact for gated write commands",
437
440
  " --wait Poll batch status until terminal completion for run-qaqc",
438
441
  " --sync After run-qaqc completes, refresh local QAQC file",
439
- " --force Allow run-qaqc to bypass stale-state protection when supported",
442
+ " --force Allow QAQC retry or owner/elevated stale Feedback apply; Feedback force requires a reason",
440
443
  " --no-sync Skip automatic post-mutation sync for suggestions, missions, and feedback mutations",
441
444
  " --print-context Print JSON payload and exit (no query call)",
442
445
  " --no-fetch Don't git fetch origin main/master before diff",
@@ -1879,18 +1882,18 @@ function collectGitDiffWithDiagnostics({
1879
1882
 
1880
1883
  const headingParts = [`${name} (${dir || "."})`];
1881
1884
  if (repo.role) headingParts.push(`role=${repo.role}`);
1882
- let section = `### ${headingParts.join(" | ")}\n\n`;
1883
- section += `# ?? Base vs HEAD (${repoSummary.base_ref_label}...HEAD | head=${headBranch})\n`;
1884
- if (repoSummary.matched_by?.length) {
1885
- section += `# ?? Matched by ${repoSummary.matched_by.join(", ")}\n`;
1886
- }
1887
- if (repoSummary.compare_remote_url) {
1888
- section += `# ?? Compare remote ${repoSummary.compare_remote_url}\n`;
1889
- }
1890
- if (baseDiff) section += `${baseDiff}\n\n`;
1891
- if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
1892
- if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
1893
- if (untrackedDiffs) section += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
1885
+ let section = `### ${headingParts.join(" | ")}\n\n`;
1886
+ if (repoSummary.matched_by?.length) {
1887
+ section += `# ?? Matched by ${repoSummary.matched_by.join(", ")}\n`;
1888
+ }
1889
+ if (repoSummary.compare_remote_url) {
1890
+ section += `# ?? Compare remote ${repoSummary.compare_remote_url}\n`;
1891
+ }
1892
+ if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
1893
+ if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
1894
+ if (untrackedDiffs) section += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
1895
+ section += `# ?? Base vs HEAD (${repoSummary.base_ref_label}...HEAD | head=${headBranch})\n`;
1896
+ if (baseDiff) section += `${baseDiff}\n\n`;
1894
1897
 
1895
1898
  if (!repoSummary.has_changes) {
1896
1899
  section += "_No local changes - working tree clean_\n\n";
@@ -2008,6 +2011,21 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
2008
2011
  if (filters.includeCommentTurns !== undefined) {
2009
2012
  url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
2010
2013
  }
2014
+ if (filters.includeCommentAttachmentText !== undefined) {
2015
+ url.searchParams.set(
2016
+ "include_comment_attachment_text",
2017
+ filters.includeCommentAttachmentText ? "true" : "false"
2018
+ );
2019
+ }
2020
+ if (
2021
+ filters.commentAttachmentTextLimit !== undefined
2022
+ && filters.commentAttachmentTextLimit !== null
2023
+ ) {
2024
+ url.searchParams.set(
2025
+ "comment_attachment_text_limit",
2026
+ String(filters.commentAttachmentTextLimit)
2027
+ );
2028
+ }
2011
2029
  if (filters.limit !== undefined && filters.limit !== null && filters.limit !== "") {
2012
2030
  url.searchParams.set("limit", String(filters.limit));
2013
2031
  }
@@ -2053,6 +2071,9 @@ function aggregateFeedbackSyncCounts(items, availableTotal) {
2053
2071
  const bySource = {};
2054
2072
  let withPrdText = 0;
2055
2073
  let withConversationTurns = 0;
2074
+ let withCommentAttachments = 0;
2075
+ let commentAttachments = 0;
2076
+ const commentAttachmentContext = {};
2056
2077
  for (const item of items) {
2057
2078
  const status = String(item?.status || "Pending");
2058
2079
  const source = String(item?.source || "User");
@@ -2062,12 +2083,30 @@ function aggregateFeedbackSyncCounts(items, availableTotal) {
2062
2083
  if (Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0) {
2063
2084
  withConversationTurns += 1;
2064
2085
  }
2086
+ const itemCommentAttachments = (item?.conversation_turns || []).reduce(
2087
+ (total, turn) => total + (Array.isArray(turn?.attachments) ? turn.attachments.length : 0),
2088
+ 0
2089
+ );
2090
+ if (itemCommentAttachments > 0) {
2091
+ withCommentAttachments += 1;
2092
+ commentAttachments += itemCommentAttachments;
2093
+ for (const turn of item?.conversation_turns || []) {
2094
+ for (const attachment of turn?.attachments || []) {
2095
+ const contextStatus = String(attachment?.context_status || "metadata_only");
2096
+ commentAttachmentContext[contextStatus] =
2097
+ (commentAttachmentContext[contextStatus] || 0) + 1;
2098
+ }
2099
+ }
2100
+ }
2065
2101
  }
2066
2102
  return {
2067
2103
  total_feedback: items.length,
2068
2104
  available_feedback: Number.isFinite(Number(availableTotal)) ? Number(availableTotal) : items.length,
2069
2105
  with_prd_text: withPrdText,
2070
2106
  with_conversation_turns: withConversationTurns,
2107
+ with_comment_attachments: withCommentAttachments,
2108
+ comment_attachments: commentAttachments,
2109
+ comment_attachment_context: commentAttachmentContext,
2071
2110
  by_status: byStatus,
2072
2111
  by_source: bySource,
2073
2112
  };
@@ -3959,22 +3998,47 @@ function ensureTrailingNewline(value) {
3959
3998
  return text.endsWith("\n") ? text : `${text}\n`;
3960
3999
  }
3961
4000
 
3962
- function normalizeFeedbackConversationTurns(turns) {
3963
- if (!Array.isArray(turns)) return [];
3964
- return turns
3965
- .map((turn) => {
3966
- const content = String(turn?.content || "").trim();
3967
- if (!content) return null;
3968
- const normalized = {
3969
- sender_name: firstNonEmptyString(turn?.sender_name, turn?.user_name, turn?.author_name) || "Unknown",
3970
- content,
3971
- };
3972
- const createdAt = firstNonEmptyString(turn?.created_at, turn?.timestamp);
3973
- if (createdAt) normalized.created_at = createdAt;
3974
- return normalized;
3975
- })
3976
- .filter(Boolean);
3977
- }
4001
+ function normalizeFeedbackConversationTurns(turns) {
4002
+ if (!Array.isArray(turns)) return [];
4003
+ return turns
4004
+ .map((turn) => {
4005
+ const content = String(turn?.content || "").trim();
4006
+ const attachments = (Array.isArray(turn?.attachments) ? turn.attachments : [])
4007
+ .map((attachment) => {
4008
+ if (!isPlainObject(attachment)) return null;
4009
+ const normalizedAttachment = {
4010
+ name: firstNonEmptyString(attachment.name) || "attachment",
4011
+ context_status: firstNonEmptyString(attachment.context_status) || "metadata_only",
4012
+ };
4013
+ for (const field of ["type", "asset_kind", "download_name", "format"]) {
4014
+ const value = firstNonEmptyString(attachment[field]);
4015
+ if (value) normalizedAttachment[field] = value;
4016
+ }
4017
+ const size = Number(attachment.size);
4018
+ if (Number.isFinite(size) && size >= 0) normalizedAttachment.size = size;
4019
+ const attachmentContent = String(attachment.content || "");
4020
+ if (attachmentContent.trim()) normalizedAttachment.content = attachmentContent;
4021
+ if (attachment.truncated !== undefined) {
4022
+ normalizedAttachment.truncated = Boolean(attachment.truncated);
4023
+ }
4024
+ return normalizedAttachment;
4025
+ })
4026
+ .filter(Boolean);
4027
+ if (!content && attachments.length === 0) return null;
4028
+ const normalized = {
4029
+ comment_id: firstNonEmptyString(turn?.comment_id, turn?.id),
4030
+ sender_name: firstNonEmptyString(turn?.sender_name, turn?.user_name, turn?.author_name) || "Unknown",
4031
+ content,
4032
+ attachments,
4033
+ };
4034
+ const createdAt = firstNonEmptyString(turn?.created_at, turn?.timestamp);
4035
+ if (createdAt) normalized.created_at = createdAt;
4036
+ const updatedAt = firstNonEmptyString(turn?.updated_at);
4037
+ if (updatedAt) normalized.updated_at = updatedAt;
4038
+ return normalized;
4039
+ })
4040
+ .filter(Boolean);
4041
+ }
3978
4042
 
3979
4043
  function readJsonFile(filePath) {
3980
4044
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
@@ -4336,20 +4400,83 @@ function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
4336
4400
  function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
4337
4401
  const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
4338
4402
  const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
4403
+ const commentAttachmentsDir = path.join(targetRoot, "feedback-sync", "comment-attachments");
4339
4404
  const transactionToken = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
4340
4405
  const stagingRoot = path.join(targetRoot, `.myte-feedback-sync-${transactionToken}`);
4341
4406
  const stagedDataRoot = path.join(stagingRoot, "data");
4342
4407
  const stagedPrdRoot = path.join(stagingRoot, "PRD", "feedback-sync");
4408
+ const stagedCommentAttachmentsRoot = path.join(stagingRoot, "feedback-sync", "comment-attachments");
4343
4409
  ensureDir(stagedDataRoot);
4344
4410
  ensureDir(stagedPrdRoot);
4411
+ ensureDir(stagedCommentAttachmentsRoot);
4345
4412
 
4346
4413
  const items = Array.isArray(snapshot.items) ? snapshot.items : [];
4347
4414
  let prdFileCount = 0;
4348
4415
  let prdDocumentSetCount = 0;
4416
+ let commentAttachmentCount = 0;
4417
+ let commentAttachmentFileCount = 0;
4418
+ const commentAttachmentContextCounts = {};
4349
4419
  const materializedItems = items.map((rawItem, index) => {
4350
4420
  const item = isPlainObject(rawItem) ? { ...rawItem } : {};
4351
4421
  const feedbackId = stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`);
4352
- const conversationTurns = normalizeFeedbackConversationTurns(item.conversation_turns);
4422
+ const feedbackFolderName = sanitizeFileSegment(feedbackId, `feedback-${index + 1}`);
4423
+ const conversationTurns = normalizeFeedbackConversationTurns(item.conversation_turns)
4424
+ .map((turn, turnIndex) => {
4425
+ const commentId = stableItemId(
4426
+ turn,
4427
+ ["comment_id", "id"],
4428
+ `comment-${String(turnIndex + 1).padStart(3, "0")}`
4429
+ );
4430
+ const commentFolderName = sanitizeFileSegment(commentId, `comment-${turnIndex + 1}`);
4431
+ const attachments = (turn.attachments || []).map((rawAttachment, attachmentIndex) => {
4432
+ const attachment = isPlainObject(rawAttachment) ? { ...rawAttachment } : {};
4433
+ const content = String(attachment.content || "");
4434
+ let localFile = null;
4435
+ commentAttachmentCount += 1;
4436
+ const contextStatus = String(attachment.context_status || "metadata_only");
4437
+ commentAttachmentContextCounts[contextStatus] =
4438
+ (commentAttachmentContextCounts[contextStatus] || 0) + 1;
4439
+ if (content.trim()) {
4440
+ const sourceName = sanitizeFileSegment(
4441
+ attachment.name,
4442
+ `attachment-${attachmentIndex + 1}`
4443
+ );
4444
+ const sourceBase = path.parse(sourceName).name || `attachment-${attachmentIndex + 1}`;
4445
+ const extension = String(attachment.format || "").toLowerCase() === "markdown"
4446
+ ? "md"
4447
+ : "txt";
4448
+ const filename = `${String(attachmentIndex + 1).padStart(2, "0")}-${sourceBase}.${extension}`;
4449
+ const stagedPath = path.join(
4450
+ stagedCommentAttachmentsRoot,
4451
+ feedbackFolderName,
4452
+ commentFolderName,
4453
+ filename
4454
+ );
4455
+ ensureDir(path.dirname(stagedPath));
4456
+ writeTextFile(stagedPath, ensureTrailingNewline(content));
4457
+ localFile = toPosixRelativePath(
4458
+ targetRoot,
4459
+ path.join(
4460
+ commentAttachmentsDir,
4461
+ feedbackFolderName,
4462
+ commentFolderName,
4463
+ filename
4464
+ )
4465
+ );
4466
+ commentAttachmentFileCount += 1;
4467
+ }
4468
+ delete attachment.content;
4469
+ return {
4470
+ ...attachment,
4471
+ local_file: localFile,
4472
+ };
4473
+ });
4474
+ return {
4475
+ ...turn,
4476
+ comment_id: commentId,
4477
+ attachments,
4478
+ };
4479
+ });
4353
4480
  const prdText = String(item.prd_text || "").trim();
4354
4481
  const rawPrdDocuments = Array.isArray(item.prd_documents) ? item.prd_documents : [];
4355
4482
  const attachmentDocuments = Array.isArray(item.attachment_documents) ? item.attachment_documents : [];
@@ -4363,7 +4490,6 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
4363
4490
 
4364
4491
  if (rawPrdDocuments.length > 0) {
4365
4492
  prdDocumentSetCount += 1;
4366
- const feedbackFolderName = sanitizeFileSegment(feedbackId, `feedback-${index + 1}`);
4367
4493
  materializedPrdDocuments = rawPrdDocuments.map((rawDocument, documentIndex) => {
4368
4494
  const document = isPlainObject(rawDocument) ? { ...rawDocument } : {};
4369
4495
  const markdown = String(
@@ -4439,9 +4565,17 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
4439
4565
  : { total_feedback: materializedItems.length };
4440
4566
  snapshotCounts.with_prd_files = prdFileCount;
4441
4567
  snapshotCounts.with_prd_document_sets = prdDocumentSetCount;
4442
- if (snapshotCounts.with_conversation_turns === undefined) {
4443
- snapshotCounts.with_conversation_turns = materializedItems.filter((item) => Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0).length;
4444
- }
4568
+ snapshotCounts.comment_attachments = commentAttachmentCount;
4569
+ snapshotCounts.comment_attachment_context = commentAttachmentContextCounts;
4570
+ snapshotCounts.readable_comment_attachment_files = commentAttachmentFileCount;
4571
+ if (snapshotCounts.with_conversation_turns === undefined) {
4572
+ snapshotCounts.with_conversation_turns = materializedItems.filter((item) => Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0).length;
4573
+ }
4574
+ snapshotCounts.with_comment_attachments = materializedItems.filter(
4575
+ (item) => (item?.conversation_turns || []).some(
4576
+ (turn) => Array.isArray(turn?.attachments) && turn.attachments.length > 0
4577
+ )
4578
+ ).length;
4445
4579
 
4446
4580
  const payload = scrubBootstrapValue({
4447
4581
  schema_version: snapshot.schema_version || 2,
@@ -4451,8 +4585,11 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
4451
4585
  counts: snapshotCounts,
4452
4586
  artifacts: {
4453
4587
  feedback_prd_root: "PRD/feedback-sync",
4588
+ feedback_comment_attachment_root: "feedback-sync/comment-attachments",
4454
4589
  with_prd_files: prdFileCount,
4455
4590
  with_prd_document_sets: prdDocumentSetCount,
4591
+ comment_attachments: commentAttachmentCount,
4592
+ readable_comment_attachment_files: commentAttachmentFileCount,
4456
4593
  },
4457
4594
  queue: materializedItems
4458
4595
  .filter((item) => String(item?.status || "").trim().toLowerCase() !== "resolved")
@@ -4494,6 +4631,10 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
4494
4631
  staged: stagedPrdRoot,
4495
4632
  target: prdSyncDir,
4496
4633
  },
4634
+ {
4635
+ staged: stagedCommentAttachmentsRoot,
4636
+ target: commentAttachmentsDir,
4637
+ },
4497
4638
  {
4498
4639
  staged: stagedFeedbackPath,
4499
4640
  target: path.join(dataRoot, "feedback.yml"),
@@ -4505,6 +4646,7 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
4505
4646
  targetRoot,
4506
4647
  dataRoot,
4507
4648
  prdSyncDir,
4649
+ commentAttachmentsDir,
4508
4650
  manifest: payload,
4509
4651
  };
4510
4652
  } finally {
@@ -4656,11 +4798,13 @@ function summarizeFeedbackRefinementResult(data) {
4656
4798
  new_snapshot_hash: data?.new_snapshot_hash || data?.feedback?.snapshot_hash || null,
4657
4799
  diff_count: Array.isArray(data?.diffs) ? data.diffs.length : 0,
4658
4800
  diffs: data?.diffs || [],
4659
- warnings: data?.warnings || [],
4660
- errors: data?.errors || [],
4661
- history_id: data?.history?.history_id || null,
4662
- };
4663
- }
4801
+ warnings: data?.warnings || [],
4802
+ errors: data?.errors || [],
4803
+ history_id: data?.history?.history_id || null,
4804
+ feedback: data?.feedback || null,
4805
+ history: data?.history || null,
4806
+ };
4807
+ }
4664
4808
 
4665
4809
  function missionOpsThreads(payload) {
4666
4810
  if (Array.isArray(payload?.threads)) return payload.threads;
@@ -6275,6 +6419,7 @@ async function runFeedbackSync(args) {
6275
6419
  const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
6276
6420
  let pageSize;
6277
6421
  let maxItems;
6422
+ let commentAttachmentTextLimit;
6278
6423
  try {
6279
6424
  pageSize = resolveBoundedInteger(
6280
6425
  args["page-size"] || args.pageSize || args.page_size,
@@ -6284,16 +6429,29 @@ async function runFeedbackSync(args) {
6284
6429
  args.limit,
6285
6430
  { fallback: 0, min: 0, max: 100_000, label: "limit" }
6286
6431
  );
6432
+ commentAttachmentTextLimit = resolveBoundedInteger(
6433
+ args["comment-attachment-limit"]
6434
+ || args.commentAttachmentLimit
6435
+ || args.comment_attachment_limit,
6436
+ {
6437
+ fallback: 20,
6438
+ min: 0,
6439
+ max: 100,
6440
+ label: "comment-attachment-limit",
6441
+ }
6442
+ );
6287
6443
  } catch (err) {
6288
6444
  console.error(err?.message || err);
6289
6445
  process.exit(1);
6290
6446
  }
6291
6447
  const filters = {
6292
- status: firstNonEmptyString(args.status) || "",
6293
- source: firstNonEmptyString(args.source) || "",
6294
- includePrdText,
6295
- includeCommentTurns: true,
6296
- };
6448
+ status: firstNonEmptyString(args.status) || "",
6449
+ source: firstNonEmptyString(args.source) || "",
6450
+ includePrdText,
6451
+ includeCommentTurns: true,
6452
+ includeCommentAttachmentText: includePrdText,
6453
+ commentAttachmentTextLimit,
6454
+ };
6297
6455
 
6298
6456
  let snapshot;
6299
6457
  try {
@@ -6403,8 +6561,10 @@ async function refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeou
6403
6561
  filters: {
6404
6562
  status: "",
6405
6563
  source: "",
6406
- includePrdText: true,
6564
+ includePrdText: true,
6407
6565
  includeCommentTurns: true,
6566
+ includeCommentAttachmentText: true,
6567
+ commentAttachmentTextLimit: 20,
6408
6568
  },
6409
6569
  pageSize: DEFAULT_FEEDBACK_SYNC_PAGE_SIZE,
6410
6570
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "description": "Myte CLI core implementation.",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",
@@ -15,7 +15,7 @@
15
15
  "package.json"
16
16
  ],
17
17
  "scripts": {
18
- "test": "node --test"
18
+ "test": "node --test --test-concurrency=1"
19
19
  },
20
20
  "license": "MIT",
21
21
  "engines": {