@mytegroupinc/myte-core 0.0.19 → 0.0.21
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 +15 -142
- package/cli.js +1043 -49
- package/package.json +2 -2
package/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* - Auth: MYTE_API_KEY (project key) from `.env` or env
|
|
6
6
|
* - Default API: https://api.myte.dev (override with MYTE_API_BASE or --base-url)
|
|
7
|
-
* - Deterministic diffs: fetch
|
|
7
|
+
* - Deterministic diffs: fetch project config, resolve local project repos, then collect scoped git context
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const fs = require("fs");
|
|
@@ -98,6 +98,7 @@ function splitCommand(argv) {
|
|
|
98
98
|
"update-owner",
|
|
99
99
|
"update-client",
|
|
100
100
|
"feedback-sync",
|
|
101
|
+
"feedback",
|
|
101
102
|
"help",
|
|
102
103
|
"--help",
|
|
103
104
|
"-h",
|
|
@@ -141,6 +142,17 @@ function parseArgs(argv) {
|
|
|
141
142
|
"target-contact-ids",
|
|
142
143
|
"status",
|
|
143
144
|
"source",
|
|
145
|
+
"feedback-id",
|
|
146
|
+
"reason",
|
|
147
|
+
"review-action",
|
|
148
|
+
"idempotency-key",
|
|
149
|
+
"user-id",
|
|
150
|
+
"assigned-user-id",
|
|
151
|
+
"due-date",
|
|
152
|
+
"priority",
|
|
153
|
+
"review-note",
|
|
154
|
+
"tags",
|
|
155
|
+
"tag",
|
|
144
156
|
"mission-ids",
|
|
145
157
|
"client-session-id",
|
|
146
158
|
],
|
|
@@ -194,7 +206,7 @@ function parseArgs(argv) {
|
|
|
194
206
|
|
|
195
207
|
function printHelp() {
|
|
196
208
|
const text = [
|
|
197
|
-
"myte - Myte
|
|
209
|
+
"myte - Myte CLI",
|
|
198
210
|
"",
|
|
199
211
|
"Usage:",
|
|
200
212
|
" myte query \"<text>\" [--with-diff] [--context \"...\"]",
|
|
@@ -212,6 +224,10 @@ function printHelp() {
|
|
|
212
224
|
" myte update-owner --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--json]",
|
|
213
225
|
" myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] [--json]",
|
|
214
226
|
" myte feedback-sync [--status <value>] [--source <value>] [--with-prd-text|--no-with-prd-text] [--output-dir ./MyteCommandCenter] [--json]",
|
|
227
|
+
" myte feedback status --feedback-id <id> --status todo|in_progress|in_review|completed|deployed|rejected|archived --reason \"...\"",
|
|
228
|
+
" myte feedback edit --feedback-id <id> [--title \"...\"] [--feedback-text \"...\"] [--priority High] [--reason \"...\"]",
|
|
229
|
+
" myte feedback validate --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
|
|
230
|
+
" myte feedback apply --file ./MyteCommandCenter/reviews/feedback/<id>-status.yml [--json]",
|
|
215
231
|
" myte create-prd <file.md> [more.md ...] [--json] [--title \"...\"] [--description \"...\"]",
|
|
216
232
|
" cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
|
|
217
233
|
" cat update.md | myte update-owner --stdin --subject \"Owner update\"",
|
|
@@ -232,13 +248,13 @@ function printHelp() {
|
|
|
232
248
|
" - Set MYTEAI_API_KEY in a workspace .env (or env var) for `myte ai`",
|
|
233
249
|
"",
|
|
234
250
|
"bootstrap contract:",
|
|
235
|
-
" - Run from
|
|
251
|
+
" - Run from any workspace where you want local MyteCommandCenter data written",
|
|
236
252
|
" - Writes MyteCommandCenter/data/project.yml plus phases, epics, stories, and missions locally",
|
|
237
253
|
" - Uses the project-scoped bootstrap snapshot from the Myte API",
|
|
238
254
|
" - Mission cards include richer execution context like complexity, estimated_hours, due_date, subtasks, technical_requirements, resources_needed, labels, and normalized test_cases",
|
|
239
255
|
"",
|
|
240
256
|
"sync-qaqc contract:",
|
|
241
|
-
" - Run from
|
|
257
|
+
" - Run from any workspace where you want local MyteCommandCenter data written",
|
|
242
258
|
" - Works even if bootstrap has not been run yet; it creates MyteCommandCenter/data/qaqc.yml",
|
|
243
259
|
" - Writes the active mission QAQC working set into one deterministic file: MyteCommandCenter/data/qaqc.yml",
|
|
244
260
|
"",
|
|
@@ -285,13 +301,19 @@ function printHelp() {
|
|
|
285
301
|
" - If the project has no linked client contacts, the backend falls back to the project owner for internal projects",
|
|
286
302
|
"",
|
|
287
303
|
"feedback-sync contract:",
|
|
288
|
-
" - Runs from
|
|
304
|
+
" - Runs from any workspace where you want local MyteCommandCenter data written",
|
|
289
305
|
" - Syncs pending feedback by default so local Command Center data stays focused on active work",
|
|
290
306
|
" - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
|
|
291
307
|
" - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
|
|
292
308
|
"",
|
|
309
|
+
"feedback review contract:",
|
|
310
|
+
" - Draft commands write review artifacts under MyteCommandCenter/reviews/feedback/*.yml for local IDE diff review",
|
|
311
|
+
" - validate/apply send those artifacts to /api/project-assistant/feedback/<id>/refinement/*",
|
|
312
|
+
" - The backend owns authorization, stale snapshot checks, allowed field/transition rules, and history",
|
|
313
|
+
" - apply is idempotent and does not rewrite local feedback.yml; run feedback-sync after apply to refresh local state",
|
|
314
|
+
"",
|
|
293
315
|
"Options:",
|
|
294
|
-
" --with-diff Include deterministic git diffs (project-scoped)",
|
|
316
|
+
" --with-diff Include deterministic git diffs (project-scoped; fails fast if no project repos are configured or resolved)",
|
|
295
317
|
" --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
|
|
296
318
|
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
297
319
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
@@ -299,7 +321,7 @@ function printHelp() {
|
|
|
299
321
|
" --json-response Ask the Myte AI gateway to return clean JSON only and send OpenAI-compatible response_format",
|
|
300
322
|
" --max-output-tokens Output token cap for `myte ai` simple queries",
|
|
301
323
|
" --temperature <num> Temperature for `myte ai` simple queries",
|
|
302
|
-
" --output-dir <path> Command Center output directory (default: <
|
|
324
|
+
" --output-dir <path> Command Center output directory (default: <current-workspace>/MyteCommandCenter)",
|
|
303
325
|
" --file <path> YAML/JSON payload file for suggestions create/revise/review",
|
|
304
326
|
" --stdin Read supported command content from stdin instead of inline text or a file path",
|
|
305
327
|
" --title <text> Override PRD title for raw markdown uploads",
|
|
@@ -310,8 +332,15 @@ function printHelp() {
|
|
|
310
332
|
" --body-file <path> Read update-owner or update-client markdown body from a file",
|
|
311
333
|
" --target-contact-id Add one client contact ObjectId (repeatable)",
|
|
312
334
|
" --target-contact-ids Comma-separated client contact ObjectIds",
|
|
313
|
-
" --
|
|
335
|
+
" --feedback-id <id> Feedback ObjectId for feedback review commands",
|
|
336
|
+
" --reason <text> Human review reason included in feedback refinement artifacts",
|
|
337
|
+
" --status <value> Mission status target, feedback-sync filter, or feedback review state depending on command",
|
|
314
338
|
" --source <value> Feedback source filter for feedback-sync",
|
|
339
|
+
" --priority <value> Feedback review priority target: Low, Medium, or High",
|
|
340
|
+
" --user-id <id> Feedback assignee user id for `myte feedback assign`",
|
|
341
|
+
" --assigned-user-id Alias for --user-id in feedback assign",
|
|
342
|
+
" --due-date <date> Feedback due date target for edit artifacts",
|
|
343
|
+
" --review-note <text> Review note stored in feedback refinement history",
|
|
315
344
|
" --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
|
|
316
345
|
" --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
|
|
317
346
|
" --mission-ids <ids> Comma-separated mission business ids for run-qaqc or mission status (quote multi-id values on PowerShell)",
|
|
@@ -340,6 +369,9 @@ function printHelp() {
|
|
|
340
369
|
" myte update-owner --subject \"QAQC progress\" --body-file ./updates/owner.md",
|
|
341
370
|
" myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md",
|
|
342
371
|
" myte feedback-sync --json",
|
|
372
|
+
" myte feedback status --feedback-id 507f1f77bcf86cd799439011 --status in_review --reason \"Ready for owner review\"",
|
|
373
|
+
" myte feedback validate --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
|
|
374
|
+
" myte feedback apply --file ./MyteCommandCenter/reviews/feedback/507f1f77bcf86cd799439011-status.yml --json",
|
|
343
375
|
" myte suggestions create --file ./suggestions/create.yml",
|
|
344
376
|
" myte suggestions revise",
|
|
345
377
|
" myte suggestions review",
|
|
@@ -589,18 +621,25 @@ function summarizeDiffDiagnosticsForContext(diagnostics) {
|
|
|
589
621
|
project_id: diagnostics.project_id || null,
|
|
590
622
|
mode: diagnostics.mode,
|
|
591
623
|
requested_repos: diagnostics.requested_repo_names || [],
|
|
624
|
+
requested_repo_bindings: diagnostics.requested_repo_bindings || [],
|
|
592
625
|
found_repos: diagnostics.found_repos || [],
|
|
593
626
|
missing_repos: diagnostics.missing_repos || [],
|
|
594
627
|
collected_any: Boolean(diagnostics.collected_any),
|
|
595
628
|
truncation: diagnostics.truncated ? "truncated" : "full",
|
|
596
629
|
repos: repos.map((repo) => ({
|
|
597
630
|
name: repo.name,
|
|
631
|
+
role: repo.role || null,
|
|
598
632
|
status: repo.status || "ok",
|
|
599
633
|
head_branch: repo.head_branch || null,
|
|
600
634
|
base_ref: repo.base_ref || null,
|
|
635
|
+
base_ref_label: repo.base_ref_label || repo.base_ref || null,
|
|
636
|
+
baseline_branch: repo.baseline_branch || null,
|
|
637
|
+
compare_mode: repo.compare_mode || "local_origin",
|
|
638
|
+
compare_remote_url: repo.compare_remote_url || null,
|
|
601
639
|
has_changes: Boolean(repo.has_changes),
|
|
602
640
|
changed_blocks: repo.changed_blocks || {},
|
|
603
641
|
untracked_file_count: repo.untracked_file_count || 0,
|
|
642
|
+
matched_by: repo.matched_by || [],
|
|
604
643
|
error_count: Array.isArray(repo.errors) ? repo.errors.length : 0,
|
|
605
644
|
})),
|
|
606
645
|
warnings: diagnostics.warnings || [],
|
|
@@ -642,8 +681,29 @@ const IGNORED_PATTERNS = [
|
|
|
642
681
|
/^out\//,
|
|
643
682
|
];
|
|
644
683
|
|
|
684
|
+
const DIFF_MARKDOWN_ALLOWLIST = new Set([
|
|
685
|
+
"README.md",
|
|
686
|
+
"AGENTS.md",
|
|
687
|
+
"CodexContext.MD",
|
|
688
|
+
"CLAUDE.md",
|
|
689
|
+
"GEMINI.md",
|
|
690
|
+
]);
|
|
691
|
+
|
|
692
|
+
function normalizeRepoPathForDiff(p) {
|
|
693
|
+
return String(p || "").replace(/\\/g, "/");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function shouldIgnoreMarkdownPath(p) {
|
|
697
|
+
const normalized = normalizeRepoPathForDiff(p);
|
|
698
|
+
if (!/\.md$/i.test(normalized)) return false;
|
|
699
|
+
const base = path.posix.basename(normalized);
|
|
700
|
+
return !DIFF_MARKDOWN_ALLOWLIST.has(base);
|
|
701
|
+
}
|
|
702
|
+
|
|
645
703
|
function shouldIgnore(p) {
|
|
646
|
-
|
|
704
|
+
const normalized = normalizeRepoPathForDiff(p);
|
|
705
|
+
if (shouldIgnoreMarkdownPath(normalized)) return true;
|
|
706
|
+
return IGNORED_PATTERNS.some((re) => re.test(normalized));
|
|
647
707
|
}
|
|
648
708
|
|
|
649
709
|
function hasGitDir(repoPath) {
|
|
@@ -833,12 +893,373 @@ function resolveConfiguredRepos(repoNames) {
|
|
|
833
893
|
return { mode: "none", root: null, repos: [], missing: names };
|
|
834
894
|
}
|
|
835
895
|
|
|
836
|
-
function
|
|
896
|
+
function findGitTopLevel(startPath) {
|
|
897
|
+
const result = runGitRaw(startPath, ["rev-parse", "--show-toplevel"]);
|
|
898
|
+
if (!result.ok) return null;
|
|
899
|
+
const value = String(result.stdout || "").trim();
|
|
900
|
+
return value || null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function normalizeRemoteFingerprint(value) {
|
|
904
|
+
const raw = String(value || "").trim();
|
|
905
|
+
if (!raw) return "";
|
|
906
|
+
|
|
907
|
+
const cleaned = raw.replace(/\\/g, "/").replace(/\.git$/i, "");
|
|
908
|
+
const looksLikeLocalPath =
|
|
909
|
+
!cleaned.includes("://") &&
|
|
910
|
+
(/^[a-zA-Z]:\//.test(cleaned) || cleaned.startsWith("/") || cleaned.startsWith("./") || cleaned.startsWith("../"));
|
|
911
|
+
if (looksLikeLocalPath) {
|
|
912
|
+
return `local/${cleaned.toLowerCase()}`;
|
|
913
|
+
}
|
|
914
|
+
const sshMatch = cleaned.match(/^(?:.+@)?([^:/]+)[:/](.+)$/);
|
|
915
|
+
if (!cleaned.includes("://") && sshMatch) {
|
|
916
|
+
const host = String(sshMatch[1] || "").trim().toLowerCase();
|
|
917
|
+
const repoPath = String(sshMatch[2] || "")
|
|
918
|
+
.trim()
|
|
919
|
+
.replace(/^\/+/, "")
|
|
920
|
+
.replace(/\/+$/, "")
|
|
921
|
+
.toLowerCase();
|
|
922
|
+
return host && repoPath ? `${host}/${repoPath}` : "";
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
const parsed = new URL(cleaned);
|
|
927
|
+
const host = String(parsed.hostname || "").trim().toLowerCase();
|
|
928
|
+
const repoPath = String(parsed.pathname || "")
|
|
929
|
+
.trim()
|
|
930
|
+
.replace(/^\/+/, "")
|
|
931
|
+
.replace(/\/+$/, "")
|
|
932
|
+
.toLowerCase();
|
|
933
|
+
return host && repoPath ? `${host}/${repoPath}` : "";
|
|
934
|
+
} catch {
|
|
935
|
+
return cleaned.toLowerCase();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function uniqueNormalizedStrings(values) {
|
|
940
|
+
const seen = new Set();
|
|
941
|
+
const ordered = [];
|
|
942
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
943
|
+
const text = String(value || "").trim();
|
|
944
|
+
if (!text) continue;
|
|
945
|
+
const key = text.toLowerCase();
|
|
946
|
+
if (seen.has(key)) continue;
|
|
947
|
+
seen.add(key);
|
|
948
|
+
ordered.push(text);
|
|
949
|
+
}
|
|
950
|
+
return ordered;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function normalizeRepoBindingEntry(binding, index = 0) {
|
|
954
|
+
if (!binding || typeof binding !== "object") return null;
|
|
955
|
+
|
|
956
|
+
const role = String(binding.role || "").trim().toLowerCase();
|
|
957
|
+
const canonicalRepoName = String(binding.canonical_repo_name || "").trim();
|
|
958
|
+
const canonicalRemoteUrl = String(binding.canonical_remote_url || "").trim();
|
|
959
|
+
const clientRepoName = String(binding.client_repo_name || "").trim();
|
|
960
|
+
const clientRemoteUrl = String(binding.client_remote_url || "").trim();
|
|
961
|
+
const localRepoAliases = uniqueNormalizedStrings(binding.local_repo_aliases || []);
|
|
962
|
+
const baselineBranch = String(binding.baseline_branch || "").trim();
|
|
963
|
+
|
|
964
|
+
const remoteFingerprints = uniqueNormalizedStrings([
|
|
965
|
+
...(Array.isArray(binding.remote_fingerprints) ? binding.remote_fingerprints : []),
|
|
966
|
+
normalizeRemoteFingerprint(canonicalRemoteUrl),
|
|
967
|
+
normalizeRemoteFingerprint(clientRemoteUrl),
|
|
968
|
+
]);
|
|
969
|
+
|
|
970
|
+
const matchingNames = uniqueNormalizedStrings([
|
|
971
|
+
...(Array.isArray(binding.matching_names) ? binding.matching_names : []),
|
|
972
|
+
canonicalRepoName,
|
|
973
|
+
clientRepoName,
|
|
974
|
+
...localRepoAliases,
|
|
975
|
+
]);
|
|
976
|
+
|
|
977
|
+
if (!role && !matchingNames.length && !remoteFingerprints.length) return null;
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
role: role || `repo_${index + 1}`,
|
|
981
|
+
canonical_repo_name: canonicalRepoName || null,
|
|
982
|
+
canonical_remote_url: canonicalRemoteUrl || null,
|
|
983
|
+
client_repo_name: clientRepoName || null,
|
|
984
|
+
client_remote_url: clientRemoteUrl || null,
|
|
985
|
+
local_repo_aliases: localRepoAliases,
|
|
986
|
+
baseline_branch: baselineBranch || null,
|
|
987
|
+
matching_names: matchingNames,
|
|
988
|
+
remote_fingerprints: remoteFingerprints,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function normalizeRepoBindings(bindings) {
|
|
993
|
+
const normalized = [];
|
|
994
|
+
const seenRoles = new Set();
|
|
995
|
+
for (const [index, binding] of (Array.isArray(bindings) ? bindings : []).entries()) {
|
|
996
|
+
const item = normalizeRepoBindingEntry(binding, index);
|
|
997
|
+
if (!item) continue;
|
|
998
|
+
const roleKey = String(item.role || "").trim().toLowerCase();
|
|
999
|
+
if (seenRoles.has(roleKey)) continue;
|
|
1000
|
+
seenRoles.add(roleKey);
|
|
1001
|
+
normalized.push(item);
|
|
1002
|
+
}
|
|
1003
|
+
return normalized;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function getRepoRemoteFingerprints(repoPath) {
|
|
1007
|
+
const output = runGitTry(repoPath, ["remote", "-v"]);
|
|
1008
|
+
if (!output) return [];
|
|
1009
|
+
const fingerprints = [];
|
|
1010
|
+
for (const line of String(output).split(/\r?\n/)) {
|
|
1011
|
+
const parts = line.trim().split(/\s+/);
|
|
1012
|
+
if (parts.length < 2) continue;
|
|
1013
|
+
const normalized = normalizeRemoteFingerprint(parts[1]);
|
|
1014
|
+
if (normalized) fingerprints.push(normalized);
|
|
1015
|
+
}
|
|
1016
|
+
return uniqueNormalizedStrings(fingerprints);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function listGitRepoCandidates(searchRoot, currentRepoRoot) {
|
|
1020
|
+
const candidates = [];
|
|
1021
|
+
const seen = new Set();
|
|
1022
|
+
|
|
1023
|
+
function pushCandidate(absPath, rootForDir) {
|
|
1024
|
+
const abs = path.resolve(absPath);
|
|
1025
|
+
if (!hasGitDir(abs) || seen.has(abs)) return;
|
|
1026
|
+
seen.add(abs);
|
|
1027
|
+
const rootBase = rootForDir ? path.resolve(rootForDir) : path.dirname(abs);
|
|
1028
|
+
const rel = path.relative(rootBase, abs);
|
|
1029
|
+
const dir = rel && rel !== "" ? rel : ".";
|
|
1030
|
+
const repoName = path.basename(abs);
|
|
1031
|
+
candidates.push({
|
|
1032
|
+
abs,
|
|
1033
|
+
dir,
|
|
1034
|
+
name: repoName,
|
|
1035
|
+
prefix: `${repoName.replace(/\\\\/g, "/")}/`,
|
|
1036
|
+
remote_fingerprints: getRepoRemoteFingerprints(abs),
|
|
1037
|
+
is_current_repo: currentRepoRoot ? path.resolve(currentRepoRoot) === abs : false,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (currentRepoRoot) {
|
|
1042
|
+
pushCandidate(currentRepoRoot, path.dirname(currentRepoRoot));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
pushCandidate(searchRoot, path.dirname(searchRoot));
|
|
1046
|
+
|
|
1047
|
+
let entries = [];
|
|
1048
|
+
try {
|
|
1049
|
+
entries = fs.readdirSync(searchRoot, { withFileTypes: true });
|
|
1050
|
+
} catch {
|
|
1051
|
+
entries = [];
|
|
1052
|
+
}
|
|
1053
|
+
for (const entry of entries) {
|
|
1054
|
+
if (!entry || !entry.isDirectory || !entry.isDirectory()) continue;
|
|
1055
|
+
const abs = path.join(searchRoot, entry.name);
|
|
1056
|
+
pushCandidate(abs, searchRoot);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return candidates;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function scoreBindingCandidate(binding, candidate) {
|
|
1063
|
+
const candidateName = String(candidate?.name || "").trim().toLowerCase();
|
|
1064
|
+
const bindingNames = new Set((binding?.matching_names || []).map((name) => String(name || "").trim().toLowerCase()).filter(Boolean));
|
|
1065
|
+
const bindingRemotes = new Set((binding?.remote_fingerprints || []).map((value) => String(value || "").trim().toLowerCase()).filter(Boolean));
|
|
1066
|
+
const candidateRemotes = new Set((candidate?.remote_fingerprints || []).map((value) => String(value || "").trim().toLowerCase()).filter(Boolean));
|
|
1067
|
+
const canonicalRemoteFingerprint = normalizeRemoteFingerprint(binding?.canonical_remote_url).toLowerCase();
|
|
1068
|
+
const clientRemoteFingerprint = normalizeRemoteFingerprint(binding?.client_remote_url).toLowerCase();
|
|
1069
|
+
|
|
1070
|
+
let score = 0;
|
|
1071
|
+
const matchedBy = [];
|
|
1072
|
+
|
|
1073
|
+
if (binding.canonical_repo_name && candidateName === String(binding.canonical_repo_name).trim().toLowerCase()) {
|
|
1074
|
+
score = Math.max(score, 240);
|
|
1075
|
+
matchedBy.push("canonical_name");
|
|
1076
|
+
}
|
|
1077
|
+
if (binding.client_repo_name && candidateName === String(binding.client_repo_name).trim().toLowerCase()) {
|
|
1078
|
+
score = Math.max(score, 260);
|
|
1079
|
+
matchedBy.push("client_name");
|
|
1080
|
+
}
|
|
1081
|
+
if (bindingNames.has(candidateName)) {
|
|
1082
|
+
score = Math.max(score, 200);
|
|
1083
|
+
matchedBy.push("alias");
|
|
1084
|
+
}
|
|
1085
|
+
for (const remote of candidateRemotes) {
|
|
1086
|
+
if (clientRemoteFingerprint && remote === clientRemoteFingerprint) {
|
|
1087
|
+
score = Math.max(score, 340);
|
|
1088
|
+
matchedBy.push("client_remote");
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
if (canonicalRemoteFingerprint && remote === canonicalRemoteFingerprint) {
|
|
1092
|
+
score = Math.max(score, 300);
|
|
1093
|
+
matchedBy.push("canonical_remote");
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
if (bindingRemotes.has(remote)) {
|
|
1097
|
+
score = Math.max(score, 320);
|
|
1098
|
+
matchedBy.push("remote");
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return { score, matchedBy: uniqueNormalizedStrings(matchedBy) };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function resolveConfiguredRepoBindings(bindings) {
|
|
1107
|
+
const normalizedBindings = normalizeRepoBindings(bindings);
|
|
1108
|
+
if (!normalizedBindings.length) {
|
|
1109
|
+
return { mode: "none", root: null, repos: [], missing: [], bindings: [] };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const cwd = process.cwd();
|
|
1113
|
+
const currentRepoRoot = findGitTopLevel(cwd);
|
|
1114
|
+
const searchRoots = [];
|
|
1115
|
+
const seenRoots = new Set();
|
|
1116
|
+
const pushSearchRoot = (value) => {
|
|
1117
|
+
if (!value) return;
|
|
1118
|
+
const resolved = path.resolve(value);
|
|
1119
|
+
if (seenRoots.has(resolved)) return;
|
|
1120
|
+
seenRoots.add(resolved);
|
|
1121
|
+
searchRoots.push(resolved);
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// Prefer the invocation workspace first. A wrapper repo can contain the
|
|
1125
|
+
// configured API/WEB repos as children, so jumping immediately to the parent
|
|
1126
|
+
// can miss the project repos entirely.
|
|
1127
|
+
pushSearchRoot(cwd);
|
|
1128
|
+
if (currentRepoRoot) pushSearchRoot(currentRepoRoot);
|
|
1129
|
+
|
|
1130
|
+
const scanStart = currentRepoRoot ? path.dirname(currentRepoRoot) : path.dirname(cwd);
|
|
1131
|
+
let cur = scanStart;
|
|
1132
|
+
for (let i = 0; i < 8; i += 1) {
|
|
1133
|
+
pushSearchRoot(cur);
|
|
1134
|
+
const parent = path.dirname(cur);
|
|
1135
|
+
if (parent === cur) break;
|
|
1136
|
+
cur = parent;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
let fallbackCandidates = [];
|
|
1140
|
+
let fallbackRoot = null;
|
|
1141
|
+
|
|
1142
|
+
for (const candidateRoot of searchRoots) {
|
|
1143
|
+
const candidates = listGitRepoCandidates(candidateRoot, currentRepoRoot);
|
|
1144
|
+
if (!fallbackCandidates.length && candidates.length) {
|
|
1145
|
+
fallbackCandidates = candidates;
|
|
1146
|
+
fallbackRoot = candidateRoot;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const assigned = new Set();
|
|
1150
|
+
const found = [];
|
|
1151
|
+
for (const binding of normalizedBindings) {
|
|
1152
|
+
let best = null;
|
|
1153
|
+
for (const candidate of candidates) {
|
|
1154
|
+
if (assigned.has(candidate.abs)) continue;
|
|
1155
|
+
const match = scoreBindingCandidate(binding, candidate);
|
|
1156
|
+
if (!match.score) continue;
|
|
1157
|
+
if (!best || match.score > best.match.score) {
|
|
1158
|
+
best = { candidate, match };
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (!best) continue;
|
|
1162
|
+
assigned.add(best.candidate.abs);
|
|
1163
|
+
found.push({
|
|
1164
|
+
name: best.candidate.name,
|
|
1165
|
+
dir: best.candidate.dir,
|
|
1166
|
+
abs: best.candidate.abs,
|
|
1167
|
+
prefix: best.candidate.prefix,
|
|
1168
|
+
role: binding.role,
|
|
1169
|
+
canonical_repo_name: binding.canonical_repo_name,
|
|
1170
|
+
canonical_remote_url: binding.canonical_remote_url,
|
|
1171
|
+
client_repo_name: binding.client_repo_name,
|
|
1172
|
+
client_remote_url: binding.client_remote_url,
|
|
1173
|
+
baseline_branch: binding.baseline_branch || null,
|
|
1174
|
+
matched_by: best.match.matchedBy,
|
|
1175
|
+
remote_fingerprints: best.candidate.remote_fingerprints,
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (found.length) {
|
|
1180
|
+
const foundRoles = new Set(found.map((repo) => repo.role));
|
|
1181
|
+
const missing = normalizedBindings
|
|
1182
|
+
.filter((binding) => !foundRoles.has(binding.role))
|
|
1183
|
+
.map((binding) => binding.client_repo_name || binding.canonical_repo_name || binding.role);
|
|
1184
|
+
return {
|
|
1185
|
+
mode: currentRepoRoot && found.length === 1 && found[0].abs === currentRepoRoot ? "repo" : "workspace",
|
|
1186
|
+
root: candidateRoot,
|
|
1187
|
+
repos: found,
|
|
1188
|
+
missing,
|
|
1189
|
+
bindings: normalizedBindings,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
mode: currentRepoRoot ? "repo" : "none",
|
|
1196
|
+
root: fallbackRoot,
|
|
1197
|
+
repos: [],
|
|
1198
|
+
missing: normalizedBindings.map((binding) => binding.client_repo_name || binding.canonical_repo_name || binding.role),
|
|
1199
|
+
bindings: normalizedBindings,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function fetchBaseBranches(repoPath, preferredBranch) {
|
|
1204
|
+
const preferred = String(preferredBranch || "").trim();
|
|
1205
|
+
if (preferred && runGitOk(repoPath, ["fetch", "origin", preferred, "--prune", "--quiet"])) return true;
|
|
837
1206
|
if (runGitOk(repoPath, ["fetch", "origin", "main", "master", "--prune", "--quiet"])) return true;
|
|
838
1207
|
return runGitOk(repoPath, ["fetch", "--all", "--prune", "--quiet"]);
|
|
839
1208
|
}
|
|
840
1209
|
|
|
841
|
-
function
|
|
1210
|
+
function buildExternalBaseRefName(remoteUrl, branch) {
|
|
1211
|
+
const branchSlug = String(branch || "main")
|
|
1212
|
+
.trim()
|
|
1213
|
+
.toLowerCase()
|
|
1214
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1215
|
+
.replace(/^-+|-+$/g, "") || "main";
|
|
1216
|
+
const digest = createHash("sha1")
|
|
1217
|
+
.update(`${String(remoteUrl || "").trim()}::${branchSlug}`)
|
|
1218
|
+
.digest("hex")
|
|
1219
|
+
.slice(0, 12);
|
|
1220
|
+
return `refs/myte/bases/${branchSlug}-${digest}`;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function fetchExternalBaseRef(repoPath, remoteUrl, preferredBranch) {
|
|
1224
|
+
const remote = String(remoteUrl || "").trim();
|
|
1225
|
+
if (!remote) return null;
|
|
1226
|
+
|
|
1227
|
+
const candidateBranches = uniqueNormalizedStrings([preferredBranch, "main", "master"]);
|
|
1228
|
+
for (const branch of candidateBranches) {
|
|
1229
|
+
const refName = buildExternalBaseRefName(remote, branch);
|
|
1230
|
+
if (runGitOk(repoPath, ["fetch", "--quiet", "--no-tags", remote, `${branch}:${refName}`])) {
|
|
1231
|
+
return { ref: refName, branch };
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function resolveExternalCompareRemoteUrl(repo) {
|
|
1238
|
+
const canonicalRemoteUrl = String(repo?.canonical_remote_url || "").trim();
|
|
1239
|
+
const clientRemoteUrl = String(repo?.client_remote_url || "").trim();
|
|
1240
|
+
if (!canonicalRemoteUrl || !clientRemoteUrl) return null;
|
|
1241
|
+
|
|
1242
|
+
const canonicalFingerprint = normalizeRemoteFingerprint(canonicalRemoteUrl);
|
|
1243
|
+
const clientFingerprint = normalizeRemoteFingerprint(clientRemoteUrl);
|
|
1244
|
+
if (!canonicalFingerprint || !clientFingerprint || canonicalFingerprint === clientFingerprint) return null;
|
|
1245
|
+
|
|
1246
|
+
const candidateFingerprints = new Set(
|
|
1247
|
+
(Array.isArray(repo?.remote_fingerprints) ? repo.remote_fingerprints : [])
|
|
1248
|
+
.map((value) => String(value || "").trim().toLowerCase())
|
|
1249
|
+
.filter(Boolean)
|
|
1250
|
+
);
|
|
1251
|
+
if (!candidateFingerprints.has(clientFingerprint.toLowerCase())) return null;
|
|
1252
|
+
if (candidateFingerprints.has(canonicalFingerprint.toLowerCase())) return null;
|
|
1253
|
+
|
|
1254
|
+
return canonicalRemoteUrl;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function resolveBaseRef(repoPath, preferredBranch) {
|
|
1258
|
+
const preferred = String(preferredBranch || "").trim();
|
|
1259
|
+
if (preferred) {
|
|
1260
|
+
if (runGitOk(repoPath, ["rev-parse", "--verify", `refs/remotes/origin/${preferred}`])) return `origin/${preferred}`;
|
|
1261
|
+
if (runGitOk(repoPath, ["rev-parse", "--verify", `refs/heads/${preferred}`])) return preferred;
|
|
1262
|
+
}
|
|
842
1263
|
if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/remotes/origin/main"])) return "origin/main";
|
|
843
1264
|
if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/remotes/origin/master"])) return "origin/master";
|
|
844
1265
|
if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/heads/main"])) return "main";
|
|
@@ -849,13 +1270,24 @@ function resolveBaseRef(repoPath) {
|
|
|
849
1270
|
function collectGitDiffWithDiagnostics({
|
|
850
1271
|
projectId,
|
|
851
1272
|
repoNames,
|
|
1273
|
+
repoBindings,
|
|
852
1274
|
maxChars,
|
|
853
1275
|
fetchRemote = true,
|
|
854
1276
|
} = {}) {
|
|
855
1277
|
const configuredRepos = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
|
|
1278
|
+
const configuredBindings = normalizeRepoBindings(repoBindings);
|
|
856
1279
|
const diagnostics = {
|
|
857
1280
|
project_id: projectId || null,
|
|
858
1281
|
requested_repo_names: configuredRepos,
|
|
1282
|
+
requested_repo_bindings: configuredBindings.map((binding) => ({
|
|
1283
|
+
role: binding.role,
|
|
1284
|
+
canonical_repo_name: binding.canonical_repo_name,
|
|
1285
|
+
canonical_remote_url: binding.canonical_remote_url,
|
|
1286
|
+
client_repo_name: binding.client_repo_name,
|
|
1287
|
+
client_remote_url: binding.client_remote_url,
|
|
1288
|
+
local_repo_aliases: binding.local_repo_aliases,
|
|
1289
|
+
baseline_branch: binding.baseline_branch,
|
|
1290
|
+
})),
|
|
859
1291
|
fetch_remote: Boolean(fetchRemote),
|
|
860
1292
|
mode: "none",
|
|
861
1293
|
search_root: null,
|
|
@@ -870,7 +1302,9 @@ function collectGitDiffWithDiagnostics({
|
|
|
870
1302
|
};
|
|
871
1303
|
|
|
872
1304
|
try {
|
|
873
|
-
const resolved =
|
|
1305
|
+
const resolved = configuredBindings.length
|
|
1306
|
+
? resolveConfiguredRepoBindings(configuredBindings)
|
|
1307
|
+
: resolveConfiguredRepos(configuredRepos);
|
|
874
1308
|
diagnostics.mode = resolved.mode || "none";
|
|
875
1309
|
diagnostics.search_root = resolved.root || null;
|
|
876
1310
|
diagnostics.found_repos = (resolved.repos || []).map((r) => r.name);
|
|
@@ -888,7 +1322,15 @@ function collectGitDiffWithDiagnostics({
|
|
|
888
1322
|
const sections = [];
|
|
889
1323
|
if (projectId) sections.push(`# Project: ${projectId}`);
|
|
890
1324
|
sections.push(`# Mode: ${resolved.mode}`);
|
|
891
|
-
|
|
1325
|
+
if (configuredBindings.length) {
|
|
1326
|
+
sections.push(
|
|
1327
|
+
`# Configured repo bindings: ${configuredBindings
|
|
1328
|
+
.map((binding) => `${binding.role}=${binding.client_repo_name || binding.canonical_repo_name || binding.role}`)
|
|
1329
|
+
.join(", ")}`
|
|
1330
|
+
);
|
|
1331
|
+
} else {
|
|
1332
|
+
sections.push(`# Configured repos: ${configuredRepos.join(", ") || ""}`);
|
|
1333
|
+
}
|
|
892
1334
|
if (resolved.missing && resolved.missing.length) {
|
|
893
1335
|
sections.push(`# Missing locally (skipped): ${resolved.missing.join(", ")}`);
|
|
894
1336
|
}
|
|
@@ -900,8 +1342,12 @@ function collectGitDiffWithDiagnostics({
|
|
|
900
1342
|
dir: repo.dir || ".",
|
|
901
1343
|
root: repo.abs,
|
|
902
1344
|
status: "ok",
|
|
1345
|
+
role: repo.role || null,
|
|
903
1346
|
head_branch: null,
|
|
904
1347
|
base_ref: null,
|
|
1348
|
+
base_ref_label: null,
|
|
1349
|
+
compare_mode: "local_origin",
|
|
1350
|
+
compare_remote_url: null,
|
|
905
1351
|
has_changes: false,
|
|
906
1352
|
changed_blocks: {
|
|
907
1353
|
base_vs_head: false,
|
|
@@ -914,22 +1360,55 @@ function collectGitDiffWithDiagnostics({
|
|
|
914
1360
|
};
|
|
915
1361
|
|
|
916
1362
|
const { name, dir, abs, prefix } = repo;
|
|
917
|
-
const
|
|
1363
|
+
const externalCompareRemoteUrl = resolveExternalCompareRemoteUrl(repo);
|
|
1364
|
+
const fetchDiag = {
|
|
1365
|
+
attempted: false,
|
|
1366
|
+
ok: false,
|
|
1367
|
+
message: "",
|
|
1368
|
+
compare_mode: externalCompareRemoteUrl ? "external_remote" : "local_origin",
|
|
1369
|
+
compare_remote_url: externalCompareRemoteUrl || null,
|
|
1370
|
+
};
|
|
1371
|
+
let baseRef = null;
|
|
1372
|
+
let baseRefLabel = null;
|
|
918
1373
|
if (fetchRemote) {
|
|
919
1374
|
fetchDiag.attempted = true;
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
fetchDiag.
|
|
923
|
-
|
|
924
|
-
|
|
1375
|
+
if (externalCompareRemoteUrl) {
|
|
1376
|
+
const externalBase = fetchExternalBaseRef(abs, externalCompareRemoteUrl, repo.baseline_branch);
|
|
1377
|
+
fetchDiag.ok = Boolean(externalBase?.ref);
|
|
1378
|
+
if (externalBase?.ref) {
|
|
1379
|
+
baseRef = externalBase.ref;
|
|
1380
|
+
baseRefLabel = `${externalBase.branch}@canonical`;
|
|
1381
|
+
} else {
|
|
1382
|
+
fetchDiag.message = `failed to refresh canonical compare branch from ${externalCompareRemoteUrl}`;
|
|
1383
|
+
repoSummary.errors.push(fetchDiag.message);
|
|
1384
|
+
diagnostics.warnings.push(`Repo "${name}": ${fetchDiag.message}`);
|
|
1385
|
+
}
|
|
1386
|
+
} else {
|
|
1387
|
+
fetchDiag.ok = fetchBaseBranches(abs, repo.baseline_branch);
|
|
1388
|
+
if (!fetchDiag.ok) {
|
|
1389
|
+
fetchDiag.message = "failed to refresh origin main/master";
|
|
1390
|
+
repoSummary.errors.push(fetchDiag.message);
|
|
1391
|
+
diagnostics.warnings.push(`Repo "${name}": ${fetchDiag.message}`);
|
|
1392
|
+
}
|
|
925
1393
|
}
|
|
926
1394
|
}
|
|
927
1395
|
|
|
928
1396
|
const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
|
|
929
|
-
|
|
1397
|
+
if (!baseRef && !externalCompareRemoteUrl) {
|
|
1398
|
+
baseRef = resolveBaseRef(abs, repo.baseline_branch);
|
|
1399
|
+
}
|
|
930
1400
|
repoSummary.head_branch = headBranch;
|
|
931
1401
|
repoSummary.base_ref = baseRef || "base-unresolved";
|
|
1402
|
+
repoSummary.base_ref_label = baseRefLabel || repoSummary.base_ref;
|
|
932
1403
|
repoSummary.fetch = fetchDiag;
|
|
1404
|
+
repoSummary.compare_mode = fetchDiag.compare_mode;
|
|
1405
|
+
repoSummary.compare_remote_url = fetchDiag.compare_remote_url;
|
|
1406
|
+
if (repo.matched_by) repoSummary.matched_by = repo.matched_by;
|
|
1407
|
+
if (repo.baseline_branch) repoSummary.baseline_branch = repo.baseline_branch;
|
|
1408
|
+
if (repo.canonical_repo_name) repoSummary.canonical_repo_name = repo.canonical_repo_name;
|
|
1409
|
+
if (repo.canonical_remote_url) repoSummary.canonical_remote_url = repo.canonical_remote_url;
|
|
1410
|
+
if (repo.client_repo_name) repoSummary.client_repo_name = repo.client_repo_name;
|
|
1411
|
+
if (repo.client_remote_url) repoSummary.client_remote_url = repo.client_remote_url;
|
|
933
1412
|
|
|
934
1413
|
let baseDiff = "";
|
|
935
1414
|
if (baseRef) {
|
|
@@ -999,8 +1478,16 @@ function collectGitDiffWithDiagnostics({
|
|
|
999
1478
|
repoSummary.status = "partial";
|
|
1000
1479
|
}
|
|
1001
1480
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1481
|
+
const headingParts = [`${name} (${dir || "."})`];
|
|
1482
|
+
if (repo.role) headingParts.push(`role=${repo.role}`);
|
|
1483
|
+
let section = `### ${headingParts.join(" | ")}\n\n`;
|
|
1484
|
+
section += `# ?? Base vs HEAD (${repoSummary.base_ref_label}...HEAD | head=${headBranch})\n`;
|
|
1485
|
+
if (repoSummary.matched_by?.length) {
|
|
1486
|
+
section += `# ?? Matched by ${repoSummary.matched_by.join(", ")}\n`;
|
|
1487
|
+
}
|
|
1488
|
+
if (repoSummary.compare_remote_url) {
|
|
1489
|
+
section += `# ?? Compare remote ${repoSummary.compare_remote_url}\n`;
|
|
1490
|
+
}
|
|
1004
1491
|
if (baseDiff) section += `${baseDiff}\n\n`;
|
|
1005
1492
|
if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
|
|
1006
1493
|
if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
|
|
@@ -1144,6 +1631,83 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
|
|
|
1144
1631
|
return body.data || {};
|
|
1145
1632
|
}
|
|
1146
1633
|
|
|
1634
|
+
async function fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId }) {
|
|
1635
|
+
const fetchFn = await getFetch();
|
|
1636
|
+
const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}`;
|
|
1637
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
1638
|
+
fetchFn,
|
|
1639
|
+
url,
|
|
1640
|
+
{
|
|
1641
|
+
method: "GET",
|
|
1642
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
1643
|
+
},
|
|
1644
|
+
timeoutMs
|
|
1645
|
+
);
|
|
1646
|
+
|
|
1647
|
+
if (!resp.ok || body.status !== "success") {
|
|
1648
|
+
const msg = body?.message || `Feedback review request failed (${resp.status})`;
|
|
1649
|
+
const err = new Error(msg);
|
|
1650
|
+
err.status = resp.status;
|
|
1651
|
+
throw err;
|
|
1652
|
+
}
|
|
1653
|
+
return body.data || {};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
async function fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId }) {
|
|
1657
|
+
const fetchFn = await getFetch();
|
|
1658
|
+
const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/history`;
|
|
1659
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
1660
|
+
fetchFn,
|
|
1661
|
+
url,
|
|
1662
|
+
{
|
|
1663
|
+
method: "GET",
|
|
1664
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
1665
|
+
},
|
|
1666
|
+
timeoutMs
|
|
1667
|
+
);
|
|
1668
|
+
|
|
1669
|
+
if (!resp.ok || body.status !== "success") {
|
|
1670
|
+
const msg = body?.message || `Feedback history request failed (${resp.status})`;
|
|
1671
|
+
const err = new Error(msg);
|
|
1672
|
+
err.status = resp.status;
|
|
1673
|
+
throw err;
|
|
1674
|
+
}
|
|
1675
|
+
return body.data || {};
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
async function postFeedbackRefinement({ apiBase, key, timeoutMs, feedbackId, mode, payload, idempotencyKey, clientSessionId }) {
|
|
1679
|
+
const fetchFn = await getFetch();
|
|
1680
|
+
const action = String(mode || "").trim();
|
|
1681
|
+
const url = `${apiBase}/project-assistant/feedback/${encodeURIComponent(String(feedbackId || ""))}/refinement/${action}`;
|
|
1682
|
+
const headers = {
|
|
1683
|
+
"Content-Type": "application/json",
|
|
1684
|
+
Authorization: `Bearer ${key}`,
|
|
1685
|
+
...(String(clientSessionId || "").trim() ? { "X-Client-Session-Id": String(clientSessionId).trim() } : {}),
|
|
1686
|
+
};
|
|
1687
|
+
if (idempotencyKey) {
|
|
1688
|
+
headers["X-Idempotency-Key"] = String(idempotencyKey).trim();
|
|
1689
|
+
}
|
|
1690
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
1691
|
+
fetchFn,
|
|
1692
|
+
url,
|
|
1693
|
+
{
|
|
1694
|
+
method: "POST",
|
|
1695
|
+
headers,
|
|
1696
|
+
body: JSON.stringify(payload || {}),
|
|
1697
|
+
},
|
|
1698
|
+
timeoutMs
|
|
1699
|
+
);
|
|
1700
|
+
|
|
1701
|
+
if (!resp.ok || body.status !== "success") {
|
|
1702
|
+
const msg = body?.message || `Feedback refinement ${action} failed (${resp.status})`;
|
|
1703
|
+
const err = new Error(msg);
|
|
1704
|
+
err.status = resp.status;
|
|
1705
|
+
err.data = body?.data;
|
|
1706
|
+
throw err;
|
|
1707
|
+
}
|
|
1708
|
+
return body.data || {};
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1147
1711
|
async function fetchSuggestionsSyncSnapshot({ apiBase, key, timeoutMs, actorScope = "" }) {
|
|
1148
1712
|
const fetchFn = await getFetch();
|
|
1149
1713
|
const url = new URL(`${apiBase}/project-assistant/suggestions`);
|
|
@@ -1469,7 +2033,7 @@ async function runRunQaqc(args) {
|
|
|
1469
2033
|
|
|
1470
2034
|
let resolved;
|
|
1471
2035
|
try {
|
|
1472
|
-
resolved =
|
|
2036
|
+
resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
1473
2037
|
} catch (err) {
|
|
1474
2038
|
console.error(err?.message || err);
|
|
1475
2039
|
process.exit(1);
|
|
@@ -2063,6 +2627,20 @@ function resolveBootstrapWorkspace(repoNames) {
|
|
|
2063
2627
|
return resolved;
|
|
2064
2628
|
}
|
|
2065
2629
|
|
|
2630
|
+
function resolvePortableWorkspace(repoNames) {
|
|
2631
|
+
const configured = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
|
|
2632
|
+
const resolved = resolveConfiguredRepos(configured);
|
|
2633
|
+
if (resolved.root) {
|
|
2634
|
+
return resolved;
|
|
2635
|
+
}
|
|
2636
|
+
return {
|
|
2637
|
+
mode: "cwd",
|
|
2638
|
+
root: process.cwd(),
|
|
2639
|
+
repos: [],
|
|
2640
|
+
missing: configured,
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2066
2644
|
function resolveCommandCenterRoots(wrapperRoot, outputDir) {
|
|
2067
2645
|
const targetRoot = outputDir
|
|
2068
2646
|
? path.resolve(process.cwd(), String(outputDir))
|
|
@@ -2301,6 +2879,150 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
2301
2879
|
};
|
|
2302
2880
|
}
|
|
2303
2881
|
|
|
2882
|
+
function resolveFeedbackReviewPaths(args, { requireFeedback = false } = {}) {
|
|
2883
|
+
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
2884
|
+
const targetRoot = outputDir
|
|
2885
|
+
? path.resolve(process.cwd(), String(outputDir))
|
|
2886
|
+
: findExistingCommandCenterRoot(process.cwd());
|
|
2887
|
+
if (!targetRoot) {
|
|
2888
|
+
if (requireFeedback) {
|
|
2889
|
+
console.error("feedback.yml was not found. Run `myte feedback-sync` first or pass --output-dir.");
|
|
2890
|
+
process.exit(1);
|
|
2891
|
+
}
|
|
2892
|
+
return null;
|
|
2893
|
+
}
|
|
2894
|
+
const dataRoot = path.join(targetRoot, "data");
|
|
2895
|
+
const feedbackPath = path.join(dataRoot, "feedback.yml");
|
|
2896
|
+
if (requireFeedback && !fs.existsSync(feedbackPath)) {
|
|
2897
|
+
console.error("feedback.yml was not found. Run `myte feedback-sync` first or pass --output-dir.");
|
|
2898
|
+
process.exit(1);
|
|
2899
|
+
}
|
|
2900
|
+
return {
|
|
2901
|
+
targetRoot,
|
|
2902
|
+
dataRoot,
|
|
2903
|
+
feedbackPath,
|
|
2904
|
+
reviewsRoot: path.join(targetRoot, "reviews", "feedback"),
|
|
2905
|
+
wrapperRoot: path.dirname(targetRoot),
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
function readFeedbackManifest(args) {
|
|
2910
|
+
const paths = resolveFeedbackReviewPaths(args, { requireFeedback: true });
|
|
2911
|
+
const manifest = readYamlFile(paths.feedbackPath) || {};
|
|
2912
|
+
if (!isPlainObject(manifest)) {
|
|
2913
|
+
console.error(`Invalid feedback manifest: ${paths.feedbackPath}`);
|
|
2914
|
+
process.exit(1);
|
|
2915
|
+
}
|
|
2916
|
+
return { paths, manifest };
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
function findFeedbackManifestItem(manifest, feedbackId) {
|
|
2920
|
+
const wanted = String(feedbackId || "").trim();
|
|
2921
|
+
if (!wanted) return null;
|
|
2922
|
+
const items = Array.isArray(manifest?.items) ? manifest.items : [];
|
|
2923
|
+
return items.find((item) => String(item?.feedback_id || item?.id || "").trim() === wanted) || null;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function readFeedbackRefinementArtifact(args) {
|
|
2927
|
+
const filePath = firstNonEmptyString(args.file);
|
|
2928
|
+
if (!filePath) {
|
|
2929
|
+
console.error("Missing --file for feedback validate/apply.");
|
|
2930
|
+
process.exit(1);
|
|
2931
|
+
}
|
|
2932
|
+
const absPath = resolveInputFile(filePath, "Feedback refinement");
|
|
2933
|
+
const text = fs.readFileSync(absPath, "utf8");
|
|
2934
|
+
let payload;
|
|
2935
|
+
if (absPath.toLowerCase().endsWith(".json")) {
|
|
2936
|
+
payload = JSON.parse(text);
|
|
2937
|
+
} else {
|
|
2938
|
+
payload = parseYaml(text);
|
|
2939
|
+
}
|
|
2940
|
+
if (!isPlainObject(payload)) {
|
|
2941
|
+
console.error(`Feedback refinement file must contain an object: ${absPath}`);
|
|
2942
|
+
process.exit(1);
|
|
2943
|
+
}
|
|
2944
|
+
return { absPath, payload };
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
function parseFeedbackTags(args) {
|
|
2948
|
+
const values = parseCsvValues(args.tags, args.tag);
|
|
2949
|
+
return values.length ? values : undefined;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
function feedbackChange(from, to) {
|
|
2953
|
+
return { from: from === undefined ? null : from, to: to === undefined ? null : to };
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
function buildFeedbackRefinementArtifact({ manifest, item, feedbackId, action, changes, reason, args }) {
|
|
2957
|
+
const clientSessionId = firstNonEmptyString(args["client-session-id"], args.clientSessionId, args.client_session_id);
|
|
2958
|
+
const artifact = {
|
|
2959
|
+
schema_version: 1,
|
|
2960
|
+
kind: "feedback_refinement",
|
|
2961
|
+
project_id: firstNonEmptyString(manifest?.project?.id, item?.project_id),
|
|
2962
|
+
feedback_id: feedbackId,
|
|
2963
|
+
title_snapshot: firstNonEmptyString(item?.title),
|
|
2964
|
+
base_snapshot_hash: firstNonEmptyString(item?.snapshot_hash),
|
|
2965
|
+
base_updated_at: firstNonEmptyString(item?.updated_at),
|
|
2966
|
+
review_action: action,
|
|
2967
|
+
reason: reason || null,
|
|
2968
|
+
changes,
|
|
2969
|
+
force: Boolean(args.force) || undefined,
|
|
2970
|
+
client_session_id: clientSessionId || undefined,
|
|
2971
|
+
created_at: new Date().toISOString(),
|
|
2972
|
+
};
|
|
2973
|
+
return Object.fromEntries(Object.entries(artifact).filter(([, value]) => value !== undefined));
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function writeFeedbackRefinementArtifact({ paths, artifact, action }) {
|
|
2977
|
+
const feedbackId = sanitizeFileSegment(artifact.feedback_id, "feedback");
|
|
2978
|
+
const actionSegment = sanitizeFileSegment(action || artifact.review_action || "refine", "refine");
|
|
2979
|
+
const filePath = path.join(paths.reviewsRoot, `${feedbackId}-${actionSegment}.yml`);
|
|
2980
|
+
writeYamlFile(filePath, artifact);
|
|
2981
|
+
return filePath;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
function printFeedbackRefinementDraft({ artifact, filePath, args }) {
|
|
2985
|
+
const output = {
|
|
2986
|
+
feedback_id: artifact.feedback_id,
|
|
2987
|
+
review_action: artifact.review_action,
|
|
2988
|
+
artifact_path: filePath || null,
|
|
2989
|
+
base_snapshot_hash: artifact.base_snapshot_hash || null,
|
|
2990
|
+
changes: artifact.changes || {},
|
|
2991
|
+
reason: artifact.reason || null,
|
|
2992
|
+
};
|
|
2993
|
+
if (args.json) {
|
|
2994
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
console.log(`Feedback: ${output.feedback_id}`);
|
|
2998
|
+
console.log(`Action: ${output.review_action}`);
|
|
2999
|
+
if (filePath) console.log(`Artifact: ${filePath}`);
|
|
3000
|
+
console.log(`Base Snapshot: ${output.base_snapshot_hash || "n/a"}`);
|
|
3001
|
+
console.log("Changes:");
|
|
3002
|
+
for (const [field, change] of Object.entries(output.changes || {})) {
|
|
3003
|
+
const fromValue = change && typeof change === "object" && "from" in change ? change.from : null;
|
|
3004
|
+
const toValue = change && typeof change === "object" && "to" in change ? change.to : change;
|
|
3005
|
+
console.log(`- ${field}: ${JSON.stringify(fromValue)} -> ${JSON.stringify(toValue)}`);
|
|
3006
|
+
}
|
|
3007
|
+
if (output.reason) console.log(`Reason: ${output.reason}`);
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
function summarizeFeedbackRefinementResult(data) {
|
|
3011
|
+
return {
|
|
3012
|
+
valid: Boolean(data?.valid),
|
|
3013
|
+
feedback_id: data?.feedback_id || data?.feedback?.feedback_id || null,
|
|
3014
|
+
project_id: data?.project_id || null,
|
|
3015
|
+
previous_snapshot_hash: data?.previous_snapshot_hash || data?.base_snapshot_hash || null,
|
|
3016
|
+
current_snapshot_hash: data?.current_snapshot_hash || null,
|
|
3017
|
+
new_snapshot_hash: data?.new_snapshot_hash || data?.feedback?.snapshot_hash || null,
|
|
3018
|
+
diff_count: Array.isArray(data?.diffs) ? data.diffs.length : 0,
|
|
3019
|
+
diffs: data?.diffs || [],
|
|
3020
|
+
warnings: data?.warnings || [],
|
|
3021
|
+
errors: data?.errors || [],
|
|
3022
|
+
history_id: data?.history?.history_id || null,
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2304
3026
|
function missionOpsThreads(payload) {
|
|
2305
3027
|
if (Array.isArray(payload?.threads)) return payload.threads;
|
|
2306
3028
|
if (Array.isArray(payload?.suggestions)) return payload.suggestions;
|
|
@@ -2545,7 +3267,7 @@ async function resolveSuggestionsOutputContext({ args, key, apiBase, timeoutMs,
|
|
|
2545
3267
|
const repoNames = Array.isArray(snapshot?.repo_names) && snapshot.repo_names.length
|
|
2546
3268
|
? snapshot.repo_names
|
|
2547
3269
|
: (await fetchProjectConfig({ apiBase, key, timeoutMs })).repo_names || [];
|
|
2548
|
-
const resolved =
|
|
3270
|
+
const resolved = resolvePortableWorkspace(repoNames);
|
|
2549
3271
|
return {
|
|
2550
3272
|
wrapperRoot: resolved.root,
|
|
2551
3273
|
targetRoot: path.join(resolved.root, "MyteCommandCenter"),
|
|
@@ -3155,11 +3877,15 @@ async function runConfig(args) {
|
|
|
3155
3877
|
}
|
|
3156
3878
|
|
|
3157
3879
|
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
3158
|
-
const
|
|
3880
|
+
const repoBindings = Array.isArray(cfg.repo_bindings) ? cfg.repo_bindings : [];
|
|
3881
|
+
const resolved = repoBindings.length
|
|
3882
|
+
? resolveConfiguredRepoBindings(repoBindings)
|
|
3883
|
+
: resolveConfiguredRepos(repoNames);
|
|
3159
3884
|
const payload = {
|
|
3160
3885
|
api_base: apiBase,
|
|
3161
3886
|
project_id: cfg.project_id,
|
|
3162
3887
|
repo_names: repoNames,
|
|
3888
|
+
repo_bindings: repoBindings,
|
|
3163
3889
|
local: {
|
|
3164
3890
|
mode: resolved.mode,
|
|
3165
3891
|
root: resolved.root,
|
|
@@ -3174,6 +3900,13 @@ async function runConfig(args) {
|
|
|
3174
3900
|
console.log(`Project: ${payload.project_id || "(unknown)"}`);
|
|
3175
3901
|
console.log(`API base: ${payload.api_base}`);
|
|
3176
3902
|
console.log(`Configured repos: ${repoNames.join(", ") || "(none)"}`);
|
|
3903
|
+
if (repoBindings.length) {
|
|
3904
|
+
console.log(
|
|
3905
|
+
`Repo bindings: ${repoBindings
|
|
3906
|
+
.map((binding) => `${binding.role}=${binding.client_repo_name || binding.canonical_repo_name || binding.role}`)
|
|
3907
|
+
.join(", ")}`
|
|
3908
|
+
);
|
|
3909
|
+
}
|
|
3177
3910
|
console.log(`Local mode: ${payload.local.mode}`);
|
|
3178
3911
|
if (payload.local.found.length) console.log(`Found locally: ${payload.local.found.join(", ")}`);
|
|
3179
3912
|
if (payload.local.missing.length) console.log(`Missing locally: ${payload.local.missing.join(", ")}`);
|
|
@@ -3209,7 +3942,7 @@ async function runBootstrap(args) {
|
|
|
3209
3942
|
|
|
3210
3943
|
let resolved;
|
|
3211
3944
|
try {
|
|
3212
|
-
resolved =
|
|
3945
|
+
resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
3213
3946
|
} catch (err) {
|
|
3214
3947
|
console.error(err?.message || err);
|
|
3215
3948
|
process.exit(1);
|
|
@@ -3303,7 +4036,7 @@ async function runSyncQaqc(args) {
|
|
|
3303
4036
|
|
|
3304
4037
|
let resolved;
|
|
3305
4038
|
try {
|
|
3306
|
-
resolved =
|
|
4039
|
+
resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
3307
4040
|
} catch (err) {
|
|
3308
4041
|
console.error(err?.message || err);
|
|
3309
4042
|
process.exit(1);
|
|
@@ -3397,7 +4130,7 @@ async function runFeedbackSync(args) {
|
|
|
3397
4130
|
|
|
3398
4131
|
let resolved;
|
|
3399
4132
|
try {
|
|
3400
|
-
resolved =
|
|
4133
|
+
resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
3401
4134
|
} catch (err) {
|
|
3402
4135
|
console.error(err?.message || err);
|
|
3403
4136
|
process.exit(1);
|
|
@@ -3463,6 +4196,251 @@ async function runFeedbackSync(args) {
|
|
|
3463
4196
|
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
3464
4197
|
}
|
|
3465
4198
|
|
|
4199
|
+
function resolveFeedbackIdArg(args) {
|
|
4200
|
+
return firstNonEmptyString(args["feedback-id"], args.feedbackId, args.feedback_id, args._?.[1]);
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4203
|
+
function requireFeedbackDraftContext(args) {
|
|
4204
|
+
const feedbackId = resolveFeedbackIdArg(args);
|
|
4205
|
+
if (!feedbackId) {
|
|
4206
|
+
console.error("Missing --feedback-id.");
|
|
4207
|
+
process.exit(1);
|
|
4208
|
+
}
|
|
4209
|
+
const { paths, manifest } = readFeedbackManifest(args);
|
|
4210
|
+
const item = findFeedbackManifestItem(manifest, feedbackId);
|
|
4211
|
+
if (!item) {
|
|
4212
|
+
console.error(`Feedback item not found in feedback.yml: ${feedbackId}. Run \`myte feedback-sync\` first.`);
|
|
4213
|
+
process.exit(1);
|
|
4214
|
+
}
|
|
4215
|
+
return { paths, manifest, item, feedbackId };
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
async function buildFeedbackDraftForCommand(args, subcommand) {
|
|
4219
|
+
const { paths, manifest, item, feedbackId } = requireFeedbackDraftContext(args);
|
|
4220
|
+
const action = subcommand || "refine";
|
|
4221
|
+
const reason = firstNonEmptyString(args.reason);
|
|
4222
|
+
const changes = {};
|
|
4223
|
+
|
|
4224
|
+
if (action === "status") {
|
|
4225
|
+
const targetStatus = firstNonEmptyString(args.status, args._?.[2]);
|
|
4226
|
+
if (!targetStatus) {
|
|
4227
|
+
console.error("Missing --status for feedback status.");
|
|
4228
|
+
process.exit(1);
|
|
4229
|
+
}
|
|
4230
|
+
changes.feedback_state = feedbackChange(firstNonEmptyString(item.feedback_state, item.status), targetStatus);
|
|
4231
|
+
} else if (action === "archive") {
|
|
4232
|
+
changes.feedback_state = feedbackChange(firstNonEmptyString(item.feedback_state, item.status), "archived");
|
|
4233
|
+
} else if (action === "assign") {
|
|
4234
|
+
const assignee = firstNonEmptyString(args["assigned-user-id"], args.assignedUserId, args.assigned_user_id, args["user-id"], args.userId, args.user_id, args._?.[2]);
|
|
4235
|
+
if (!assignee) {
|
|
4236
|
+
console.error("Missing --user-id or --assigned-user-id for feedback assign.");
|
|
4237
|
+
process.exit(1);
|
|
4238
|
+
}
|
|
4239
|
+
changes.assigned_user_id = feedbackChange(firstNonEmptyString(item.assigned_user_id), assignee);
|
|
4240
|
+
} else if (action === "edit" || action === "refine") {
|
|
4241
|
+
const title = firstNonEmptyString(args.title);
|
|
4242
|
+
if (title) changes.title = feedbackChange(firstNonEmptyString(item.title), title);
|
|
4243
|
+
|
|
4244
|
+
let feedbackText = firstNonEmptyString(args["feedback-text"], args.feedbackText, args.feedback_text);
|
|
4245
|
+
const bodyFile = firstNonEmptyString(args["body-file"], args.bodyFile, args.body_file);
|
|
4246
|
+
if (!feedbackText && bodyFile) {
|
|
4247
|
+
const bodyPath = resolveInputFile(bodyFile, "Feedback body");
|
|
4248
|
+
feedbackText = fs.readFileSync(bodyPath, "utf8").trim();
|
|
4249
|
+
}
|
|
4250
|
+
if (feedbackText) changes.feedback_text = feedbackChange(firstNonEmptyString(item.feedback_text), feedbackText);
|
|
4251
|
+
|
|
4252
|
+
const priority = firstNonEmptyString(args.priority);
|
|
4253
|
+
if (priority) changes.priority = feedbackChange(firstNonEmptyString(item.priority), priority);
|
|
4254
|
+
|
|
4255
|
+
const dueDate = firstNonEmptyString(args["due-date"], args.dueDate, args.due_date);
|
|
4256
|
+
if (dueDate) changes.due_date = feedbackChange(firstNonEmptyString(item.due_date, item.due_date_text), dueDate);
|
|
4257
|
+
|
|
4258
|
+
const tags = parseFeedbackTags(args);
|
|
4259
|
+
if (tags) changes.tags = feedbackChange(Array.isArray(item.tags) ? item.tags : [], tags);
|
|
4260
|
+
|
|
4261
|
+
const reviewNote = firstNonEmptyString(args["review-note"], args.reviewNote, args.review_note);
|
|
4262
|
+
if (reviewNote) changes.review_note = feedbackChange(null, reviewNote);
|
|
4263
|
+
} else {
|
|
4264
|
+
console.error("Unknown feedback command. Use status, edit, assign, archive, get, history, validate, or apply.");
|
|
4265
|
+
process.exit(1);
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
if (!Object.keys(changes).length) {
|
|
4269
|
+
console.error("No feedback changes were provided.");
|
|
4270
|
+
process.exit(1);
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
const artifact = buildFeedbackRefinementArtifact({
|
|
4274
|
+
manifest,
|
|
4275
|
+
item,
|
|
4276
|
+
feedbackId,
|
|
4277
|
+
action,
|
|
4278
|
+
changes,
|
|
4279
|
+
reason,
|
|
4280
|
+
args,
|
|
4281
|
+
});
|
|
4282
|
+
|
|
4283
|
+
if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
|
|
4284
|
+
if (args.json) {
|
|
4285
|
+
console.log(JSON.stringify(artifact, null, 2));
|
|
4286
|
+
} else {
|
|
4287
|
+
console.log(stringifyYaml(artifact));
|
|
4288
|
+
}
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
const filePath = writeFeedbackRefinementArtifact({ paths, artifact, action });
|
|
4293
|
+
printFeedbackRefinementDraft({ artifact, filePath, args });
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
async function runFeedbackValidateOrApply(args, mode) {
|
|
4297
|
+
const key = getProjectApiKey();
|
|
4298
|
+
if (!key) {
|
|
4299
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
4300
|
+
process.exit(1);
|
|
4301
|
+
}
|
|
4302
|
+
const { absPath, payload } = readFeedbackRefinementArtifact(args);
|
|
4303
|
+
const feedbackId = firstNonEmptyString(args["feedback-id"], args.feedbackId, args.feedback_id, payload.feedback_id);
|
|
4304
|
+
if (!feedbackId) {
|
|
4305
|
+
console.error("Feedback refinement artifact is missing feedback_id.");
|
|
4306
|
+
process.exit(1);
|
|
4307
|
+
}
|
|
4308
|
+
if (args.force) payload.force = true;
|
|
4309
|
+
|
|
4310
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
4311
|
+
const apiBase = resolveApiBase(args);
|
|
4312
|
+
const clientSessionId = firstNonEmptyString(
|
|
4313
|
+
args["client-session-id"],
|
|
4314
|
+
args.clientSessionId,
|
|
4315
|
+
args.client_session_id,
|
|
4316
|
+
payload.client_session_id
|
|
4317
|
+
);
|
|
4318
|
+
const idempotencyKey = mode === "apply"
|
|
4319
|
+
? resolveProjectMutationIdempotencyKey({
|
|
4320
|
+
args,
|
|
4321
|
+
operation: `feedback_refinement_apply:${feedbackId}`,
|
|
4322
|
+
payload,
|
|
4323
|
+
})
|
|
4324
|
+
: null;
|
|
4325
|
+
|
|
4326
|
+
let data;
|
|
4327
|
+
try {
|
|
4328
|
+
data = await postFeedbackRefinement({
|
|
4329
|
+
apiBase,
|
|
4330
|
+
key,
|
|
4331
|
+
timeoutMs,
|
|
4332
|
+
feedbackId,
|
|
4333
|
+
mode,
|
|
4334
|
+
payload,
|
|
4335
|
+
idempotencyKey,
|
|
4336
|
+
clientSessionId,
|
|
4337
|
+
});
|
|
4338
|
+
} catch (err) {
|
|
4339
|
+
if (args.json) {
|
|
4340
|
+
console.log(JSON.stringify({
|
|
4341
|
+
ok: false,
|
|
4342
|
+
status: err?.status || null,
|
|
4343
|
+
message: err?.message || String(err),
|
|
4344
|
+
data: err?.data || null,
|
|
4345
|
+
artifact_path: absPath,
|
|
4346
|
+
}, null, 2));
|
|
4347
|
+
} else {
|
|
4348
|
+
console.error(`Feedback ${mode} failed:`, err?.message || err);
|
|
4349
|
+
if (err?.data) console.error(JSON.stringify(err.data, null, 2));
|
|
4350
|
+
}
|
|
4351
|
+
process.exit(1);
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
const summary = {
|
|
4355
|
+
ok: true,
|
|
4356
|
+
mode,
|
|
4357
|
+
artifact_path: absPath,
|
|
4358
|
+
...summarizeFeedbackRefinementResult(data),
|
|
4359
|
+
};
|
|
4360
|
+
if (args.json) {
|
|
4361
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
console.log(`Feedback ${mode}: ${summary.valid ? "valid" : "invalid"}`);
|
|
4365
|
+
console.log(`Feedback: ${summary.feedback_id || feedbackId}`);
|
|
4366
|
+
if (summary.history_id) console.log(`History: ${summary.history_id}`);
|
|
4367
|
+
if (summary.new_snapshot_hash) console.log(`New Snapshot: ${summary.new_snapshot_hash}`);
|
|
4368
|
+
if (summary.current_snapshot_hash) console.log(`Current Snapshot: ${summary.current_snapshot_hash}`);
|
|
4369
|
+
console.log(`Diffs: ${summary.diff_count}`);
|
|
4370
|
+
for (const diff of summary.diffs || []) {
|
|
4371
|
+
console.log(`- ${diff.field}: ${JSON.stringify(diff.from)} -> ${JSON.stringify(diff.to)}`);
|
|
4372
|
+
}
|
|
4373
|
+
if (mode === "apply") {
|
|
4374
|
+
console.log("Local feedback.yml is not modified by apply. Run `myte feedback-sync` to refresh it.");
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
async function runFeedbackGetOrHistory(args, mode) {
|
|
4379
|
+
const key = getProjectApiKey();
|
|
4380
|
+
if (!key) {
|
|
4381
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
4382
|
+
process.exit(1);
|
|
4383
|
+
}
|
|
4384
|
+
const feedbackId = resolveFeedbackIdArg(args);
|
|
4385
|
+
if (!feedbackId) {
|
|
4386
|
+
console.error("Missing --feedback-id.");
|
|
4387
|
+
process.exit(1);
|
|
4388
|
+
}
|
|
4389
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
4390
|
+
const apiBase = resolveApiBase(args);
|
|
4391
|
+
|
|
4392
|
+
let data;
|
|
4393
|
+
try {
|
|
4394
|
+
data = mode === "history"
|
|
4395
|
+
? await fetchFeedbackHistory({ apiBase, key, timeoutMs, feedbackId })
|
|
4396
|
+
: await fetchFeedbackReview({ apiBase, key, timeoutMs, feedbackId });
|
|
4397
|
+
} catch (err) {
|
|
4398
|
+
console.error(`Feedback ${mode} failed:`, err?.message || err);
|
|
4399
|
+
process.exit(1);
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
if (args.json) {
|
|
4403
|
+
console.log(JSON.stringify(data, null, 2));
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
if (mode === "history") {
|
|
4407
|
+
console.log(`Feedback: ${data.feedback_id || feedbackId}`);
|
|
4408
|
+
console.log(`Events: ${data.count || 0}`);
|
|
4409
|
+
for (const event of data.events || []) {
|
|
4410
|
+
console.log(`- ${event.created_at || "unknown"} ${event.action || "refine"} ${event.history_id || ""}`);
|
|
4411
|
+
}
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
const feedback = data.feedback || {};
|
|
4415
|
+
console.log(`Feedback: ${feedback.feedback_id || feedbackId}`);
|
|
4416
|
+
console.log(`Title: ${feedback.title || "(untitled)"}`);
|
|
4417
|
+
console.log(`Status: ${feedback.feedback_state || feedback.status || "unknown"} (${feedback.status || "legacy unknown"})`);
|
|
4418
|
+
console.log(`Priority: ${feedback.priority || "n/a"}`);
|
|
4419
|
+
console.log(`Snapshot: ${feedback.snapshot_hash || "n/a"}`);
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
async function runFeedback(args) {
|
|
4423
|
+
const subcommand = firstNonEmptyString(args._?.[0]) || "help";
|
|
4424
|
+
if (subcommand === "help") {
|
|
4425
|
+
printHelp();
|
|
4426
|
+
return;
|
|
4427
|
+
}
|
|
4428
|
+
if (["status", "edit", "assign", "archive", "refine"].includes(subcommand)) {
|
|
4429
|
+
await buildFeedbackDraftForCommand(args, subcommand);
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
4432
|
+
if (subcommand === "validate" || subcommand === "apply") {
|
|
4433
|
+
await runFeedbackValidateOrApply(args, subcommand);
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
if (subcommand === "get" || subcommand === "history") {
|
|
4437
|
+
await runFeedbackGetOrHistory(args, subcommand);
|
|
4438
|
+
return;
|
|
4439
|
+
}
|
|
4440
|
+
console.error("Unknown feedback command. Use status, edit, assign, archive, get, history, validate, or apply.");
|
|
4441
|
+
process.exit(1);
|
|
4442
|
+
}
|
|
4443
|
+
|
|
3466
4444
|
async function runSuggestionsSync(args) {
|
|
3467
4445
|
const key = getProjectApiKey();
|
|
3468
4446
|
if (!key) {
|
|
@@ -4037,30 +5015,41 @@ async function runQuery(args) {
|
|
|
4037
5015
|
try {
|
|
4038
5016
|
cfg = await fetchProjectConfig({ apiBase, key, timeoutMs });
|
|
4039
5017
|
} catch (err) {
|
|
4040
|
-
console.
|
|
4041
|
-
|
|
4042
|
-
cfg = null;
|
|
5018
|
+
console.error("Failed to fetch project config for --with-diff:", err?.message || err);
|
|
5019
|
+
process.exit(1);
|
|
4043
5020
|
}
|
|
4044
5021
|
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
fetchRemote,
|
|
4052
|
-
});
|
|
4053
|
-
diffText = diffResult.text;
|
|
4054
|
-
diffDiagnostics = diffResult.diagnostics;
|
|
4055
|
-
if (diffDiagnostics?.errors?.length) {
|
|
4056
|
-
for (const warning of diffDiagnostics.errors) console.warn(`Warning: ${warning}`);
|
|
4057
|
-
}
|
|
4058
|
-
} else {
|
|
4059
|
-
diffText = "";
|
|
4060
|
-
diffDiagnostics = null;
|
|
5022
|
+
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
5023
|
+
const repoBindings = Array.isArray(cfg.repo_bindings) ? cfg.repo_bindings : [];
|
|
5024
|
+
if (!repoBindings.length && !repoNames.length) {
|
|
5025
|
+
console.error("No project repositories are configured for --with-diff.");
|
|
5026
|
+
console.error("Ask the project owner or builder to configure the project repos in Myte first.");
|
|
5027
|
+
process.exit(1);
|
|
4061
5028
|
}
|
|
4062
|
-
|
|
4063
|
-
|
|
5029
|
+
|
|
5030
|
+
const diffResult = collectGitDiffWithDiagnostics({
|
|
5031
|
+
projectId: cfg.project_id,
|
|
5032
|
+
repoNames,
|
|
5033
|
+
repoBindings,
|
|
5034
|
+
maxChars: diffLimit,
|
|
5035
|
+
fetchRemote,
|
|
5036
|
+
});
|
|
5037
|
+
diffText = diffResult.text;
|
|
5038
|
+
diffDiagnostics = diffResult.diagnostics;
|
|
5039
|
+
if (diffDiagnostics?.errors?.length) {
|
|
5040
|
+
for (const warning of diffDiagnostics.errors) console.warn(`Warning: ${warning}`);
|
|
5041
|
+
}
|
|
5042
|
+
if (!Array.isArray(diffDiagnostics?.found_repos) || !diffDiagnostics.found_repos.length) {
|
|
5043
|
+
console.error("No configured project repos were found locally for --with-diff.");
|
|
5044
|
+
const expectedBindings = Array.isArray(diffDiagnostics?.requested_repo_bindings)
|
|
5045
|
+
? diffDiagnostics.requested_repo_bindings
|
|
5046
|
+
.map((binding) => binding.client_repo_name || binding.canonical_repo_name || binding.role)
|
|
5047
|
+
.filter(Boolean)
|
|
5048
|
+
: [];
|
|
5049
|
+
const expected = expectedBindings.length ? expectedBindings : repoNames;
|
|
5050
|
+
console.error(`Expected repo mapping or folder match for: ${expected.join(", ")}`);
|
|
5051
|
+
console.error("Run `myte config --json` to inspect the configured repos and local resolution.");
|
|
5052
|
+
process.exit(1);
|
|
4064
5053
|
}
|
|
4065
5054
|
}
|
|
4066
5055
|
|
|
@@ -4193,6 +5182,11 @@ async function main() {
|
|
|
4193
5182
|
return;
|
|
4194
5183
|
}
|
|
4195
5184
|
|
|
5185
|
+
if (command === "feedback") {
|
|
5186
|
+
await runFeedback(args);
|
|
5187
|
+
return;
|
|
5188
|
+
}
|
|
5189
|
+
|
|
4196
5190
|
if (command === "suggestions") {
|
|
4197
5191
|
await runSuggestions(args);
|
|
4198
5192
|
return;
|