@mytegroupinc/myte-core 0.0.43 → 0.0.45

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/cli.js CHANGED
@@ -208,10 +208,10 @@ function printHelp() {
208
208
  " myte feedback reviews [--status open|terminal|all] [--request-id <id>] [--json]",
209
209
  " myte feedback review --request-id <id> --action approve|reject|request_changes|cancel [--reason \"...\"] [--json]",
210
210
  " myte feedback review --request-ids \"<id1,id2>\" --action approve|reject|request_changes|cancel [--reason \"...\"] [--json]",
211
- " myte feedback move --feedback-id <id> --to-state in_progress --from-state todo --reason \"...\" [--json]",
212
- " myte feedback move --feedback-ids \"<id1,id2>\" --to-state in_progress --reason \"...\" [--json]",
213
- " myte feedback comment --feedback-id <id> --body \"...\" [--body-file ./comment.md] [--json]",
214
- " myte feedback undo --feedback-id <id> --event-id <id> --reason \"...\" [--json]",
211
+ " myte feedback move --feedback-id <id> --to-state in_progress --from-state todo --reason \"...\" [--json]",
212
+ " myte feedback move --feedback-ids \"<id1,id2>\" --to-state in_progress --reason \"...\" [--json]",
213
+ " myte feedback comment --feedback-id <id> --body \"...\" [--body-file ./comment.md] [--json]",
214
+ " myte feedback undo --feedback-id <id> --event-id <id> --reason \"...\" [--json]",
215
215
  " myte feedback prd-versions --feedback-id <id> [--json]",
216
216
  " myte feedback prd-diff --feedback-id <id> --version-id <id> [--compare-to <id>] [--json]",
217
217
  " myte feedback validate --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
@@ -235,6 +235,12 @@ function printHelp() {
235
235
  " - Set MYTE_API_KEY in a workspace .env (or env var)",
236
236
  " - Set MYTEAI_API_KEY in a workspace .env (or env var) for `myte ai`",
237
237
  "",
238
+ "Live write approval:",
239
+ " - Live project-key mutations require --confirm-write --approval-artifact <path>",
240
+ " - The artifact must be a local .md, .yml/.yaml, or .json file shown to and approved by the user before the command is rerun",
241
+ " - Batch artifacts must enumerate every feedback id, mission id, suggestion id, request id, or PRD item",
242
+ " - Read/sync/query/validate, --print-context, and --dry-run commands do not require write approval",
243
+ "",
238
244
  "bootstrap contract:",
239
245
  " - Run from any workspace where you want local MyteCommandCenter data written",
240
246
  " - Writes MyteCommandCenter/data/project.yml plus phases, epics, stories, and missions locally",
@@ -313,7 +319,7 @@ function printHelp() {
313
319
  " - Archived feedback is intentionally excluded from normal sync; returning it to active work is web UI only",
314
320
  " - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
315
321
  " - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
316
- " - Reads existing feedback comments and can create text-only feedback-specific comments through the project-key API",
322
+ " - Reads existing feedback comments and can create text-only feedback-specific comments through the project-key API",
317
323
  "",
318
324
  "feedback review contract:",
319
325
  " - Draft commands write review artifacts under MyteCommandCenter/reviews/feedback/*.yml for local IDE diff review",
@@ -322,9 +328,9 @@ function printHelp() {
322
328
  " - reviews/review expose owner/delegate approval, rejection, request-changes, and cancel decisions",
323
329
  " - review with --request-ids sends one batch review request; it does not loop single-review calls",
324
330
  " - live lifecycle moves return pending_feedback_review while an active linked review request exists",
325
- " - move/undo operate safe board states directly; governed states remain backend-authorized",
326
- " - move with --feedback-ids sends one batch board move request; it supports active-state moves and authorized archive",
327
- " - comment creates text-only feedback-specific comments; attachments remain web UI only for this endpoint",
331
+ " - move/undo operate safe board states directly; governed states remain backend-authorized",
332
+ " - move with --feedback-ids sends one batch board move request; it supports active-state moves and authorized archive",
333
+ " - comment creates text-only feedback-specific comments; attachments remain web UI only for this endpoint",
328
334
  " - project-key API/CLI cannot unarchive archived Feedback; use the web archived Feedback view",
329
335
  " - get/history read current feedback snapshots and audited event history through the project-key API",
330
336
  " - prd-versions/prd-diff expose retained PRD versions and backend-generated text diffs",
@@ -340,9 +346,9 @@ function printHelp() {
340
346
  " - revise: resubmit the original submitter's request after request_changes",
341
347
  " - reviews: list open/terminal/all requests or inspect one request",
342
348
  " - review: approve, reject, request_changes, or cancel; backend restricts this to owner/delegate capability",
343
- " - move: direct audited board movement for allowed states; governed transitions remain backend-authorized",
344
- " - comment: create a text-only comment on one Feedback item",
345
- " - undo: reverse an audited board event when conflict checks allow it",
349
+ " - move: direct audited board movement for allowed states; governed transitions remain backend-authorized",
350
+ " - comment: create a text-only comment on one Feedback item",
351
+ " - undo: reverse an audited board event when conflict checks allow it",
346
352
  " - prd-versions: list retained PRD baselines/revisions for a feedback item",
347
353
  " - prd-diff: fetch backend-generated text diff between PRD versions",
348
354
  " - history: list board/refinement events for audit review",
@@ -363,11 +369,11 @@ function printHelp() {
363
369
  " --stdin Read supported command content from stdin instead of inline text or a file path",
364
370
  " --title <text> Override PRD title for raw markdown uploads",
365
371
  " --description <text> Set feedback description/card summary for raw markdown uploads",
366
- " --content <text> Team update content for update-team",
367
- " --body <text> Feedback comment body for feedback comment",
368
- " --subject <text> Subject for update-owner or update-client",
372
+ " --content <text> Team update content for update-team",
373
+ " --body <text> Feedback comment body for feedback comment",
374
+ " --subject <text> Subject for update-owner or update-client",
369
375
  " --body-markdown <md> Markdown body for update-owner or update-client",
370
- " --body-file <path> Read feedback comment, update-owner, or update-client markdown body from a file",
376
+ " --body-file <path> Read feedback comment, update-owner, or update-client markdown body from a file",
371
377
  " --target-contact-id Add one client contact ObjectId (repeatable)",
372
378
  " --target-contact-ids Comma-separated client contact ObjectIds",
373
379
  " --feedback-id <id> Feedback ObjectId for feedback review commands",
@@ -396,6 +402,8 @@ function printHelp() {
396
402
  " --wait Poll batch status until terminal completion for run-qaqc",
397
403
  " --sync After run-qaqc completes, refresh local QAQC file",
398
404
  " --force Allow run-qaqc to bypass stale-state protection when supported",
405
+ " --confirm-write Confirm the user explicitly approved a live Myte project-key mutation",
406
+ " --approval-artifact Local .md/.yml/.json approval artifact shown to and approved by the user",
399
407
  " --no-sync Skip automatic post-mutation sync for suggestions, missions, and feedback mutations",
400
408
  " --print-context Print JSON payload and exit (no query call)",
401
409
  " --no-fetch Don't git fetch origin main/master before diff",
@@ -421,10 +429,10 @@ function printHelp() {
421
429
  " myte feedback status --feedback-id 507f1f77bcf86cd799439011 --status in_review --reason \"Ready for owner review\"",
422
430
  " myte feedback submit --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-edit.yml --json",
423
431
  " myte feedback reviews --status open --json",
424
- " myte feedback review --request-id 507f1f77bcf86cd799439022 --action approve --reason \"Looks correct\" --json",
425
- " myte feedback move --feedback-id 507f1f77bcf86cd799439011 --to-state in_progress --reason \"Started\" --json",
426
- " myte feedback comment --feedback-id 507f1f77bcf86cd799439011 --body \"Implementation notes are ready for review.\" --json",
427
- " myte feedback validate --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
432
+ " myte feedback review --request-id 507f1f77bcf86cd799439022 --action approve --reason \"Looks correct\" --json",
433
+ " myte feedback move --feedback-id 507f1f77bcf86cd799439011 --to-state in_progress --reason \"Started\" --json",
434
+ " myte feedback comment --feedback-id 507f1f77bcf86cd799439011 --body \"Implementation notes are ready for review.\" --json",
435
+ " myte feedback validate --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
428
436
  " myte feedback apply --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
429
437
  " myte suggestions create --file ./suggestions/create.yml",
430
438
  " myte suggestions revise",
@@ -693,6 +701,164 @@ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
693
701
  }
694
702
  }
695
703
 
704
+ const WRITE_APPROVAL_TRANSPORT_KEYS = new Set(["write_approval", "client_session_id"]);
705
+
706
+ function canonicalizeWritePayload(value) {
707
+ if (Array.isArray(value)) {
708
+ return value.map((item) => canonicalizeWritePayload(item));
709
+ }
710
+ if (value && typeof value === "object") {
711
+ return Object.fromEntries(
712
+ Object.keys(value)
713
+ .filter((key) => !WRITE_APPROVAL_TRANSPORT_KEYS.has(String(key)))
714
+ .sort()
715
+ .map((key) => [key, canonicalizeWritePayload(value[key])])
716
+ );
717
+ }
718
+ return value;
719
+ }
720
+
721
+ function projectApiWritePayloadHash(payload) {
722
+ return createHash("sha256")
723
+ .update(pythonJsonDumpsCompatible(canonicalizeWritePayload(payload || {})))
724
+ .digest("hex");
725
+ }
726
+
727
+ function isConfirmWrite(args) {
728
+ return Boolean(args["confirm-write"] || args.confirmWrite || args.confirm_write);
729
+ }
730
+
731
+ function resolveWriteApprovalArtifactPath(args) {
732
+ const explicit = firstNonEmptyString(args["approval-artifact"], args.approvalArtifact, args.approval_artifact);
733
+ if (!explicit) return "";
734
+ const resolved = path.resolve(process.cwd(), explicit);
735
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
736
+ console.error(`Approval artifact not found: ${resolved}`);
737
+ process.exit(1);
738
+ }
739
+ return resolved;
740
+ }
741
+
742
+ function inferApprovalArtifactKind(filePath, fallbackKind) {
743
+ const ext = path.extname(String(filePath || "")).toLowerCase();
744
+ if (ext === ".md" || ext === ".markdown") return "markdown";
745
+ if (ext === ".yml" || ext === ".yaml") return "yaml";
746
+ if (ext === ".json") return "json";
747
+ return fallbackKind || "artifact";
748
+ }
749
+
750
+ function normalizeApprovalTargets(targets) {
751
+ return toStringArray(targets).map((item) => item.trim()).filter(Boolean);
752
+ }
753
+
754
+ function writePayloadItems(payload) {
755
+ if (Array.isArray(payload?.items)) return payload.items;
756
+ if (Array.isArray(payload)) return payload;
757
+ return payload && typeof payload === "object" ? [payload] : [];
758
+ }
759
+
760
+ function suggestionApprovalTargets(payload) {
761
+ return writePayloadItems(payload).map((item, index) => {
762
+ if (!item || typeof item !== "object") return `index:${index}`;
763
+ return firstNonEmptyString(
764
+ item.suggestion_id,
765
+ item.id,
766
+ item._id,
767
+ item.mission_id,
768
+ item.target_mission_id,
769
+ item.title ? `${item.change_type || "suggestion"}:${item.title}` : ""
770
+ ) || `index:${index}`;
771
+ });
772
+ }
773
+
774
+ function prdApprovalTargets(payload) {
775
+ return writePayloadItems(payload).map((item, index) => {
776
+ if (!item || typeof item !== "object") return `index:${index}`;
777
+ return firstNonEmptyString(item.client_ref, item.title) || `index:${index}`;
778
+ });
779
+ }
780
+
781
+ function requireWriteApproval(args, payload, { operation, artifactKind, targets = [], batchCount = null } = {}) {
782
+ const payloadSha256 = projectApiWritePayloadHash(payload || {});
783
+ const targetValues = normalizeApprovalTargets(targets);
784
+ const expectedBatchCount = batchCount !== null && batchCount !== undefined
785
+ ? Number(batchCount)
786
+ : targetValues.length > 1
787
+ ? targetValues.length
788
+ : null;
789
+
790
+ if (!isConfirmWrite(args)) {
791
+ console.error("Refusing live Myte write without explicit human approval.");
792
+ console.error(`Operation: ${operation || "unknown"}`);
793
+ console.error(`Payload SHA256: ${payloadSha256}`);
794
+ if (targetValues.length) console.error(`Targets: ${targetValues.join(", ")}`);
795
+ if (expectedBatchCount !== null) console.error(`Batch count: ${expectedBatchCount}`);
796
+ console.error("Create a local .md/.yml/.json approval artifact that lists the exact operation, payload summary, and every target.");
797
+ console.error("Show that artifact to the user and ask for explicit approval.");
798
+ console.error("Then rerun with --confirm-write --approval-artifact <path>.");
799
+ process.exit(1);
800
+ }
801
+
802
+ const artifactPath = resolveWriteApprovalArtifactPath(args);
803
+ if (!artifactPath) {
804
+ console.error("Missing --approval-artifact <path> for approved Myte write.");
805
+ console.error(`Payload SHA256: ${payloadSha256}`);
806
+ console.error("The artifact must be a local .md/.yml/.json file shown to and explicitly approved by the user.");
807
+ process.exit(1);
808
+ }
809
+
810
+ return {
811
+ approved: true,
812
+ source: "explicit_user_approval",
813
+ artifact_path: artifactPath,
814
+ artifact_kind: inferApprovalArtifactKind(artifactPath, artifactKind),
815
+ payload_sha256: payloadSha256,
816
+ targets: targetValues,
817
+ batch_count: expectedBatchCount,
818
+ };
819
+ }
820
+
821
+ function writeApprovalHeaders(writeApproval) {
822
+ if (!writeApproval) return {};
823
+ const headers = {
824
+ "X-Myte-Write-Approval": "explicit_user_approval",
825
+ "X-Myte-Approval-Artifact": String(writeApproval.artifact_path || ""),
826
+ "X-Myte-Approval-Artifact-Kind": String(writeApproval.artifact_kind || ""),
827
+ "X-Myte-Approval-Payload-SHA256": String(writeApproval.payload_sha256 || ""),
828
+ };
829
+ if (writeApproval.batch_count !== null && writeApproval.batch_count !== undefined) {
830
+ headers["X-Myte-Approval-Batch-Count"] = String(writeApproval.batch_count);
831
+ }
832
+ if (Array.isArray(writeApproval.targets) && writeApproval.targets.length) {
833
+ headers["X-Myte-Approval-Targets"] = writeApproval.targets.join(",");
834
+ }
835
+ return headers;
836
+ }
837
+
838
+ function formatProjectApiErrorMessage(body, fallback) {
839
+ const message = body?.message || fallback;
840
+ const data = body?.data || {};
841
+ const code = String(data?.code || "");
842
+ if (!code.startsWith("myte_write_approval")) {
843
+ return message;
844
+ }
845
+ const lines = [
846
+ message,
847
+ `Approval error: ${code}`,
848
+ ];
849
+ if (data.operation) lines.push(`Operation: ${data.operation}`);
850
+ if (data.payload_sha256) lines.push(`Payload SHA256: ${data.payload_sha256}`);
851
+ if (Array.isArray(data.targets) && data.targets.length) lines.push(`Targets: ${data.targets.join(", ")}`);
852
+ if (data.batch_count !== undefined && data.batch_count !== null) lines.push(`Batch count: ${data.batch_count}`);
853
+ if (Array.isArray(data.required_agent_steps) && data.required_agent_steps.length) {
854
+ lines.push("Required agent steps:");
855
+ for (const step of data.required_agent_steps) {
856
+ lines.push(`- ${step}`);
857
+ }
858
+ }
859
+ return lines.join("\n");
860
+ }
861
+
696
862
  function summarizeDiffDiagnosticsForContext(diagnostics) {
697
863
  if (!diagnostics) return null;
698
864
  const repos = Array.isArray(diagnostics.repo_summaries)
@@ -1756,7 +1922,7 @@ async function fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId }) {
1756
1922
  return body.data || {};
1757
1923
  }
1758
1924
 
1759
- async function postFeedbackRefinement({ apiBase, key, timeoutMs, feedbackId, mode, payload, idempotencyKey, clientSessionId }) {
1925
+ async function postFeedbackRefinement({ apiBase, key, timeoutMs, feedbackId, mode, payload, idempotencyKey, clientSessionId, writeApproval }) {
1760
1926
  const fetchFn = await getFetch();
1761
1927
  const action = String(mode || "").trim();
1762
1928
  const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/${action}`;
@@ -1764,6 +1930,7 @@ async function postFeedbackRefinement({ apiBase, key, timeoutMs, feedbackId, mod
1764
1930
  "Content-Type": "application/json",
1765
1931
  Authorization: `Bearer ${key}`,
1766
1932
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
1933
+ ...writeApprovalHeaders(writeApproval),
1767
1934
  };
1768
1935
  if (idempotencyKey) {
1769
1936
  headers["X-Idempotency-Key"] = String(idempotencyKey).trim();
@@ -1780,7 +1947,7 @@ async function postFeedbackRefinement({ apiBase, key, timeoutMs, feedbackId, mod
1780
1947
  );
1781
1948
 
1782
1949
  if (!resp.ok || body.status !== "success") {
1783
- const msg = body?.message || `Feedback refinement ${action} failed (${resp.status})`;
1950
+ const msg = formatProjectApiErrorMessage(body, `Feedback refinement ${action} failed (${resp.status})`);
1784
1951
  const err = new Error(msg);
1785
1952
  err.status = resp.status;
1786
1953
  err.data = body?.data;
@@ -1859,7 +2026,7 @@ async function fetchFeedbackEvents({ apiBase, key, timeoutMs, feedbackId, limit
1859
2026
  return body.data || {};
1860
2027
  }
1861
2028
 
1862
- async function postFeedbackReviewSubmission({ apiBase, key, timeoutMs, feedbackId, payload, idempotencyKey, clientSessionId }) {
2029
+ async function postFeedbackReviewSubmission({ apiBase, key, timeoutMs, feedbackId, payload, idempotencyKey, clientSessionId, writeApproval }) {
1863
2030
  const fetchFn = await getFetch();
1864
2031
  const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/requests`;
1865
2032
  const { resp, body } = await fetchJsonWithTimeout(
@@ -1872,6 +2039,7 @@ async function postFeedbackReviewSubmission({ apiBase, key, timeoutMs, feedbackI
1872
2039
  Authorization: `Bearer ${key}`,
1873
2040
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
1874
2041
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2042
+ ...writeApprovalHeaders(writeApproval),
1875
2043
  },
1876
2044
  body: JSON.stringify(payload || {}),
1877
2045
  },
@@ -1879,7 +2047,7 @@ async function postFeedbackReviewSubmission({ apiBase, key, timeoutMs, feedbackI
1879
2047
  );
1880
2048
 
1881
2049
  if (!resp.ok || body.status !== "success") {
1882
- const msg = body?.message || `Feedback review submission failed (${resp.status})`;
2050
+ const msg = formatProjectApiErrorMessage(body, `Feedback review submission failed (${resp.status})`);
1883
2051
  const err = new Error(msg);
1884
2052
  err.status = resp.status;
1885
2053
  err.data = body?.data;
@@ -1888,7 +2056,7 @@ async function postFeedbackReviewSubmission({ apiBase, key, timeoutMs, feedbackI
1888
2056
  return body.data || {};
1889
2057
  }
1890
2058
 
1891
- async function postFeedbackReviewRequestMutation({ apiBase, key, timeoutMs, requestId, endpointAction, payload, idempotencyKey, clientSessionId }) {
2059
+ async function postFeedbackReviewRequestMutation({ apiBase, key, timeoutMs, requestId, endpointAction, payload, idempotencyKey, clientSessionId, writeApproval }) {
1892
2060
  const fetchFn = await getFetch();
1893
2061
  const url = `${apiBase}/project-assistant/feedback-review-requests/${encodeURIComponent(String(requestId || ""))}/${endpointAction}`;
1894
2062
  const { resp, body } = await fetchJsonWithTimeout(
@@ -1901,6 +2069,7 @@ async function postFeedbackReviewRequestMutation({ apiBase, key, timeoutMs, requ
1901
2069
  Authorization: `Bearer ${key}`,
1902
2070
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
1903
2071
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2072
+ ...writeApprovalHeaders(writeApproval),
1904
2073
  },
1905
2074
  body: JSON.stringify(payload || {}),
1906
2075
  },
@@ -1908,7 +2077,7 @@ async function postFeedbackReviewRequestMutation({ apiBase, key, timeoutMs, requ
1908
2077
  );
1909
2078
 
1910
2079
  if (!resp.ok || body.status !== "success") {
1911
- const msg = body?.message || `Feedback review ${endpointAction} failed (${resp.status})`;
2080
+ const msg = formatProjectApiErrorMessage(body, `Feedback review ${endpointAction} failed (${resp.status})`);
1912
2081
  const err = new Error(msg);
1913
2082
  err.status = resp.status;
1914
2083
  err.data = body?.data;
@@ -1917,7 +2086,7 @@ async function postFeedbackReviewRequestMutation({ apiBase, key, timeoutMs, requ
1917
2086
  return body.data || {};
1918
2087
  }
1919
2088
 
1920
- async function postFeedbackBatchReviewMutation({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId }) {
2089
+ async function postFeedbackBatchReviewMutation({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId, writeApproval }) {
1921
2090
  const fetchFn = await getFetch();
1922
2091
  const url = `${apiBase}/project-assistant/feedback-review-requests/batch-review`;
1923
2092
  const { resp, body } = await fetchJsonWithTimeout(
@@ -1930,6 +2099,7 @@ async function postFeedbackBatchReviewMutation({ apiBase, key, timeoutMs, payloa
1930
2099
  Authorization: `Bearer ${key}`,
1931
2100
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
1932
2101
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2102
+ ...writeApprovalHeaders(writeApproval),
1933
2103
  },
1934
2104
  body: JSON.stringify(payload || {}),
1935
2105
  },
@@ -1937,7 +2107,7 @@ async function postFeedbackBatchReviewMutation({ apiBase, key, timeoutMs, payloa
1937
2107
  );
1938
2108
 
1939
2109
  if (!resp.ok || body.status !== "success") {
1940
- const msg = body?.message || `Feedback batch review failed (${resp.status})`;
2110
+ const msg = formatProjectApiErrorMessage(body, `Feedback batch review failed (${resp.status})`);
1941
2111
  const err = new Error(msg);
1942
2112
  err.status = resp.status;
1943
2113
  err.data = body?.data;
@@ -1946,7 +2116,7 @@ async function postFeedbackBatchReviewMutation({ apiBase, key, timeoutMs, payloa
1946
2116
  return body.data || {};
1947
2117
  }
1948
2118
 
1949
- async function postFeedbackBoardMutation({ apiBase, key, timeoutMs, feedbackId, endpoint, payload, idempotencyKey, clientSessionId }) {
2119
+ async function postFeedbackBoardMutation({ apiBase, key, timeoutMs, feedbackId, endpoint, payload, idempotencyKey, clientSessionId, writeApproval }) {
1950
2120
  const fetchFn = await getFetch();
1951
2121
  const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/${endpoint}`;
1952
2122
  const { resp, body } = await fetchJsonWithTimeout(
@@ -1959,6 +2129,7 @@ async function postFeedbackBoardMutation({ apiBase, key, timeoutMs, feedbackId,
1959
2129
  Authorization: `Bearer ${key}`,
1960
2130
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
1961
2131
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2132
+ ...writeApprovalHeaders(writeApproval),
1962
2133
  },
1963
2134
  body: JSON.stringify(payload || {}),
1964
2135
  },
@@ -1966,7 +2137,7 @@ async function postFeedbackBoardMutation({ apiBase, key, timeoutMs, feedbackId,
1966
2137
  );
1967
2138
 
1968
2139
  if (!resp.ok || body.status !== "success") {
1969
- const msg = body?.message || `Feedback board mutation failed (${resp.status})`;
2140
+ const msg = formatProjectApiErrorMessage(body, `Feedback board mutation failed (${resp.status})`);
1970
2141
  const err = new Error(msg);
1971
2142
  err.status = resp.status;
1972
2143
  err.data = body?.data;
@@ -1975,7 +2146,7 @@ async function postFeedbackBoardMutation({ apiBase, key, timeoutMs, feedbackId,
1975
2146
  return body.data || {};
1976
2147
  }
1977
2148
 
1978
- async function postFeedbackBatchBoardMutation({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId }) {
2149
+ async function postFeedbackBatchBoardMutation({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId, writeApproval }) {
1979
2150
  const fetchFn = await getFetch();
1980
2151
  const url = `${apiBase}/project-assistant/feedback/batch-board-move`;
1981
2152
  const { resp, body } = await fetchJsonWithTimeout(
@@ -1988,6 +2159,7 @@ async function postFeedbackBatchBoardMutation({ apiBase, key, timeoutMs, payload
1988
2159
  Authorization: `Bearer ${key}`,
1989
2160
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
1990
2161
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2162
+ ...writeApprovalHeaders(writeApproval),
1991
2163
  },
1992
2164
  body: JSON.stringify(payload || {}),
1993
2165
  },
@@ -1995,45 +2167,46 @@ async function postFeedbackBatchBoardMutation({ apiBase, key, timeoutMs, payload
1995
2167
  );
1996
2168
 
1997
2169
  if (!resp.ok || body.status !== "success") {
1998
- const msg = body?.message || `Feedback batch board mutation failed (${resp.status})`;
2170
+ const msg = formatProjectApiErrorMessage(body, `Feedback batch board mutation failed (${resp.status})`);
1999
2171
  const err = new Error(msg);
2000
2172
  err.status = resp.status;
2001
2173
  err.data = body?.data;
2002
2174
  throw err;
2003
2175
  }
2004
- return body.data || {};
2005
- }
2006
-
2007
- async function postFeedbackComment({ apiBase, key, timeoutMs, feedbackId, payload, idempotencyKey, clientSessionId }) {
2008
- const fetchFn = await getFetch();
2009
- const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/comments`;
2010
- const { resp, body } = await fetchJsonWithTimeout(
2011
- fetchFn,
2012
- url,
2013
- {
2014
- method: "POST",
2015
- headers: {
2016
- "Content-Type": "application/json",
2017
- Authorization: `Bearer ${key}`,
2018
- "X-Idempotency-Key": String(idempotencyKey || "").trim(),
2019
- ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2020
- },
2021
- body: JSON.stringify(payload || {}),
2022
- },
2023
- timeoutMs
2024
- );
2025
-
2026
- if (!resp.ok || body.status !== "success") {
2027
- const msg = body?.message || `Feedback comment failed (${resp.status})`;
2028
- const err = new Error(msg);
2029
- err.status = resp.status;
2030
- err.data = body?.data;
2031
- throw err;
2032
- }
2033
- return body.data || {};
2034
- }
2035
-
2036
- async function fetchFeedbackPrdVersions({ apiBase, key, timeoutMs, feedbackId }) {
2176
+ return body.data || {};
2177
+ }
2178
+
2179
+ async function postFeedbackComment({ apiBase, key, timeoutMs, feedbackId, payload, idempotencyKey, clientSessionId, writeApproval }) {
2180
+ const fetchFn = await getFetch();
2181
+ const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/comments`;
2182
+ const { resp, body } = await fetchJsonWithTimeout(
2183
+ fetchFn,
2184
+ url,
2185
+ {
2186
+ method: "POST",
2187
+ headers: {
2188
+ "Content-Type": "application/json",
2189
+ Authorization: `Bearer ${key}`,
2190
+ "X-Idempotency-Key": String(idempotencyKey || "").trim(),
2191
+ ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2192
+ ...writeApprovalHeaders(writeApproval),
2193
+ },
2194
+ body: JSON.stringify(payload || {}),
2195
+ },
2196
+ timeoutMs
2197
+ );
2198
+
2199
+ if (!resp.ok || body.status !== "success") {
2200
+ const msg = formatProjectApiErrorMessage(body, `Feedback comment failed (${resp.status})`);
2201
+ const err = new Error(msg);
2202
+ err.status = resp.status;
2203
+ err.data = body?.data;
2204
+ throw err;
2205
+ }
2206
+ return body.data || {};
2207
+ }
2208
+
2209
+ async function fetchFeedbackPrdVersions({ apiBase, key, timeoutMs, feedbackId }) {
2037
2210
  const fetchFn = await getFetch();
2038
2211
  const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/prd/versions`;
2039
2212
  const { resp, body } = await fetchJsonWithTimeout(
@@ -2130,7 +2303,7 @@ function resolveProjectMutationIdempotencyKey({ args, operation, payload }) {
2130
2303
  return createHash("sha256").update(fingerprint).digest("hex");
2131
2304
  }
2132
2305
 
2133
- async function postSuggestionsMutation({ apiBase, key, timeoutMs, endpoint, payload, idempotencyKey, clientSessionId }) {
2306
+ async function postSuggestionsMutation({ apiBase, key, timeoutMs, endpoint, payload, idempotencyKey, clientSessionId, writeApproval }) {
2134
2307
  const fetchFn = await getFetch();
2135
2308
  const url = `${apiBase}${endpoint}`;
2136
2309
  const { resp, body } = await fetchJsonWithTimeout(
@@ -2143,6 +2316,7 @@ async function postSuggestionsMutation({ apiBase, key, timeoutMs, endpoint, payl
2143
2316
  Authorization: `Bearer ${key}`,
2144
2317
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
2145
2318
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2319
+ ...writeApprovalHeaders(writeApproval),
2146
2320
  },
2147
2321
  body: JSON.stringify(payload),
2148
2322
  },
@@ -2150,15 +2324,16 @@ async function postSuggestionsMutation({ apiBase, key, timeoutMs, endpoint, payl
2150
2324
  );
2151
2325
 
2152
2326
  if (!resp.ok || body.status !== "success") {
2153
- const msg = body?.message || `Mission suggestions request failed (${resp.status})`;
2327
+ const msg = formatProjectApiErrorMessage(body, `Mission suggestions request failed (${resp.status})`);
2154
2328
  const err = new Error(msg);
2155
2329
  err.status = resp.status;
2330
+ err.data = body?.data;
2156
2331
  throw err;
2157
2332
  }
2158
2333
  return body.data || {};
2159
2334
  }
2160
2335
 
2161
- async function createRunQaqcBatch({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId }) {
2336
+ async function createRunQaqcBatch({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId, writeApproval }) {
2162
2337
  const fetchFn = await getFetch();
2163
2338
  const url = `${apiBase}/project-assistant/run-qaqc`;
2164
2339
  const { resp, body } = await fetchJsonWithTimeout(
@@ -2171,6 +2346,7 @@ async function createRunQaqcBatch({ apiBase, key, timeoutMs, payload, idempotenc
2171
2346
  Authorization: `Bearer ${key}`,
2172
2347
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
2173
2348
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2349
+ ...writeApprovalHeaders(writeApproval),
2174
2350
  },
2175
2351
  body: JSON.stringify(payload),
2176
2352
  },
@@ -2178,15 +2354,16 @@ async function createRunQaqcBatch({ apiBase, key, timeoutMs, payload, idempotenc
2178
2354
  );
2179
2355
 
2180
2356
  if (!resp.ok || body.status !== "success") {
2181
- const msg = body?.message || `Run QAQC request failed (${resp.status})`;
2357
+ const msg = formatProjectApiErrorMessage(body, `Run QAQC request failed (${resp.status})`);
2182
2358
  const err = new Error(msg);
2183
2359
  err.status = resp.status;
2360
+ err.data = body?.data;
2184
2361
  throw err;
2185
2362
  }
2186
2363
  return body.data || {};
2187
2364
  }
2188
2365
 
2189
- async function createMissionStatusUpdate({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId }) {
2366
+ async function createMissionStatusUpdate({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId, writeApproval }) {
2190
2367
  const fetchFn = await getFetch();
2191
2368
  const url = `${apiBase}/project-assistant/mission-status-update`;
2192
2369
  const { resp, body } = await fetchJsonWithTimeout(
@@ -2199,6 +2376,7 @@ async function createMissionStatusUpdate({ apiBase, key, timeoutMs, payload, ide
2199
2376
  Authorization: `Bearer ${key}`,
2200
2377
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
2201
2378
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2379
+ ...writeApprovalHeaders(writeApproval),
2202
2380
  },
2203
2381
  body: JSON.stringify(payload),
2204
2382
  },
@@ -2206,15 +2384,16 @@ async function createMissionStatusUpdate({ apiBase, key, timeoutMs, payload, ide
2206
2384
  );
2207
2385
 
2208
2386
  if (!resp.ok || body.status !== "success") {
2209
- const msg = body?.message || `Mission status update request failed (${resp.status})`;
2387
+ const msg = formatProjectApiErrorMessage(body, `Mission status update request failed (${resp.status})`);
2210
2388
  const err = new Error(msg);
2211
2389
  err.status = resp.status;
2390
+ err.data = body?.data;
2212
2391
  throw err;
2213
2392
  }
2214
2393
  return body.data || {};
2215
2394
  }
2216
2395
 
2217
- async function createMissionArchiveUpdate({ apiBase, key, timeoutMs, endpoint, payload, idempotencyKey, clientSessionId }) {
2396
+ async function createMissionArchiveUpdate({ apiBase, key, timeoutMs, endpoint, payload, idempotencyKey, clientSessionId, writeApproval }) {
2218
2397
  const fetchFn = await getFetch();
2219
2398
  const url = `${apiBase}${endpoint}`;
2220
2399
  const { resp, body } = await fetchJsonWithTimeout(
@@ -2227,6 +2406,7 @@ async function createMissionArchiveUpdate({ apiBase, key, timeoutMs, endpoint, p
2227
2406
  Authorization: `Bearer ${key}`,
2228
2407
  "X-Idempotency-Key": String(idempotencyKey || "").trim(),
2229
2408
  ...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
2409
+ ...writeApprovalHeaders(writeApproval),
2230
2410
  },
2231
2411
  body: JSON.stringify(payload),
2232
2412
  },
@@ -2234,9 +2414,10 @@ async function createMissionArchiveUpdate({ apiBase, key, timeoutMs, endpoint, p
2234
2414
  );
2235
2415
 
2236
2416
  if (!resp.ok || body.status !== "success") {
2237
- const msg = body?.message || `Mission archive request failed (${resp.status})`;
2417
+ const msg = formatProjectApiErrorMessage(body, `Mission archive request failed (${resp.status})`);
2238
2418
  const err = new Error(msg);
2239
2419
  err.status = resp.status;
2420
+ err.data = body?.data;
2240
2421
  throw err;
2241
2422
  }
2242
2423
  return body.data || {};
@@ -2370,6 +2551,12 @@ async function runRunQaqc(args) {
2370
2551
  const apiBase = resolveApiBase(args);
2371
2552
  const shouldWait = Boolean(args.wait || args.sync);
2372
2553
  const shouldSync = Boolean(args.sync);
2554
+ const writeApproval = requireWriteApproval(args, payload, {
2555
+ operation: "run-qaqc",
2556
+ artifactKind: "yaml",
2557
+ targets: missionIds,
2558
+ batchCount: missionIds.length,
2559
+ });
2373
2560
 
2374
2561
  let data;
2375
2562
  try {
@@ -2380,6 +2567,7 @@ async function runRunQaqc(args) {
2380
2567
  payload,
2381
2568
  idempotencyKey,
2382
2569
  clientSessionId,
2570
+ writeApproval,
2383
2571
  });
2384
2572
  } catch (err) {
2385
2573
  if (err?.name === "AbortError") {
@@ -2523,6 +2711,12 @@ async function runMissionStatus(args) {
2523
2711
 
2524
2712
  const timeoutMs = resolveTimeoutMs(args);
2525
2713
  const apiBase = resolveApiBase(args);
2714
+ const writeApproval = requireWriteApproval(args, payload, {
2715
+ operation: "mission-status-update",
2716
+ artifactKind: "yaml",
2717
+ targets: missionIds,
2718
+ batchCount: missionIds.length,
2719
+ });
2526
2720
 
2527
2721
  let data;
2528
2722
  try {
@@ -2533,6 +2727,7 @@ async function runMissionStatus(args) {
2533
2727
  payload,
2534
2728
  idempotencyKey,
2535
2729
  clientSessionId,
2730
+ writeApproval,
2536
2731
  });
2537
2732
  } catch (err) {
2538
2733
  if (err?.name === "AbortError") {
@@ -2630,6 +2825,12 @@ async function runMissionArchiveCommand(args) {
2630
2825
  const timeoutMs = resolveTimeoutMs(args);
2631
2826
  const apiBase = resolveApiBase(args);
2632
2827
  const endpoint = "/project-assistant/mission-archive";
2828
+ const writeApproval = requireWriteApproval(args, payload, {
2829
+ operation: "mission-archive",
2830
+ artifactKind: "yaml",
2831
+ targets: missionIds,
2832
+ batchCount: missionIds.length,
2833
+ });
2633
2834
 
2634
2835
  let data;
2635
2836
  try {
@@ -2641,6 +2842,7 @@ async function runMissionArchiveCommand(args) {
2641
2842
  payload,
2642
2843
  idempotencyKey,
2643
2844
  clientSessionId,
2845
+ writeApproval,
2644
2846
  });
2645
2847
  } catch (err) {
2646
2848
  if (err?.name === "AbortError") {
@@ -2740,6 +2942,10 @@ async function runUpdateTeam(args) {
2740
2942
  operation: "update_team",
2741
2943
  payload,
2742
2944
  });
2945
+ const writeApproval = requireWriteApproval(args, payload, {
2946
+ operation: "update_team",
2947
+ artifactKind: "markdown",
2948
+ });
2743
2949
 
2744
2950
  let data;
2745
2951
  try {
@@ -2751,6 +2957,7 @@ async function runUpdateTeam(args) {
2751
2957
  endpoint: "/project-assistant/project-comment",
2752
2958
  idempotencyKey,
2753
2959
  clientSessionId,
2960
+ writeApproval,
2754
2961
  });
2755
2962
  } catch (err) {
2756
2963
  if (err?.name === "AbortError") {
@@ -2809,6 +3016,10 @@ async function runUpdateOwner(args) {
2809
3016
  operation: "update_owner",
2810
3017
  payload,
2811
3018
  });
3019
+ const writeApproval = requireWriteApproval(args, payload, {
3020
+ operation: "update_owner",
3021
+ artifactKind: "markdown",
3022
+ });
2812
3023
 
2813
3024
  let data;
2814
3025
  try {
@@ -2820,6 +3031,7 @@ async function runUpdateOwner(args) {
2820
3031
  endpoint: "/project-assistant/update-owner",
2821
3032
  idempotencyKey,
2822
3033
  clientSessionId,
3034
+ writeApproval,
2823
3035
  });
2824
3036
  } catch (err) {
2825
3037
  if (err?.name === "AbortError") {
@@ -2887,6 +3099,10 @@ async function runUpdateClient(args) {
2887
3099
  operation: "update_client",
2888
3100
  payload,
2889
3101
  });
3102
+ const writeApproval = requireWriteApproval(args, payload, {
3103
+ operation: "update_client",
3104
+ artifactKind: "markdown",
3105
+ });
2890
3106
 
2891
3107
  let data;
2892
3108
  try {
@@ -2898,6 +3114,7 @@ async function runUpdateClient(args) {
2898
3114
  endpoint: "/project-assistant/client-update-drafts",
2899
3115
  idempotencyKey,
2900
3116
  clientSessionId,
3117
+ writeApproval,
2901
3118
  });
2902
3119
  } catch (err) {
2903
3120
  if (err?.name === "AbortError") {
@@ -2990,20 +3207,48 @@ function stringifyYaml(value) {
2990
3207
  return `${JSON.stringify(value, null, 2)}\n`;
2991
3208
  }
2992
3209
 
2993
- function stableJsonStringify(value) {
2994
- if (Array.isArray(value)) {
2995
- return `[${value.map((item) => stableJsonStringify(item)).join(",")}]`;
3210
+ function stableJsonStringify(value) {
3211
+ if (Array.isArray(value)) {
3212
+ return `[${value.map((item) => stableJsonStringify(item)).join(",")}]`;
2996
3213
  }
2997
3214
  if (value && typeof value === "object") {
2998
3215
  const entries = Object.keys(value)
2999
3216
  .sort()
3000
3217
  .map((key) => `${JSON.stringify(key)}:${stableJsonStringify(value[key])}`);
3001
3218
  return `{${entries.join(",")}}`;
3002
- }
3003
- return JSON.stringify(value);
3004
- }
3005
-
3006
- function parseYaml(text) {
3219
+ }
3220
+ return JSON.stringify(value);
3221
+ }
3222
+
3223
+ function jsonStringifyEnsureAscii(value) {
3224
+ return JSON.stringify(String(value)).replace(/[^\x00-\x7F]/g, (char) => {
3225
+ const code = char.charCodeAt(0);
3226
+ return `\\u${code.toString(16).padStart(4, "0")}`;
3227
+ });
3228
+ }
3229
+
3230
+ function pythonJsonDumpsCompatible(value) {
3231
+ if (Array.isArray(value)) {
3232
+ return `[${value.map((item) => pythonJsonDumpsCompatible(item)).join(", ")}]`;
3233
+ }
3234
+ if (value && typeof value === "object") {
3235
+ const entries = Object.keys(value)
3236
+ .sort()
3237
+ .map((key) => `${jsonStringifyEnsureAscii(key)}: ${pythonJsonDumpsCompatible(value[key])}`);
3238
+ return `{${entries.join(", ")}}`;
3239
+ }
3240
+ if (value === null) return "null";
3241
+ if (typeof value === "boolean") return value ? "true" : "false";
3242
+ if (typeof value === "number") {
3243
+ if (Number.isNaN(value)) return "NaN";
3244
+ if (value === Infinity) return "Infinity";
3245
+ if (value === -Infinity) return "-Infinity";
3246
+ return JSON.stringify(value);
3247
+ }
3248
+ return jsonStringifyEnsureAscii(value);
3249
+ }
3250
+
3251
+ function parseYaml(text) {
3007
3252
  const raw = String(text || "").trim();
3008
3253
  if (!raw) return null;
3009
3254
  if (raw.startsWith("{") || raw.startsWith("[")) {
@@ -4269,7 +4514,7 @@ async function fetchSuggestionsSyncSnapshot({ apiBase, key, timeoutMs, actorScop
4269
4514
  return body.data || {};
4270
4515
  }
4271
4516
 
4272
- async function createProjectSuggestions({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId }) {
4517
+ async function createProjectSuggestions({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId, writeApproval }) {
4273
4518
  return postSuggestionsMutation({
4274
4519
  apiBase,
4275
4520
  key,
@@ -4278,10 +4523,11 @@ async function createProjectSuggestions({ apiBase, key, timeoutMs, payload, idem
4278
4523
  payload,
4279
4524
  idempotencyKey,
4280
4525
  clientSessionId,
4526
+ writeApproval,
4281
4527
  });
4282
4528
  }
4283
4529
 
4284
- async function reviseProjectSuggestions({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId }) {
4530
+ async function reviseProjectSuggestions({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId, writeApproval }) {
4285
4531
  return postSuggestionsMutation({
4286
4532
  apiBase,
4287
4533
  key,
@@ -4290,10 +4536,11 @@ async function reviseProjectSuggestions({ apiBase, key, timeoutMs, payload, idem
4290
4536
  payload,
4291
4537
  idempotencyKey,
4292
4538
  clientSessionId,
4539
+ writeApproval,
4293
4540
  });
4294
4541
  }
4295
4542
 
4296
- async function reviewProjectSuggestions({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId }) {
4543
+ async function reviewProjectSuggestions({ apiBase, key, timeoutMs, payload, idempotencyKey, clientSessionId, writeApproval }) {
4297
4544
  return postSuggestionsMutation({
4298
4545
  apiBase,
4299
4546
  key,
@@ -4302,6 +4549,7 @@ async function reviewProjectSuggestions({ apiBase, key, timeoutMs, payload, idem
4302
4549
  payload,
4303
4550
  idempotencyKey,
4304
4551
  clientSessionId,
4552
+ writeApproval,
4305
4553
  });
4306
4554
  }
4307
4555
 
@@ -4419,6 +4667,11 @@ async function runCreatePrd(args) {
4419
4667
  operation: "create_prd",
4420
4668
  payload,
4421
4669
  });
4670
+ const writeApproval = requireWriteApproval(args, payload, {
4671
+ operation: "create_prd",
4672
+ artifactKind: "markdown",
4673
+ targets: prdApprovalTargets(payload),
4674
+ });
4422
4675
  try {
4423
4676
  data = await postSuggestionsMutation({
4424
4677
  apiBase,
@@ -4428,6 +4681,7 @@ async function runCreatePrd(args) {
4428
4681
  endpoint: "/project-assistant/create-prd",
4429
4682
  idempotencyKey,
4430
4683
  clientSessionId,
4684
+ writeApproval,
4431
4685
  });
4432
4686
  } catch (err) {
4433
4687
  if (err?.name === "AbortError") {
@@ -4478,6 +4732,13 @@ async function runCreatePrd(args) {
4478
4732
  operation: "create_prds",
4479
4733
  payload,
4480
4734
  });
4735
+ const prdTargets = prdApprovalTargets(payload);
4736
+ const writeApproval = requireWriteApproval(args, payload, {
4737
+ operation: "create_prds",
4738
+ artifactKind: "markdown",
4739
+ targets: prdTargets,
4740
+ batchCount: prdTargets.length,
4741
+ });
4481
4742
  const data = await postSuggestionsMutation({
4482
4743
  apiBase,
4483
4744
  key,
@@ -4486,6 +4747,7 @@ async function runCreatePrd(args) {
4486
4747
  endpoint: "/project-assistant/create-prds",
4487
4748
  idempotencyKey,
4488
4749
  clientSessionId,
4750
+ writeApproval,
4489
4751
  });
4490
4752
  aggregated.project_id = data.project_id || null;
4491
4753
  aggregated.created_count = Number(data.created_count || 0);
@@ -5065,6 +5327,13 @@ async function runFeedbackValidateOrApply(args, mode) {
5065
5327
  payload,
5066
5328
  })
5067
5329
  : null;
5330
+ const writeApproval = mode === "apply"
5331
+ ? requireWriteApproval(args, payload, {
5332
+ operation: `feedback_refinement_apply:${feedbackId}`,
5333
+ artifactKind: "yaml",
5334
+ targets: [feedbackId],
5335
+ })
5336
+ : null;
5068
5337
 
5069
5338
  let data;
5070
5339
  try {
@@ -5077,6 +5346,7 @@ async function runFeedbackValidateOrApply(args, mode) {
5077
5346
  payload,
5078
5347
  idempotencyKey,
5079
5348
  clientSessionId,
5349
+ writeApproval,
5080
5350
  });
5081
5351
  } catch (err) {
5082
5352
  if (args.json) {
@@ -5122,33 +5392,33 @@ function resolveFeedbackRequestIdArg(args) {
5122
5392
  return firstNonEmptyString(args["request-id"], args.requestId, args.request_id, args._?.[1]);
5123
5393
  }
5124
5394
 
5125
- function resolveFeedbackEventIdArg(args) {
5126
- return firstNonEmptyString(args["event-id"], args.eventId, args.event_id, args._?.[1]);
5127
- }
5395
+ function resolveFeedbackEventIdArg(args) {
5396
+ return firstNonEmptyString(args["event-id"], args.eventId, args.event_id, args._?.[1]);
5397
+ }
5128
5398
 
5129
- function resolveFeedbackVersionIdArg(args) {
5130
- return firstNonEmptyString(args["version-id"], args.versionId, args.version_id, args._?.[1]);
5131
- }
5132
-
5133
- async function resolveFeedbackCommentContent(args, feedbackId) {
5134
- const explicit = firstNonEmptyString(args.body, args.content, args["body-markdown"], args.bodyMarkdown, args.body_markdown);
5135
- if (explicit) return explicit;
5136
-
5137
- const bodyFile = firstNonEmptyString(args["body-file"], args.bodyFile, args.body_file);
5138
- if (bodyFile) {
5139
- const bodyPath = resolveInputFile(bodyFile, "Feedback comment body");
5140
- return fs.readFileSync(bodyPath, "utf8");
5141
- }
5142
-
5143
- if (args.stdin) {
5144
- return readStdinText();
5145
- }
5146
-
5147
- const positionals = Array.isArray(args._) ? args._.slice(1) : [];
5148
- const feedbackIdFromPositional = positionals[0] && String(positionals[0]) === String(feedbackId);
5149
- const textParts = feedbackIdFromPositional ? positionals.slice(1) : positionals;
5150
- return textParts.join(" ");
5151
- }
5399
+ function resolveFeedbackVersionIdArg(args) {
5400
+ return firstNonEmptyString(args["version-id"], args.versionId, args.version_id, args._?.[1]);
5401
+ }
5402
+
5403
+ async function resolveFeedbackCommentContent(args, feedbackId) {
5404
+ const explicit = firstNonEmptyString(args.body, args.content, args["body-markdown"], args.bodyMarkdown, args.body_markdown);
5405
+ if (explicit) return explicit;
5406
+
5407
+ const bodyFile = firstNonEmptyString(args["body-file"], args.bodyFile, args.body_file);
5408
+ if (bodyFile) {
5409
+ const bodyPath = resolveInputFile(bodyFile, "Feedback comment body");
5410
+ return fs.readFileSync(bodyPath, "utf8");
5411
+ }
5412
+
5413
+ if (args.stdin) {
5414
+ return readStdinText();
5415
+ }
5416
+
5417
+ const positionals = Array.isArray(args._) ? args._.slice(1) : [];
5418
+ const feedbackIdFromPositional = positionals[0] && String(positionals[0]) === String(feedbackId);
5419
+ const textParts = feedbackIdFromPositional ? positionals.slice(1) : positionals;
5420
+ return textParts.join(" ");
5421
+ }
5152
5422
 
5153
5423
  function summarizeFeedbackReviewRequestPayload(data) {
5154
5424
  const request = data?.request || data || {};
@@ -5207,6 +5477,11 @@ async function runFeedbackSubmit(args) {
5207
5477
  operation: `feedback_refinement_request_submit:${feedbackId}`,
5208
5478
  payload,
5209
5479
  });
5480
+ const writeApproval = requireWriteApproval(args, payload, {
5481
+ operation: `feedback_refinement_request_submit:${feedbackId}`,
5482
+ artifactKind: "yaml",
5483
+ targets: [feedbackId],
5484
+ });
5210
5485
 
5211
5486
  let data;
5212
5487
  try {
@@ -5218,6 +5493,7 @@ async function runFeedbackSubmit(args) {
5218
5493
  payload,
5219
5494
  idempotencyKey,
5220
5495
  clientSessionId,
5496
+ writeApproval,
5221
5497
  });
5222
5498
  } catch (err) {
5223
5499
  if (args.json) {
@@ -5273,6 +5549,11 @@ async function runFeedbackRevise(args) {
5273
5549
  operation: `feedback_refinement_request_revise:${requestId}`,
5274
5550
  payload,
5275
5551
  });
5552
+ const writeApproval = requireWriteApproval(args, payload, {
5553
+ operation: `feedback_refinement_request_revise:${requestId}`,
5554
+ artifactKind: "yaml",
5555
+ targets: [requestId],
5556
+ });
5276
5557
 
5277
5558
  let data;
5278
5559
  try {
@@ -5285,6 +5566,7 @@ async function runFeedbackRevise(args) {
5285
5566
  payload,
5286
5567
  idempotencyKey,
5287
5568
  clientSessionId,
5569
+ writeApproval,
5288
5570
  });
5289
5571
  } catch (err) {
5290
5572
  if (args.json) {
@@ -5416,6 +5698,15 @@ async function runFeedbackReviewDecision(args) {
5416
5698
  operation: `feedback_review_batch:${action}:${items.map((item) => item.request_id || item.id || "").join(",")}`,
5417
5699
  payload,
5418
5700
  });
5701
+ const requestTargets = items
5702
+ .map((item, index) => firstNonEmptyString(item.request_id, item.id, item._id) || `index:${index}`)
5703
+ .filter(Boolean);
5704
+ const writeApproval = requireWriteApproval(args, payload, {
5705
+ operation: `feedback_review_batch:${action}`,
5706
+ artifactKind: "yaml",
5707
+ targets: requestTargets,
5708
+ batchCount: items.length,
5709
+ });
5419
5710
 
5420
5711
  let data;
5421
5712
  try {
@@ -5426,6 +5717,7 @@ async function runFeedbackReviewDecision(args) {
5426
5717
  payload,
5427
5718
  idempotencyKey,
5428
5719
  clientSessionId,
5720
+ writeApproval,
5429
5721
  });
5430
5722
  } catch (err) {
5431
5723
  if (args.json) {
@@ -5475,6 +5767,11 @@ async function runFeedbackReviewDecision(args) {
5475
5767
  operation: `feedback_review_request_${operationAction}:${requestId}`,
5476
5768
  payload,
5477
5769
  });
5770
+ const writeApproval = requireWriteApproval(args, payload, {
5771
+ operation: `feedback_review_request_${operationAction}:${requestId}`,
5772
+ artifactKind: "yaml",
5773
+ targets: [requestId],
5774
+ });
5478
5775
 
5479
5776
  let data;
5480
5777
  try {
@@ -5487,6 +5784,7 @@ async function runFeedbackReviewDecision(args) {
5487
5784
  payload,
5488
5785
  idempotencyKey,
5489
5786
  clientSessionId,
5787
+ writeApproval,
5490
5788
  });
5491
5789
  } catch (err) {
5492
5790
  if (args.json) {
@@ -5507,7 +5805,7 @@ async function runFeedbackReviewDecision(args) {
5507
5805
  if (postSync?.error) console.error(`Feedback sync warning: ${postSync.error}`);
5508
5806
  }
5509
5807
 
5510
- async function runFeedbackMove(args) {
5808
+ async function runFeedbackMove(args) {
5511
5809
  const key = getProjectApiKey();
5512
5810
  if (!key) {
5513
5811
  console.error("Missing MYTE_API_KEY (project key) in environment/.env");
@@ -5545,6 +5843,12 @@ async function runFeedbackMove(args) {
5545
5843
  operation: `feedback_board_batch_move:${toState}:${feedbackIds.join(",")}`,
5546
5844
  payload,
5547
5845
  });
5846
+ const writeApproval = requireWriteApproval(args, payload, {
5847
+ operation: `feedback_board_batch_move:${toState}`,
5848
+ artifactKind: "yaml",
5849
+ targets: feedbackIds,
5850
+ batchCount: feedbackIds.length,
5851
+ });
5548
5852
  let data;
5549
5853
  try {
5550
5854
  data = await postFeedbackBatchBoardMutation({
@@ -5554,6 +5858,7 @@ async function runFeedbackMove(args) {
5554
5858
  payload,
5555
5859
  idempotencyKey,
5556
5860
  clientSessionId,
5861
+ writeApproval,
5557
5862
  });
5558
5863
  } catch (err) {
5559
5864
  if (args.json) {
@@ -5581,6 +5886,11 @@ async function runFeedbackMove(args) {
5581
5886
  operation: `feedback_board_move:${feedbackId}`,
5582
5887
  payload,
5583
5888
  });
5889
+ const writeApproval = requireWriteApproval(args, payload, {
5890
+ operation: `feedback_board_move:${feedbackId}`,
5891
+ artifactKind: "yaml",
5892
+ targets: [feedbackId],
5893
+ });
5584
5894
 
5585
5895
  let data;
5586
5896
  try {
@@ -5593,6 +5903,7 @@ async function runFeedbackMove(args) {
5593
5903
  payload,
5594
5904
  idempotencyKey,
5595
5905
  clientSessionId,
5906
+ writeApproval,
5596
5907
  });
5597
5908
  } catch (err) {
5598
5909
  if (args.json) {
@@ -5613,85 +5924,92 @@ async function runFeedbackMove(args) {
5613
5924
  console.log(`State: ${data.feedback_state || toState}`);
5614
5925
  if (data.event?.event_id) console.log(`Event: ${data.event.event_id}`);
5615
5926
  if (postSync?.error) console.error(`Feedback sync warning: ${postSync.error}`);
5616
- }
5617
-
5618
- async function runFeedbackComment(args) {
5619
- const key = getProjectApiKey();
5620
- if (!key) {
5621
- console.error("Missing MYTE_API_KEY (project key) in environment/.env");
5622
- process.exit(1);
5623
- }
5624
- const feedbackId = resolveFeedbackIdArg(args);
5625
- if (!feedbackId) {
5626
- console.error("Missing --feedback-id.");
5627
- process.exit(1);
5628
- }
5629
-
5630
- const content = String(await resolveFeedbackCommentContent(args, feedbackId) || "").trim();
5631
- if (!content) {
5632
- console.error("Missing feedback comment body. Use --body, --body-file, --stdin, or positional text.");
5633
- process.exit(1);
5634
- }
5635
-
5636
- const payload = { content };
5637
- const timeoutMs = resolveTimeoutMs(args);
5638
- const apiBase = resolveApiBase(args);
5639
- const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
5640
- if (clientSessionId) payload.client_session_id = clientSessionId;
5641
- const idempotencyKey = resolveProjectMutationIdempotencyKey({
5642
- args,
5643
- operation: `feedback_comment_create:${feedbackId}`,
5644
- payload,
5645
- });
5646
-
5647
- if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
5648
- console.log(JSON.stringify(payload, null, 2));
5649
- return;
5650
- }
5651
-
5652
- let data;
5653
- try {
5654
- data = await postFeedbackComment({
5655
- apiBase,
5656
- key,
5657
- timeoutMs,
5658
- feedbackId,
5659
- payload,
5660
- idempotencyKey,
5661
- clientSessionId,
5662
- });
5663
- } catch (err) {
5664
- if (args.json) {
5665
- console.log(JSON.stringify({ ok: false, status: err?.status || null, message: err?.message || String(err), data: err?.data || null }, null, 2));
5666
- } else {
5667
- console.error("Feedback comment failed:", err?.message || err);
5668
- if (err?.data) console.error(JSON.stringify(err.data, null, 2));
5669
- }
5670
- process.exit(1);
5671
- }
5672
-
5673
- const postSync = await refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeoutMs });
5674
- if (args.json) {
5675
- console.log(JSON.stringify({ ok: true, ...data, post_sync: postSync }, null, 2));
5676
- return;
5677
- }
5678
-
5679
- if (data.comment_id) console.log(`Comment ID: ${data.comment_id}`);
5680
- console.log(`Feedback: ${data.feedback_id || feedbackId}`);
5681
- if (data.project_id) console.log(`Project ID: ${data.project_id}`);
5682
- if (data.user_name) console.log(`Author: ${data.user_name}`);
5683
- if (data.created_at) console.log(`Created At: ${data.created_at}`);
5684
- if (data.content) {
5685
- const body = String(data.content);
5686
- const truncated = body.length > 2000 ? `${body.slice(0, 2000)}\n...[truncated]` : body;
5687
- console.log("Content:");
5688
- console.log(truncated);
5689
- }
5690
- if (postSync?.data_root) console.log(`Synced feedback: ${postSync.data_root}`);
5691
- if (postSync?.error) console.error(`Feedback sync warning: ${postSync.error}`);
5692
- }
5693
-
5694
- async function runFeedbackUndo(args) {
5927
+ }
5928
+
5929
+ async function runFeedbackComment(args) {
5930
+ const key = getProjectApiKey();
5931
+ if (!key) {
5932
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
5933
+ process.exit(1);
5934
+ }
5935
+ const feedbackId = resolveFeedbackIdArg(args);
5936
+ if (!feedbackId) {
5937
+ console.error("Missing --feedback-id.");
5938
+ process.exit(1);
5939
+ }
5940
+
5941
+ const content = String(await resolveFeedbackCommentContent(args, feedbackId) || "").trim();
5942
+ if (!content) {
5943
+ console.error("Missing feedback comment body. Use --body, --body-file, --stdin, or positional text.");
5944
+ process.exit(1);
5945
+ }
5946
+
5947
+ const payload = { content };
5948
+ const timeoutMs = resolveTimeoutMs(args);
5949
+ const apiBase = resolveApiBase(args);
5950
+ const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
5951
+ if (clientSessionId) payload.client_session_id = clientSessionId;
5952
+ const idempotencyKey = resolveProjectMutationIdempotencyKey({
5953
+ args,
5954
+ operation: `feedback_comment_create:${feedbackId}`,
5955
+ payload,
5956
+ });
5957
+
5958
+ if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
5959
+ console.log(JSON.stringify(payload, null, 2));
5960
+ return;
5961
+ }
5962
+
5963
+ const writeApproval = requireWriteApproval(args, payload, {
5964
+ operation: `feedback_comment_create:${feedbackId}`,
5965
+ artifactKind: "markdown",
5966
+ targets: [feedbackId],
5967
+ });
5968
+
5969
+ let data;
5970
+ try {
5971
+ data = await postFeedbackComment({
5972
+ apiBase,
5973
+ key,
5974
+ timeoutMs,
5975
+ feedbackId,
5976
+ payload,
5977
+ idempotencyKey,
5978
+ clientSessionId,
5979
+ writeApproval,
5980
+ });
5981
+ } catch (err) {
5982
+ if (args.json) {
5983
+ console.log(JSON.stringify({ ok: false, status: err?.status || null, message: err?.message || String(err), data: err?.data || null }, null, 2));
5984
+ } else {
5985
+ console.error("Feedback comment failed:", err?.message || err);
5986
+ if (err?.data) console.error(JSON.stringify(err.data, null, 2));
5987
+ }
5988
+ process.exit(1);
5989
+ }
5990
+
5991
+ const postSync = await refreshFeedbackSnapshotAfterMutation(args, { apiBase, key, timeoutMs });
5992
+ if (args.json) {
5993
+ console.log(JSON.stringify({ ok: true, ...data, post_sync: postSync }, null, 2));
5994
+ return;
5995
+ }
5996
+
5997
+ if (data.comment_id) console.log(`Comment ID: ${data.comment_id}`);
5998
+ console.log(`Feedback: ${data.feedback_id || feedbackId}`);
5999
+ if (data.project_id) console.log(`Project ID: ${data.project_id}`);
6000
+ if (data.user_name) console.log(`Author: ${data.user_name}`);
6001
+ if (data.created_at) console.log(`Created At: ${data.created_at}`);
6002
+ if (data.content) {
6003
+ const body = String(data.content);
6004
+ const truncated = body.length > 2000 ? `${body.slice(0, 2000)}\n...[truncated]` : body;
6005
+ console.log("Content:");
6006
+ console.log(truncated);
6007
+ }
6008
+ if (postSync?.data_root) console.log(`Synced feedback: ${postSync.data_root}`);
6009
+ if (postSync?.error) console.error(`Feedback sync warning: ${postSync.error}`);
6010
+ }
6011
+
6012
+ async function runFeedbackUndo(args) {
5695
6013
  const key = getProjectApiKey();
5696
6014
  if (!key) {
5697
6015
  console.error("Missing MYTE_API_KEY (project key) in environment/.env");
@@ -5717,6 +6035,17 @@ async function runFeedbackUndo(args) {
5717
6035
  payload,
5718
6036
  });
5719
6037
 
6038
+ if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
6039
+ console.log(JSON.stringify(payload, null, 2));
6040
+ return;
6041
+ }
6042
+
6043
+ const writeApproval = requireWriteApproval(args, payload, {
6044
+ operation: `feedback_event_undo:${feedbackId}:${eventId}`,
6045
+ artifactKind: "yaml",
6046
+ targets: [feedbackId, eventId],
6047
+ });
6048
+
5720
6049
  let data;
5721
6050
  try {
5722
6051
  data = await postFeedbackBoardMutation({
@@ -5728,6 +6057,7 @@ async function runFeedbackUndo(args) {
5728
6057
  payload,
5729
6058
  idempotencyKey,
5730
6059
  clientSessionId,
6060
+ writeApproval,
5731
6061
  });
5732
6062
  } catch (err) {
5733
6063
  if (args.json) {
@@ -5886,15 +6216,15 @@ async function runFeedback(args) {
5886
6216
  await runFeedbackReviewDecision(args);
5887
6217
  return;
5888
6218
  }
5889
- if (subcommand === "move") {
5890
- await runFeedbackMove(args);
5891
- return;
5892
- }
5893
- if (subcommand === "comment" || subcommand === "comments") {
5894
- await runFeedbackComment(args);
5895
- return;
5896
- }
5897
- if (subcommand === "undo") {
6219
+ if (subcommand === "move") {
6220
+ await runFeedbackMove(args);
6221
+ return;
6222
+ }
6223
+ if (subcommand === "comment" || subcommand === "comments") {
6224
+ await runFeedbackComment(args);
6225
+ return;
6226
+ }
6227
+ if (subcommand === "undo") {
5898
6228
  await runFeedbackUndo(args);
5899
6229
  return;
5900
6230
  }
@@ -5910,7 +6240,7 @@ async function runFeedback(args) {
5910
6240
  await runFeedbackGetOrHistory(args, subcommand);
5911
6241
  return;
5912
6242
  }
5913
- console.error("Unknown feedback command. Use status, edit, assign, archive, submit, revise, reviews, review, move, comment, undo, prd-versions, prd-diff, get, history, validate, or apply.");
6243
+ console.error("Unknown feedback command. Use status, edit, assign, archive, submit, revise, reviews, review, move, comment, undo, prd-versions, prd-diff, get, history, validate, or apply.");
5914
6244
  process.exit(1);
5915
6245
  }
5916
6246
 
@@ -6106,6 +6436,14 @@ async function runSuggestionsCreate(args) {
6106
6436
  return;
6107
6437
  }
6108
6438
 
6439
+ const createTargets = suggestionApprovalTargets(context.payload);
6440
+ const writeApproval = requireWriteApproval(args, context.payload, {
6441
+ operation: "suggestions.create",
6442
+ artifactKind: "yaml",
6443
+ targets: createTargets,
6444
+ batchCount: createTargets.length > 1 ? createTargets.length : null,
6445
+ });
6446
+
6109
6447
  let data;
6110
6448
  try {
6111
6449
  data = await createProjectSuggestions({
@@ -6115,6 +6453,7 @@ async function runSuggestionsCreate(args) {
6115
6453
  payload: context.payload,
6116
6454
  idempotencyKey: context.idempotencyKey,
6117
6455
  clientSessionId: context.clientSessionId,
6456
+ writeApproval,
6118
6457
  });
6119
6458
  } catch (err) {
6120
6459
  console.error("Suggestions create failed:", err?.message || err);
@@ -6191,6 +6530,14 @@ async function runSuggestionsRevise(args) {
6191
6530
  return;
6192
6531
  }
6193
6532
 
6533
+ const reviseTargets = suggestionApprovalTargets(context.payload);
6534
+ const writeApproval = requireWriteApproval(args, context.payload, {
6535
+ operation: "suggestions.revise",
6536
+ artifactKind: "yaml",
6537
+ targets: reviseTargets,
6538
+ batchCount: reviseTargets.length > 1 ? reviseTargets.length : null,
6539
+ });
6540
+
6194
6541
  let data;
6195
6542
  try {
6196
6543
  data = await reviseProjectSuggestions({
@@ -6200,6 +6547,7 @@ async function runSuggestionsRevise(args) {
6200
6547
  payload: context.payload,
6201
6548
  idempotencyKey: context.idempotencyKey,
6202
6549
  clientSessionId: context.clientSessionId,
6550
+ writeApproval,
6203
6551
  });
6204
6552
  } catch (err) {
6205
6553
  console.error("Suggestions revise failed:", err?.message || err);
@@ -6274,6 +6622,14 @@ async function runSuggestionsReview(args) {
6274
6622
  return;
6275
6623
  }
6276
6624
 
6625
+ const reviewTargets = suggestionApprovalTargets(context.payload);
6626
+ const writeApproval = requireWriteApproval(args, context.payload, {
6627
+ operation: "suggestions.review",
6628
+ artifactKind: "yaml",
6629
+ targets: reviewTargets,
6630
+ batchCount: reviewTargets.length > 1 ? reviewTargets.length : null,
6631
+ });
6632
+
6277
6633
  let data;
6278
6634
  try {
6279
6635
  data = await reviewProjectSuggestions({
@@ -6283,6 +6639,7 @@ async function runSuggestionsReview(args) {
6283
6639
  payload: context.payload,
6284
6640
  idempotencyKey: context.idempotencyKey,
6285
6641
  clientSessionId: context.clientSessionId,
6642
+ writeApproval,
6286
6643
  });
6287
6644
  } catch (err) {
6288
6645
  console.error("Suggestions review failed:", err?.message || err);