@mytegroupinc/myte-core 0.0.20 → 0.0.22
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 +1 -0
- package/cli.js +544 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,5 +19,6 @@ This package exists so the public wrapper can stay small and versioned cleanly.
|
|
|
19
19
|
## Behavior Summary
|
|
20
20
|
|
|
21
21
|
- Snapshot-style commands such as `bootstrap`, `sync-qaqc`, `feedback-sync`, and `suggestions sync` write local `MyteCommandCenter` data.
|
|
22
|
+
- `feedback status|edit|assign|archive` writes reviewable local YAML artifacts, while `feedback validate|apply` sends those artifacts to the backend so business rules stay server-side.
|
|
22
23
|
- `query --with-diff` requires project repos to be configured for diff collection and fails fast when no matching local project repo can be resolved.
|
|
23
24
|
- Public package documentation is intentionally minimal. Internal rollout and design notes are not part of the npm package contract.
|
package/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
} = require("./lib/ai-gateway");
|
|
23
23
|
|
|
24
24
|
const DEFAULT_API_BASE = "https://api.myte.dev";
|
|
25
|
+
const DEFAULT_DIFF_LIMIT_CHARS = 500_000;
|
|
25
26
|
const REMOVED_COMMAND_MESSAGES = {
|
|
26
27
|
ask: "The `ask` alias has been removed. Use `myte query \"...\"`.",
|
|
27
28
|
chat: "The `chat` command has been removed. Use repeated `myte query` calls instead.",
|
|
@@ -98,6 +99,7 @@ function splitCommand(argv) {
|
|
|
98
99
|
"update-owner",
|
|
99
100
|
"update-client",
|
|
100
101
|
"feedback-sync",
|
|
102
|
+
"feedback",
|
|
101
103
|
"help",
|
|
102
104
|
"--help",
|
|
103
105
|
"-h",
|
|
@@ -141,6 +143,17 @@ function parseArgs(argv) {
|
|
|
141
143
|
"target-contact-ids",
|
|
142
144
|
"status",
|
|
143
145
|
"source",
|
|
146
|
+
"feedback-id",
|
|
147
|
+
"reason",
|
|
148
|
+
"review-action",
|
|
149
|
+
"idempotency-key",
|
|
150
|
+
"user-id",
|
|
151
|
+
"assigned-user-id",
|
|
152
|
+
"due-date",
|
|
153
|
+
"priority",
|
|
154
|
+
"review-note",
|
|
155
|
+
"tags",
|
|
156
|
+
"tag",
|
|
144
157
|
"mission-ids",
|
|
145
158
|
"client-session-id",
|
|
146
159
|
],
|
|
@@ -212,6 +225,10 @@ function printHelp() {
|
|
|
212
225
|
" myte update-owner --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--json]",
|
|
213
226
|
" myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] [--json]",
|
|
214
227
|
" myte feedback-sync [--status <value>] [--source <value>] [--with-prd-text|--no-with-prd-text] [--output-dir ./MyteCommandCenter] [--json]",
|
|
228
|
+
" myte feedback status --feedback-id <id> --status todo|in_progress|in_review|completed|deployed|rejected|archived --reason \"...\"",
|
|
229
|
+
" myte feedback edit --feedback-id <id> [--title \"...\"] [--feedback-text \"...\"] [--priority High] [--reason \"...\"]",
|
|
230
|
+
" myte feedback validate --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
|
|
231
|
+
" myte feedback apply --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
|
|
215
232
|
" myte create-prd <file.md> [more.md ...] [--json] [--title \"...\"] [--description \"...\"]",
|
|
216
233
|
" cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
|
|
217
234
|
" cat update.md | myte update-owner --stdin --subject \"Owner update\"",
|
|
@@ -290,9 +307,15 @@ function printHelp() {
|
|
|
290
307
|
" - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
|
|
291
308
|
" - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
|
|
292
309
|
"",
|
|
310
|
+
"feedback review contract:",
|
|
311
|
+
" - Draft commands write review artifacts under MyteCommandCenter/reviews/feedback/*.yml for local IDE diff review",
|
|
312
|
+
" - validate/apply send those artifacts to /api/project-assistant/feedback/<id>/refinement/*",
|
|
313
|
+
" - The backend owns authorization, stale snapshot checks, allowed field/transition rules, and history",
|
|
314
|
+
" - apply is idempotent and does not rewrite local feedback.yml; run feedback-sync after apply to refresh local state",
|
|
315
|
+
"",
|
|
293
316
|
"Options:",
|
|
294
317
|
" --with-diff Include deterministic git diffs (project-scoped; fails fast if no project repos are configured or resolved)",
|
|
295
|
-
" --diff-limit <chars> Truncate diff context to N chars (default:
|
|
318
|
+
" --diff-limit <chars> Truncate diff context to N chars (default: 500000)",
|
|
296
319
|
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
297
320
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
298
321
|
" --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
|
|
@@ -310,8 +333,15 @@ function printHelp() {
|
|
|
310
333
|
" --body-file <path> Read update-owner or update-client markdown body from a file",
|
|
311
334
|
" --target-contact-id Add one client contact ObjectId (repeatable)",
|
|
312
335
|
" --target-contact-ids Comma-separated client contact ObjectIds",
|
|
313
|
-
" --
|
|
336
|
+
" --feedback-id <id> Feedback ObjectId for feedback review commands",
|
|
337
|
+
" --reason <text> Human review reason included in feedback refinement artifacts",
|
|
338
|
+
" --status <value> Mission status target, feedback-sync filter, or feedback review state depending on command",
|
|
314
339
|
" --source <value> Feedback source filter for feedback-sync",
|
|
340
|
+
" --priority <value> Feedback review priority target: Low, Medium, or High",
|
|
341
|
+
" --user-id <id> Feedback assignee user id for `myte feedback assign`",
|
|
342
|
+
" --assigned-user-id Alias for --user-id in feedback assign",
|
|
343
|
+
" --due-date <date> Feedback due date target for edit artifacts",
|
|
344
|
+
" --review-note <text> Review note stored in feedback refinement history",
|
|
315
345
|
" --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
|
|
316
346
|
" --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
|
|
317
347
|
" --mission-ids <ids> Comma-separated mission business ids for run-qaqc or mission status (quote multi-id values on PowerShell)",
|
|
@@ -340,6 +370,9 @@ function printHelp() {
|
|
|
340
370
|
" myte update-owner --subject \"QAQC progress\" --body-file ./updates/owner.md",
|
|
341
371
|
" myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md",
|
|
342
372
|
" myte feedback-sync --json",
|
|
373
|
+
" myte feedback status --feedback-id 507f1f77bcf86cd799439011 --status in_review --reason \"Ready for owner review\"",
|
|
374
|
+
" myte feedback validate --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
|
|
375
|
+
" myte feedback apply --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
|
|
343
376
|
" myte suggestions create --file ./suggestions/create.yml",
|
|
344
377
|
" myte suggestions revise",
|
|
345
378
|
" myte suggestions review",
|
|
@@ -572,6 +605,8 @@ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
|
|
|
572
605
|
} catch {
|
|
573
606
|
const err = new Error(`Non-JSON response (${resp.status}): ${text.slice(0, 500)}`);
|
|
574
607
|
err.status = resp.status;
|
|
608
|
+
const retryAfter = resp.headers?.get?.("retry-after");
|
|
609
|
+
if (retryAfter) err.retryAfter = retryAfter;
|
|
575
610
|
throw err;
|
|
576
611
|
}
|
|
577
612
|
return { resp, body };
|
|
@@ -1079,21 +1114,35 @@ function resolveConfiguredRepoBindings(bindings) {
|
|
|
1079
1114
|
|
|
1080
1115
|
const cwd = process.cwd();
|
|
1081
1116
|
const currentRepoRoot = findGitTopLevel(cwd);
|
|
1082
|
-
const
|
|
1117
|
+
const searchRoots = [];
|
|
1118
|
+
const seenRoots = new Set();
|
|
1119
|
+
const pushSearchRoot = (value) => {
|
|
1120
|
+
if (!value) return;
|
|
1121
|
+
const resolved = path.resolve(value);
|
|
1122
|
+
if (seenRoots.has(resolved)) return;
|
|
1123
|
+
seenRoots.add(resolved);
|
|
1124
|
+
searchRoots.push(resolved);
|
|
1125
|
+
};
|
|
1083
1126
|
|
|
1084
|
-
|
|
1127
|
+
// Prefer the invocation workspace first. A wrapper repo can contain the
|
|
1128
|
+
// configured API/WEB repos as children, so jumping immediately to the parent
|
|
1129
|
+
// can miss the project repos entirely.
|
|
1130
|
+
pushSearchRoot(cwd);
|
|
1131
|
+
if (currentRepoRoot) pushSearchRoot(currentRepoRoot);
|
|
1132
|
+
|
|
1133
|
+
const scanStart = currentRepoRoot ? path.dirname(currentRepoRoot) : path.dirname(cwd);
|
|
1085
1134
|
let cur = scanStart;
|
|
1086
1135
|
for (let i = 0; i < 8; i += 1) {
|
|
1087
|
-
|
|
1136
|
+
pushSearchRoot(cur);
|
|
1088
1137
|
const parent = path.dirname(cur);
|
|
1089
1138
|
if (parent === cur) break;
|
|
1090
1139
|
cur = parent;
|
|
1091
1140
|
}
|
|
1092
1141
|
|
|
1093
|
-
let fallbackCandidates =
|
|
1094
|
-
let fallbackRoot =
|
|
1142
|
+
let fallbackCandidates = [];
|
|
1143
|
+
let fallbackRoot = null;
|
|
1095
1144
|
|
|
1096
|
-
for (const candidateRoot of
|
|
1145
|
+
for (const candidateRoot of searchRoots) {
|
|
1097
1146
|
const candidates = listGitRepoCandidates(candidateRoot, currentRepoRoot);
|
|
1098
1147
|
if (!fallbackCandidates.length && candidates.length) {
|
|
1099
1148
|
fallbackCandidates = candidates;
|
|
@@ -1585,6 +1634,83 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
|
|
|
1585
1634
|
return body.data || {};
|
|
1586
1635
|
}
|
|
1587
1636
|
|
|
1637
|
+
async function fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId }) {
|
|
1638
|
+
const fetchFn = await getFetch();
|
|
1639
|
+
const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}`;
|
|
1640
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
1641
|
+
fetchFn,
|
|
1642
|
+
url,
|
|
1643
|
+
{
|
|
1644
|
+
method: "GET",
|
|
1645
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
1646
|
+
},
|
|
1647
|
+
timeoutMs
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
if (!resp.ok || body.status !== "success") {
|
|
1651
|
+
const msg = body?.message || `Feedback review request failed (${resp.status})`;
|
|
1652
|
+
const err = new Error(msg);
|
|
1653
|
+
err.status = resp.status;
|
|
1654
|
+
throw err;
|
|
1655
|
+
}
|
|
1656
|
+
return body.data || {};
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
async function fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId }) {
|
|
1660
|
+
const fetchFn = await getFetch();
|
|
1661
|
+
const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/history`;
|
|
1662
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
1663
|
+
fetchFn,
|
|
1664
|
+
url,
|
|
1665
|
+
{
|
|
1666
|
+
method: "GET",
|
|
1667
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
1668
|
+
},
|
|
1669
|
+
timeoutMs
|
|
1670
|
+
);
|
|
1671
|
+
|
|
1672
|
+
if (!resp.ok || body.status !== "success") {
|
|
1673
|
+
const msg = body?.message || `Feedback history request failed (${resp.status})`;
|
|
1674
|
+
const err = new Error(msg);
|
|
1675
|
+
err.status = resp.status;
|
|
1676
|
+
throw err;
|
|
1677
|
+
}
|
|
1678
|
+
return body.data || {};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
async function postFeedbackRefinement({ apiBase, key, timeoutMs, feedbackId, mode, payload, idempotencyKey, clientSessionId }) {
|
|
1682
|
+
const fetchFn = await getFetch();
|
|
1683
|
+
const action = String(mode || "").trim();
|
|
1684
|
+
const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/${action}`;
|
|
1685
|
+
const headers = {
|
|
1686
|
+
"Content-Type": "application/json",
|
|
1687
|
+
Authorization: `Bearer ${key}`,
|
|
1688
|
+
...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
|
|
1689
|
+
};
|
|
1690
|
+
if (idempotencyKey) {
|
|
1691
|
+
headers["X-Idempotency-Key"] = String(idempotencyKey).trim();
|
|
1692
|
+
}
|
|
1693
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
1694
|
+
fetchFn,
|
|
1695
|
+
url,
|
|
1696
|
+
{
|
|
1697
|
+
method: "POST",
|
|
1698
|
+
headers,
|
|
1699
|
+
body: JSON.stringify(payload || {}),
|
|
1700
|
+
},
|
|
1701
|
+
timeoutMs
|
|
1702
|
+
);
|
|
1703
|
+
|
|
1704
|
+
if (!resp.ok || body.status !== "success") {
|
|
1705
|
+
const msg = body?.message || `Feedback refinement ${action} failed (${resp.status})`;
|
|
1706
|
+
const err = new Error(msg);
|
|
1707
|
+
err.status = resp.status;
|
|
1708
|
+
err.data = body?.data;
|
|
1709
|
+
throw err;
|
|
1710
|
+
}
|
|
1711
|
+
return body.data || {};
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1588
1714
|
async function fetchSuggestionsSyncSnapshot({ apiBase, key, timeoutMs, actorScope = "" }) {
|
|
1589
1715
|
const fetchFn = await getFetch();
|
|
1590
1716
|
const url = new URL(`${apiBase}/project-assistant/suggestions`);
|
|
@@ -1758,6 +1884,10 @@ function resolveRetryAfterMs(err, fallbackMs = 5_000) {
|
|
|
1758
1884
|
return fallbackMs;
|
|
1759
1885
|
}
|
|
1760
1886
|
|
|
1887
|
+
function isTransientQueryStatusError(err) {
|
|
1888
|
+
return [408, 429, 500, 502, 503, 504].includes(Number(err?.status));
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1761
1891
|
async function createAssistantQueryJob({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
|
|
1762
1892
|
const fetchFn = await getFetch();
|
|
1763
1893
|
const url = `${apiBase}${endpoint}`;
|
|
@@ -2756,6 +2886,150 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
2756
2886
|
};
|
|
2757
2887
|
}
|
|
2758
2888
|
|
|
2889
|
+
function resolveFeedbackReviewPaths(args, { requireFeedback = false } = {}) {
|
|
2890
|
+
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
2891
|
+
const targetRoot = outputDir
|
|
2892
|
+
? path.resolve(process.cwd(), String(outputDir))
|
|
2893
|
+
: findExistingCommandCenterRoot(process.cwd());
|
|
2894
|
+
if (!targetRoot) {
|
|
2895
|
+
if (requireFeedback) {
|
|
2896
|
+
console.error("feedback.yml was not found. Run `myte feedback-sync` first or pass --output-dir.");
|
|
2897
|
+
process.exit(1);
|
|
2898
|
+
}
|
|
2899
|
+
return null;
|
|
2900
|
+
}
|
|
2901
|
+
const dataRoot = path.join(targetRoot, "data");
|
|
2902
|
+
const feedbackPath = path.join(dataRoot, "feedback.yml");
|
|
2903
|
+
if (requireFeedback && !fs.existsSync(feedbackPath)) {
|
|
2904
|
+
console.error("feedback.yml was not found. Run `myte feedback-sync` first or pass --output-dir.");
|
|
2905
|
+
process.exit(1);
|
|
2906
|
+
}
|
|
2907
|
+
return {
|
|
2908
|
+
targetRoot,
|
|
2909
|
+
dataRoot,
|
|
2910
|
+
feedbackPath,
|
|
2911
|
+
reviewsRoot: path.join(targetRoot, "reviews", "feedback"),
|
|
2912
|
+
wrapperRoot: path.dirname(targetRoot),
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
function readFeedbackManifest(args) {
|
|
2917
|
+
const paths = resolveFeedbackReviewPaths(args, { requireFeedback: true });
|
|
2918
|
+
const manifest = readYamlFile(paths.feedbackPath) || {};
|
|
2919
|
+
if (!isPlainObject(manifest)) {
|
|
2920
|
+
console.error(`Invalid feedback manifest: ${paths.feedbackPath}`);
|
|
2921
|
+
process.exit(1);
|
|
2922
|
+
}
|
|
2923
|
+
return { paths, manifest };
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function findFeedbackManifestItem(manifest, feedbackId) {
|
|
2927
|
+
const wanted = String(feedbackId || "").trim();
|
|
2928
|
+
if (!wanted) return null;
|
|
2929
|
+
const items = Array.isArray(manifest?.items) ? manifest.items : [];
|
|
2930
|
+
return items.find((item) => String(item?.feedback_id || item?.id || "").trim() === wanted) || null;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
function readFeedbackRefinementArtifact(args) {
|
|
2934
|
+
const filePath = firstNonEmptyString(args.file);
|
|
2935
|
+
if (!filePath) {
|
|
2936
|
+
console.error("Missing --file for feedback validate/apply.");
|
|
2937
|
+
process.exit(1);
|
|
2938
|
+
}
|
|
2939
|
+
const absPath = resolveInputFile(filePath, "Feedback refinement");
|
|
2940
|
+
const text = fs.readFileSync(absPath, "utf8");
|
|
2941
|
+
let payload;
|
|
2942
|
+
if (absPath.toLowerCase().endsWith(".json")) {
|
|
2943
|
+
payload = JSON.parse(text);
|
|
2944
|
+
} else {
|
|
2945
|
+
payload = parseYaml(text);
|
|
2946
|
+
}
|
|
2947
|
+
if (!isPlainObject(payload)) {
|
|
2948
|
+
console.error(`Feedback refinement file must contain an object: ${absPath}`);
|
|
2949
|
+
process.exit(1);
|
|
2950
|
+
}
|
|
2951
|
+
return { absPath, payload };
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
function parseFeedbackTags(args) {
|
|
2955
|
+
const values = parseCsvValues(args.tags, args.tag);
|
|
2956
|
+
return values.length ? values : undefined;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
function feedbackChange(from, to) {
|
|
2960
|
+
return { from: from === undefined ? null : from, to: to === undefined ? null : to };
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
|
|
2964
|
+
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
2965
|
+
const artifact = {
|
|
2966
|
+
schema_version: 1,
|
|
2967
|
+
kind: "feedback_refinement",
|
|
2968
|
+
project_id: firstNonEmptyString(manifest?.project?.id, item?.project_id),
|
|
2969
|
+
feedback_id: feedbackId,
|
|
2970
|
+
title_snapshot: firstNonEmptyString(item?.title),
|
|
2971
|
+
base_snapshot_hash: firstNonEmptyString(item?.snapshot_hash),
|
|
2972
|
+
base_updated_at: firstNonEmptyString(item?.updated_at),
|
|
2973
|
+
review_action: action,
|
|
2974
|
+
reason: reason || null,
|
|
2975
|
+
changes,
|
|
2976
|
+
force: Boolean(args.force) || undefined,
|
|
2977
|
+
client_session_id: clientSessionId || undefined,
|
|
2978
|
+
created_at: new Date().toISOString(),
|
|
2979
|
+
};
|
|
2980
|
+
return Object.fromEntries(Object.entries(artifact).filter(([, value]) => value !== undefined));
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function writeFeedbackRefinementArtifact({ paths, artifact, action }) {
|
|
2984
|
+
const feedbackId = sanitizeFileSegment(artifact.feedback_id, "feedback");
|
|
2985
|
+
const actionSegment = sanitizeFileSegment(action || artifact.review_action || "refine", "refine");
|
|
2986
|
+
const filePath = path.join(paths.reviewsRoot, `${feedbackId}-${actionSegment}.yml`);
|
|
2987
|
+
writeYamlFile(filePath, artifact);
|
|
2988
|
+
return filePath;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function printFeedbackRefinementDraft({ artifact, filePath, args }) {
|
|
2992
|
+
const output = {
|
|
2993
|
+
feedback_id: artifact.feedback_id,
|
|
2994
|
+
review_action: artifact.review_action,
|
|
2995
|
+
artifact_path: filePath || null,
|
|
2996
|
+
base_snapshot_hash: artifact.base_snapshot_hash || null,
|
|
2997
|
+
changes: artifact.changes || {},
|
|
2998
|
+
reason: artifact.reason || null,
|
|
2999
|
+
};
|
|
3000
|
+
if (args.json) {
|
|
3001
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
console.log(`Feedback: ${output.feedback_id}`);
|
|
3005
|
+
console.log(`Action: ${output.review_action}`);
|
|
3006
|
+
if (filePath) console.log(`Artifact: ${filePath}`);
|
|
3007
|
+
console.log(`Base Snapshot: ${output.base_snapshot_hash || "n/a"}`);
|
|
3008
|
+
console.log("Changes:");
|
|
3009
|
+
for (const [field, change] of Object.entries(output.changes || {})) {
|
|
3010
|
+
const fromValue = change && typeof change === "object" && "from" in change ? change.from : null;
|
|
3011
|
+
const toValue = change && typeof change === "object" && "to" in change ? change.to : change;
|
|
3012
|
+
console.log(`- ${field}: ${JSON.stringify(fromValue)} -> ${JSON.stringify(toValue)}`);
|
|
3013
|
+
}
|
|
3014
|
+
if (output.reason) console.log(`Reason: ${output.reason}`);
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
function summarizeFeedbackRefinementResult(data) {
|
|
3018
|
+
return {
|
|
3019
|
+
valid: Boolean(data?.valid),
|
|
3020
|
+
feedback_id: data?.feedback_id || data?.feedback?.feedback_id || null,
|
|
3021
|
+
project_id: data?.project_id || null,
|
|
3022
|
+
previous_snapshot_hash: data?.previous_snapshot_hash || data?.base_snapshot_hash || null,
|
|
3023
|
+
current_snapshot_hash: data?.current_snapshot_hash || null,
|
|
3024
|
+
new_snapshot_hash: data?.new_snapshot_hash || data?.feedback?.snapshot_hash || null,
|
|
3025
|
+
diff_count: Array.isArray(data?.diffs) ? data.diffs.length : 0,
|
|
3026
|
+
diffs: data?.diffs || [],
|
|
3027
|
+
warnings: data?.warnings || [],
|
|
3028
|
+
errors: data?.errors || [],
|
|
3029
|
+
history_id: data?.history?.history_id || null,
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
|
|
2759
3033
|
function missionOpsThreads(payload) {
|
|
2760
3034
|
if (Array.isArray(payload?.threads)) return payload.threads;
|
|
2761
3035
|
if (Array.isArray(payload?.suggestions)) return payload.suggestions;
|
|
@@ -3929,6 +4203,251 @@ async function runFeedbackSync(args) {
|
|
|
3929
4203
|
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
3930
4204
|
}
|
|
3931
4205
|
|
|
4206
|
+
function resolveFeedbackIdArg(args) {
|
|
4207
|
+
return firstNonEmptyString(args["feedback-id"], args.feedbackId, args.feedback_id, args._?.[1]);
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
function requireFeedbackDraftContext(args) {
|
|
4211
|
+
const feedbackId = resolveFeedbackIdArg(args);
|
|
4212
|
+
if (!feedbackId) {
|
|
4213
|
+
console.error("Missing --feedback-id.");
|
|
4214
|
+
process.exit(1);
|
|
4215
|
+
}
|
|
4216
|
+
const { paths, manifest } = readFeedbackManifest(args);
|
|
4217
|
+
const item = findFeedbackManifestItem(manifest, feedbackId);
|
|
4218
|
+
if (!item) {
|
|
4219
|
+
console.error(`Feedback item not found in feedback.yml: ${feedbackId}. Run \`myte feedback-sync\` first.`);
|
|
4220
|
+
process.exit(1);
|
|
4221
|
+
}
|
|
4222
|
+
return { paths, manifest, item, feedbackId };
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
async function buildFeedbackDraftForCommand(args, subcommand) {
|
|
4226
|
+
const { paths, manifest, item, feedbackId } = requireFeedbackDraftContext(args);
|
|
4227
|
+
const action = subcommand || "refine";
|
|
4228
|
+
const reason = firstNonEmptyString(args.reason);
|
|
4229
|
+
const changes = {};
|
|
4230
|
+
|
|
4231
|
+
if (action === "status") {
|
|
4232
|
+
const targetStatus = firstNonEmptyString(args.status, args._?.[2]);
|
|
4233
|
+
if (!targetStatus) {
|
|
4234
|
+
console.error("Missing --status for feedback status.");
|
|
4235
|
+
process.exit(1);
|
|
4236
|
+
}
|
|
4237
|
+
changes.feedback_state = feedbackChange(firstNonEmptyString(item.feedback_state, item.status), targetStatus);
|
|
4238
|
+
} else if (action === "archive") {
|
|
4239
|
+
changes.feedback_state = feedbackChange(firstNonEmptyString(item.feedback_state, item.status), "archived");
|
|
4240
|
+
} else if (action === "assign") {
|
|
4241
|
+
const assignee = firstNonEmptyString(args["assigned-user-id"], args.assignedUserId, args.assigned_user_id, args["user-id"], args.userId, args.user_id, args._?.[2]);
|
|
4242
|
+
if (!assignee) {
|
|
4243
|
+
console.error("Missing --user-id or --assigned-user-id for feedback assign.");
|
|
4244
|
+
process.exit(1);
|
|
4245
|
+
}
|
|
4246
|
+
changes.assigned_user_id = feedbackChange(firstNonEmptyString(item.assigned_user_id), assignee);
|
|
4247
|
+
} else if (action === "edit" || action === "refine") {
|
|
4248
|
+
const title = firstNonEmptyString(args.title);
|
|
4249
|
+
if (title) changes.title = feedbackChange(firstNonEmptyString(item.title), title);
|
|
4250
|
+
|
|
4251
|
+
let feedbackText = firstNonEmptyString(args["feedback-text"], args.feedbackText, args.feedback_text);
|
|
4252
|
+
const bodyFile = firstNonEmptyString(args["body-file"], args.bodyFile, args.body_file);
|
|
4253
|
+
if (!feedbackText && bodyFile) {
|
|
4254
|
+
const bodyPath = resolveInputFile(bodyFile, "Feedback body");
|
|
4255
|
+
feedbackText = fs.readFileSync(bodyPath, "utf8").trim();
|
|
4256
|
+
}
|
|
4257
|
+
if (feedbackText) changes.feedback_text = feedbackChange(firstNonEmptyString(item.feedback_text), feedbackText);
|
|
4258
|
+
|
|
4259
|
+
const priority = firstNonEmptyString(args.priority);
|
|
4260
|
+
if (priority) changes.priority = feedbackChange(firstNonEmptyString(item.priority), priority);
|
|
4261
|
+
|
|
4262
|
+
const dueDate = firstNonEmptyString(args["due-date"], args.dueDate, args.due_date);
|
|
4263
|
+
if (dueDate) changes.due_date = feedbackChange(firstNonEmptyString(item.due_date, item.due_date_text), dueDate);
|
|
4264
|
+
|
|
4265
|
+
const tags = parseFeedbackTags(args);
|
|
4266
|
+
if (tags) changes.tags = feedbackChange(Array.isArray(item.tags) ? item.tags : [], tags);
|
|
4267
|
+
|
|
4268
|
+
const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
|
|
4269
|
+
if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
|
|
4270
|
+
} else {
|
|
4271
|
+
console.error("Unknown feedback command. Use status, edit, assign, archive, get, history, validate, or apply.");
|
|
4272
|
+
process.exit(1);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
if (!Object.keys(changes).length) {
|
|
4276
|
+
console.error("No feedback changes were provided.");
|
|
4277
|
+
process.exit(1);
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
const artifact = buildFeedbackRefinementArtifact({
|
|
4281
|
+
manifest,
|
|
4282
|
+
item,
|
|
4283
|
+
feedbackId,
|
|
4284
|
+
action,
|
|
4285
|
+
changes,
|
|
4286
|
+
reason,
|
|
4287
|
+
args,
|
|
4288
|
+
});
|
|
4289
|
+
|
|
4290
|
+
if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
|
|
4291
|
+
if (args.json) {
|
|
4292
|
+
console.log(JSON.stringify(artifact, null, 2));
|
|
4293
|
+
} else {
|
|
4294
|
+
console.log(stringifyYaml(artifact));
|
|
4295
|
+
}
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
const filePath = writeFeedbackRefinementArtifact({ paths, artifact, action });
|
|
4300
|
+
printFeedbackRefinementDraft({ artifact, filePath, args });
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
async function runFeedbackValidateOrApply(args, mode) {
|
|
4304
|
+
const key = getProjectApiKey();
|
|
4305
|
+
if (!key) {
|
|
4306
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
4307
|
+
process.exit(1);
|
|
4308
|
+
}
|
|
4309
|
+
const { absPath, payload } = readFeedbackRefinementArtifact(args);
|
|
4310
|
+
const feedbackId = firstNonEmptyString(args["feedback-id"], args.feedbackId, args.feedback_id, payload.feedback_id);
|
|
4311
|
+
if (!feedbackId) {
|
|
4312
|
+
console.error("Feedback refinement artifact is missing feedback_id.");
|
|
4313
|
+
process.exit(1);
|
|
4314
|
+
}
|
|
4315
|
+
if (args.force) payload.force = true;
|
|
4316
|
+
|
|
4317
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
4318
|
+
const apiBase = resolveApiBase(args);
|
|
4319
|
+
const clientSessionId = firstNonEmptyString(
|
|
4320
|
+
args["client-session-id"],
|
|
4321
|
+
args.clientSessionId,
|
|
4322
|
+
args.client_session_id,
|
|
4323
|
+
payload.client_session_id
|
|
4324
|
+
);
|
|
4325
|
+
const idempotencyKey = mode === "apply"
|
|
4326
|
+
? resolveProjectMutationIdempotencyKey({
|
|
4327
|
+
args,
|
|
4328
|
+
operation: `feedback_refinement_apply:${feedbackId}`,
|
|
4329
|
+
payload,
|
|
4330
|
+
})
|
|
4331
|
+
: null;
|
|
4332
|
+
|
|
4333
|
+
let data;
|
|
4334
|
+
try {
|
|
4335
|
+
data = await postFeedbackRefinement({
|
|
4336
|
+
apiBase,
|
|
4337
|
+
key,
|
|
4338
|
+
timeoutMs,
|
|
4339
|
+
feedbackId,
|
|
4340
|
+
mode,
|
|
4341
|
+
payload,
|
|
4342
|
+
idempotencyKey,
|
|
4343
|
+
clientSessionId,
|
|
4344
|
+
});
|
|
4345
|
+
} catch (err) {
|
|
4346
|
+
if (args.json) {
|
|
4347
|
+
console.log(JSON.stringify({
|
|
4348
|
+
ok: false,
|
|
4349
|
+
status: err?.status || null,
|
|
4350
|
+
message: err?.message || String(err),
|
|
4351
|
+
data: err?.data || null,
|
|
4352
|
+
artifact_path: absPath,
|
|
4353
|
+
}, null, 2));
|
|
4354
|
+
} else {
|
|
4355
|
+
console.error(`Feedback ${mode} failed:`, err?.message || err);
|
|
4356
|
+
if (err?.data) console.error(JSON.stringify(err.data, null, 2));
|
|
4357
|
+
}
|
|
4358
|
+
process.exit(1);
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
const summary = {
|
|
4362
|
+
ok: true,
|
|
4363
|
+
mode,
|
|
4364
|
+
artifact_path: absPath,
|
|
4365
|
+
...summarizeFeedbackRefinementResult(data),
|
|
4366
|
+
};
|
|
4367
|
+
if (args.json) {
|
|
4368
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
console.log(`Feedback ${mode}: ${summary.valid ? "valid" : "invalid"}`);
|
|
4372
|
+
console.log(`Feedback: ${summary.feedback_id || feedbackId}`);
|
|
4373
|
+
if (summary.history_id) console.log(`History: ${summary.history_id}`);
|
|
4374
|
+
if (summary.new_snapshot_hash) console.log(`New Snapshot: ${summary.new_snapshot_hash}`);
|
|
4375
|
+
if (summary.current_snapshot_hash) console.log(`Current Snapshot: ${summary.current_snapshot_hash}`);
|
|
4376
|
+
console.log(`Diffs: ${summary.diff_count}`);
|
|
4377
|
+
for (const diff of summary.diffs || []) {
|
|
4378
|
+
console.log(`- ${diff.field}: ${JSON.stringify(diff.from)} -> ${JSON.stringify(diff.to)}`);
|
|
4379
|
+
}
|
|
4380
|
+
if (mode === "apply") {
|
|
4381
|
+
console.log("Local feedback.yml is not modified by apply. Run `myte feedback-sync` to refresh it.");
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
async function runFeedbackGetOrHistory(args, mode) {
|
|
4386
|
+
const key = getProjectApiKey();
|
|
4387
|
+
if (!key) {
|
|
4388
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
4389
|
+
process.exit(1);
|
|
4390
|
+
}
|
|
4391
|
+
const feedbackId = resolveFeedbackIdArg(args);
|
|
4392
|
+
if (!feedbackId) {
|
|
4393
|
+
console.error("Missing --feedback-id.");
|
|
4394
|
+
process.exit(1);
|
|
4395
|
+
}
|
|
4396
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
4397
|
+
const apiBase = resolveApiBase(args);
|
|
4398
|
+
|
|
4399
|
+
let data;
|
|
4400
|
+
try {
|
|
4401
|
+
data = mode === "history"
|
|
4402
|
+
? await fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId })
|
|
4403
|
+
: await fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId });
|
|
4404
|
+
} catch (err) {
|
|
4405
|
+
console.error(`Feedback ${mode} failed:`, err?.message || err);
|
|
4406
|
+
process.exit(1);
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
if (args.json) {
|
|
4410
|
+
console.log(JSON.stringify(data, null, 2));
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4413
|
+
if (mode === "history") {
|
|
4414
|
+
console.log(`Feedback: ${data.feedback_id || feedbackId}`);
|
|
4415
|
+
console.log(`Events: ${data.count || 0}`);
|
|
4416
|
+
for (const event of data.events || []) {
|
|
4417
|
+
console.log(`- ${event.created_at || "unknown"} ${event.action || "refine"} ${event.history_id || ""}`);
|
|
4418
|
+
}
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
const feedback = data.feedback || {};
|
|
4422
|
+
console.log(`Feedback: ${feedback.feedback_id || feedbackId}`);
|
|
4423
|
+
console.log(`Title: ${feedback.title || "(untitled)"}`);
|
|
4424
|
+
console.log(`Status: ${feedback.feedback_state || feedback.status || "unknown"} (${feedback.status || "legacy unknown"})`);
|
|
4425
|
+
console.log(`Priority: ${feedback.priority || "n/a"}`);
|
|
4426
|
+
console.log(`Snapshot: ${feedback.snapshot_hash || "n/a"}`);
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
async function runFeedback(args) {
|
|
4430
|
+
const subcommand = firstNonEmptyString(args._?.[0]) || "help";
|
|
4431
|
+
if (subcommand === "help") {
|
|
4432
|
+
printHelp();
|
|
4433
|
+
return;
|
|
4434
|
+
}
|
|
4435
|
+
if (["status", "edit", "assign", "archive", "refine"].includes(subcommand)) {
|
|
4436
|
+
await buildFeedbackDraftForCommand(args, subcommand);
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
if (subcommand === "validate" || subcommand === "apply") {
|
|
4440
|
+
await runFeedbackValidateOrApply(args, subcommand);
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
if (subcommand === "get" || subcommand === "history") {
|
|
4444
|
+
await runFeedbackGetOrHistory(args, subcommand);
|
|
4445
|
+
return;
|
|
4446
|
+
}
|
|
4447
|
+
console.error("Unknown feedback command. Use status, edit, assign, archive, get, history, validate, or apply.");
|
|
4448
|
+
process.exit(1);
|
|
4449
|
+
}
|
|
4450
|
+
|
|
3932
4451
|
async function runSuggestionsSync(args) {
|
|
3933
4452
|
const key = getProjectApiKey();
|
|
3934
4453
|
if (!key) {
|
|
@@ -4490,8 +5009,9 @@ async function runQuery(args) {
|
|
|
4490
5009
|
const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
|
|
4491
5010
|
|
|
4492
5011
|
const charLimitRaw = args["diff-limit"] || args.diffLimit || args.diff_limit;
|
|
4493
|
-
const charLimitParsed =
|
|
4494
|
-
|
|
5012
|
+
const charLimitParsed =
|
|
5013
|
+
charLimitRaw !== undefined && charLimitRaw !== null ? Number(charLimitRaw) : DEFAULT_DIFF_LIMIT_CHARS;
|
|
5014
|
+
const diffLimit = Number.isFinite(charLimitParsed) ? charLimitParsed : DEFAULT_DIFF_LIMIT_CHARS;
|
|
4495
5015
|
|
|
4496
5016
|
const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
|
|
4497
5017
|
const apiBase = normalizeApiBase(baseRaw);
|
|
@@ -4565,13 +5085,19 @@ async function runQuery(args) {
|
|
|
4565
5085
|
const pollTimeoutMs = Math.max(timeoutMs, 900_000);
|
|
4566
5086
|
const startedAt = Date.now();
|
|
4567
5087
|
let finalStatus = null;
|
|
5088
|
+
let pollDelayMs = 2_000;
|
|
4568
5089
|
do {
|
|
4569
|
-
await sleep(
|
|
5090
|
+
await sleep(pollDelayMs);
|
|
4570
5091
|
try {
|
|
4571
5092
|
finalStatus = await fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId });
|
|
5093
|
+
pollDelayMs = Math.min(10_000, Math.ceil(pollDelayMs * 1.25));
|
|
4572
5094
|
} catch (err) {
|
|
4573
|
-
if (
|
|
4574
|
-
|
|
5095
|
+
if (isTransientQueryStatusError(err)) {
|
|
5096
|
+
const fallbackMs =
|
|
5097
|
+
Number(err?.status) === 429 ? 5_000 : Math.min(30_000, Math.max(5_000, pollDelayMs * 2));
|
|
5098
|
+
const waitMs = resolveRetryAfterMs(err, fallbackMs);
|
|
5099
|
+
await sleep(waitMs);
|
|
5100
|
+
pollDelayMs = Math.min(30_000, Math.max(pollDelayMs, waitMs));
|
|
4575
5101
|
continue;
|
|
4576
5102
|
}
|
|
4577
5103
|
throw err;
|
|
@@ -4670,6 +5196,11 @@ async function main() {
|
|
|
4670
5196
|
return;
|
|
4671
5197
|
}
|
|
4672
5198
|
|
|
5199
|
+
if (command === "feedback") {
|
|
5200
|
+
await runFeedback(args);
|
|
5201
|
+
return;
|
|
5202
|
+
}
|
|
5203
|
+
|
|
4673
5204
|
if (command === "suggestions") {
|
|
4674
5205
|
await runSuggestions(args);
|
|
4675
5206
|
return;
|