vibe-coding-master 0.8.13 → 0.9.1
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 +7 -3
- package/dist/backend/adapters/claude-adapter.js +2 -1
- package/dist/backend/cli/install-vcm-harness.js +66 -16
- package/dist/backend/gateway/gateway-service.js +5 -3
- package/dist/backend/runtime/terminal-submit.js +1 -0
- package/dist/backend/server.js +14 -4
- package/dist/backend/services/app-settings-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +4 -3
- package/dist/backend/services/claude-hook-service.js +6 -0
- package/dist/backend/services/claude-transcript-service.js +2 -13
- package/dist/backend/services/claude-version-check.js +87 -0
- package/dist/backend/services/gate-review-service.js +46 -8
- package/dist/backend/services/harness-service.js +41 -20
- package/dist/backend/services/message-service.js +30 -6
- package/dist/backend/services/project-service.js +8 -2
- package/dist/backend/services/runtime-recovery-service.js +36 -9
- package/dist/backend/services/session-service.js +10 -19
- package/dist/backend/services/task-close-service.js +1 -0
- package/dist/backend/services/terminal-process-exit-service.js +4 -1
- package/dist/backend/services/workflow-control-service.js +55 -11
- package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
- package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
- package/dist/backend/templates/harness/gate-review.js +676 -643
- package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
- package/dist/backend/templates/harness/project-owned-history.js +21 -0
- package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
- package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
- package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
- package/dist/shared/types/app-settings.js +31 -0
- package/dist-frontend/assets/index-Bs4g_1yF.css +32 -0
- package/dist-frontend/assets/{index-DqANomt1.js → index-CjM9OqZh.js} +41 -41
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/check-durable-docs +332 -298
- package/scripts/harness-tools/generate-module-index +1047 -512
- package/scripts/harness-tools/generate-public-surface +1231 -1069
- package/scripts/harness-tools/package.json +3 -0
- package/scripts/harness-tools/run-long-check +608 -531
- package/scripts/harness-tools/vcm-artifact +181 -113
- package/scripts/harness-tools/vcm-bash-guard +839 -605
- package/scripts/harness-tools/vcm-subagent-guard +67 -63
- package/scripts/harness-tools/watch-job +313 -238
- package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NODE_TOOL_ARGUMENT_PARSER, NODE_TOOL_HTTP, renderNodeToolHeader } from "./node-tool-runtime.js";
|
|
1
2
|
import { renderRoleMemoryRules } from "./role-memory.js";
|
|
2
3
|
import { GATE_ANALYSIS_FIELDS, GATE_ANALYSIS_HEADINGS } from "../../services/managed-artifact-validation.js";
|
|
3
4
|
function renderGateAnalysisSections() {
|
|
@@ -494,665 +495,697 @@ When VCM sends \`[VCM GATE REVIEW CALLBACK]\`, read the named report path.
|
|
|
494
495
|
Do not ask Reviewer to choose owners, fixes, Replan, or user-intervention needs. PM routes those decisions through normal VCM responsibilities.`;
|
|
495
496
|
}
|
|
496
497
|
export function renderRequestGateReviewTool() {
|
|
497
|
-
return
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
"
|
|
519
|
-
"
|
|
520
|
-
|
|
498
|
+
return String.raw `${renderNodeToolHeader("Request a VCM-managed Gate Review Gate.")}${NODE_TOOL_ARGUMENT_PARSER}${NODE_TOOL_HTTP}const crypto = require("node:crypto");
|
|
499
|
+
const childProcess = require("node:child_process");
|
|
500
|
+
const fs = require("node:fs");
|
|
501
|
+
const path = require("node:path");
|
|
502
|
+
|
|
503
|
+
const GATES = ["architecture-plan", "validation-adequacy", "code-diff"];
|
|
504
|
+
const CODE_DIFF_SOURCES = ["coder", "architect-debug", "architect-diagnosis"];
|
|
505
|
+
const HARNESS_COMMIT_PREFIX = "[VCM Harness] ";
|
|
506
|
+
const LATEST_REPORTS = {
|
|
507
|
+
"architecture-plan": ".ai/vcm/gate-reviews/architecture-plan-review.md",
|
|
508
|
+
"validation-adequacy": ".ai/vcm/gate-reviews/validation-adequacy-review.md",
|
|
509
|
+
"code-diff": ".ai/vcm/gate-reviews/code-diff-review.md"
|
|
510
|
+
};
|
|
511
|
+
const SOURCE_ARTIFACTS = {
|
|
512
|
+
"architecture-plan": [
|
|
513
|
+
".ai/vcm/handoffs/architecture-brief.md",
|
|
514
|
+
".ai/vcm/handoffs/architecture-evidence.md",
|
|
515
|
+
".ai/vcm/handoffs/architecture-plan.md"
|
|
516
|
+
],
|
|
517
|
+
"validation-adequacy": [
|
|
518
|
+
".ai/vcm/handoffs/architecture-plan.md",
|
|
519
|
+
".ai/vcm/handoffs/test-report.md",
|
|
520
|
+
"docs/TESTING.md"
|
|
521
|
+
],
|
|
522
|
+
"code-diff": [
|
|
523
|
+
".ai/vcm/handoffs/test-report.md",
|
|
524
|
+
".ai/vcm/gate-reviews/validation-adequacy-review.md"
|
|
525
|
+
]
|
|
526
|
+
};
|
|
527
|
+
const CODE_DIFF_SOURCE_ARTIFACTS = {
|
|
528
|
+
"coder": [
|
|
529
|
+
".ai/vcm/handoffs/architecture-plan.md",
|
|
530
|
+
".ai/vcm/handoffs/coder-completion.md"
|
|
531
|
+
],
|
|
532
|
+
"architect-debug": [
|
|
533
|
+
".ai/vcm/handoffs/role-commands/architect.md",
|
|
534
|
+
".ai/vcm/handoffs/architect-debug.md"
|
|
535
|
+
],
|
|
536
|
+
"architect-diagnosis": [".ai/vcm/handoffs/architecture-diagnosis.md"]
|
|
537
|
+
};
|
|
538
|
+
const CORE_INPUT_ARTIFACTS = {
|
|
539
|
+
"architecture-plan": ".ai/vcm/handoffs/architecture-plan.md",
|
|
540
|
+
"validation-adequacy": ".ai/vcm/handoffs/test-report.md"
|
|
541
|
+
};
|
|
542
|
+
const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024 * 1024;
|
|
543
|
+
|
|
544
|
+
function rootDir() {
|
|
545
|
+
return path.resolve(path.dirname(fs.realpathSync(__filename)), "..", "..");
|
|
521
546
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
".ai/vcm/handoffs/architecture-evidence.md",
|
|
526
|
-
".ai/vcm/handoffs/architecture-plan.md",
|
|
527
|
-
],
|
|
528
|
-
"validation-adequacy": [
|
|
529
|
-
".ai/vcm/handoffs/architecture-plan.md",
|
|
530
|
-
".ai/vcm/handoffs/test-report.md",
|
|
531
|
-
"docs/TESTING.md",
|
|
532
|
-
],
|
|
533
|
-
"code-diff": [
|
|
534
|
-
".ai/vcm/handoffs/test-report.md",
|
|
535
|
-
".ai/vcm/gate-reviews/validation-adequacy-review.md",
|
|
536
|
-
],
|
|
547
|
+
|
|
548
|
+
function nowIso() {
|
|
549
|
+
return new Date().toISOString();
|
|
537
550
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
".ai/vcm/handoffs/coder-completion.md",
|
|
542
|
-
],
|
|
543
|
-
"architect-debug": [
|
|
544
|
-
".ai/vcm/handoffs/role-commands/architect.md",
|
|
545
|
-
".ai/vcm/handoffs/architect-debug.md",
|
|
546
|
-
],
|
|
547
|
-
"architect-diagnosis": [".ai/vcm/handoffs/architecture-diagnosis.md"],
|
|
551
|
+
|
|
552
|
+
function isObject(value) {
|
|
553
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
548
554
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
555
|
+
|
|
556
|
+
// Python truthiness, so empty lists and objects in the index count as false.
|
|
557
|
+
function truthy(value) {
|
|
558
|
+
if (Array.isArray(value)) {
|
|
559
|
+
return value.length > 0;
|
|
560
|
+
}
|
|
561
|
+
if (isObject(value)) {
|
|
562
|
+
return Object.keys(value).length > 0;
|
|
563
|
+
}
|
|
564
|
+
return Boolean(value);
|
|
552
565
|
}
|
|
553
566
|
|
|
567
|
+
function unique(values) {
|
|
568
|
+
return [...new Set(values)];
|
|
569
|
+
}
|
|
554
570
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
print(status)
|
|
565
|
-
for key, value in fields.items():
|
|
566
|
-
if value is not None:
|
|
567
|
-
print(f"{key}={value}")
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
def call_vcm_api(gate: str, source: str | None) -> int | None:
|
|
571
|
-
base_url = os.environ.get("VCM_API_URL")
|
|
572
|
-
task_slug = os.environ.get("VCM_TASK_SLUG")
|
|
573
|
-
if not base_url or not task_slug:
|
|
574
|
-
return None
|
|
575
|
-
|
|
576
|
-
url = (
|
|
577
|
-
base_url.rstrip("/")
|
|
578
|
-
+ "/api/tasks/"
|
|
579
|
-
+ urllib.parse.quote(task_slug, safe="")
|
|
580
|
-
+ "/gate-review/"
|
|
581
|
-
+ urllib.parse.quote(gate, safe="")
|
|
582
|
-
+ "/request"
|
|
583
|
-
)
|
|
584
|
-
request = urllib.request.Request(
|
|
585
|
-
url,
|
|
586
|
-
data=json.dumps({"codeDiffSource": source}).encode("utf-8"),
|
|
587
|
-
method="POST",
|
|
588
|
-
headers={"content-type": "application/json"},
|
|
589
|
-
)
|
|
590
|
-
try:
|
|
591
|
-
with urllib.request.urlopen(request, timeout=10) as response:
|
|
592
|
-
payload = json.loads(response.read().decode("utf-8"))
|
|
593
|
-
except urllib.error.HTTPError as error:
|
|
594
|
-
try:
|
|
595
|
-
payload = json.loads(error.read().decode("utf-8"))
|
|
596
|
-
reason = payload.get("error", {}).get("message", str(error))
|
|
597
|
-
except Exception:
|
|
598
|
-
reason = str(error)
|
|
599
|
-
print_result("failed_to_start", gate=gate, reason=reason)
|
|
600
|
-
return 2
|
|
601
|
-
except Exception as error:
|
|
602
|
-
print_result("failed_to_start", gate=gate, reason=str(error))
|
|
603
|
-
return 2
|
|
604
|
-
|
|
605
|
-
record = payload.get("record", {}) if isinstance(payload, dict) else {}
|
|
606
|
-
print_result(
|
|
607
|
-
payload.get("status", "failed_to_start"),
|
|
608
|
-
gate=gate,
|
|
609
|
-
request=record.get("requestPath"),
|
|
610
|
-
report=record.get("reportPath"),
|
|
611
|
-
message=payload.get("message"),
|
|
612
|
-
)
|
|
613
|
-
return 0
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
def read_json(path: Path) -> dict:
|
|
617
|
-
try:
|
|
618
|
-
return json.loads(path.read_text())
|
|
619
|
-
except (OSError, ValueError):
|
|
620
|
-
return {"version": 1, "enabled": False, "activeGate": None, "gates": {}}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
def write_json(path: Path, data: dict) -> None:
|
|
624
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
625
|
-
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
626
|
-
tmp.write_text(json.dumps(data, indent=2, sort_keys=True) + "\\n")
|
|
627
|
-
tmp.replace(path)
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
def command_output(root: Path, command: list[str]) -> bytes:
|
|
631
|
-
result = subprocess.run(
|
|
632
|
-
command,
|
|
633
|
-
cwd=root,
|
|
634
|
-
check=False,
|
|
635
|
-
stdout=subprocess.PIPE,
|
|
636
|
-
stderr=subprocess.DEVNULL,
|
|
637
|
-
)
|
|
638
|
-
return result.stdout if result.returncode == 0 else b""
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
def command_text(root: Path, command: list[str]) -> str:
|
|
642
|
-
return command_output(root, command).decode("utf-8", errors="replace").strip()
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
def is_ancestor(root: Path, ancestor: str, descendant: str) -> bool:
|
|
646
|
-
result = subprocess.run(
|
|
647
|
-
["git", "merge-base", "--is-ancestor", ancestor, descendant],
|
|
648
|
-
cwd=root,
|
|
649
|
-
check=False,
|
|
650
|
-
stdout=subprocess.DEVNULL,
|
|
651
|
-
stderr=subprocess.DEVNULL,
|
|
652
|
-
)
|
|
653
|
-
return result.returncode == 0
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
def code_diff_range(root: Path, gate_record: dict):
|
|
657
|
-
head = command_text(root, ["git", "rev-parse", "HEAD"])
|
|
658
|
-
if not head:
|
|
659
|
-
return (None, None)
|
|
660
|
-
|
|
661
|
-
base = None
|
|
662
|
-
if (
|
|
663
|
-
gate_record.get("status") == "completed"
|
|
664
|
-
and gate_record.get("decision") == "request_changes"
|
|
665
|
-
and gate_record.get("baseCommit")
|
|
666
|
-
and is_ancestor(root, gate_record["baseCommit"], head)
|
|
667
|
-
):
|
|
668
|
-
base = gate_record["baseCommit"]
|
|
669
|
-
elif (
|
|
670
|
-
gate_record.get("status") == "failed"
|
|
671
|
-
and gate_record.get("baseCommit")
|
|
672
|
-
and is_ancestor(root, gate_record["baseCommit"], head)
|
|
673
|
-
):
|
|
674
|
-
base = gate_record["baseCommit"]
|
|
675
|
-
elif (
|
|
676
|
-
gate_record.get("status") == "completed"
|
|
677
|
-
and gate_record.get("decision") == "approve"
|
|
678
|
-
and gate_record.get("headCommit")
|
|
679
|
-
and is_ancestor(root, gate_record["headCommit"], head)
|
|
680
|
-
):
|
|
681
|
-
base = gate_record["headCommit"]
|
|
682
|
-
elif (
|
|
683
|
-
gate_record.get("status") == "not_required"
|
|
684
|
-
and gate_record.get("headCommit")
|
|
685
|
-
and is_ancestor(root, gate_record["headCommit"], head)
|
|
686
|
-
):
|
|
687
|
-
base = gate_record["headCommit"]
|
|
688
|
-
else:
|
|
689
|
-
base = os.environ.get("VCM_BASE_COMMIT", "").strip()
|
|
690
|
-
if not base or not is_ancestor(root, base, head):
|
|
691
|
-
upstream = command_text(root, ["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"])
|
|
692
|
-
base = command_text(root, ["git", "merge-base", "HEAD", upstream]) if upstream else ""
|
|
693
|
-
|
|
694
|
-
return (base or head, head)
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
def normalize_code_diff_sources(gate_record: dict) -> list[str]:
|
|
698
|
-
sources = gate_record.get("codeDiffSources")
|
|
699
|
-
normalized = [item for item in sources if item in CODE_DIFF_SOURCES] if isinstance(sources, list) else []
|
|
700
|
-
source = gate_record.get("codeDiffSource")
|
|
701
|
-
if not normalized and source in CODE_DIFF_SOURCES:
|
|
702
|
-
normalized.append(source)
|
|
703
|
-
return list(dict.fromkeys(normalized))
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
def code_diff_sources(gate_record: dict, source: str | None, code_diff: dict) -> list[str]:
|
|
707
|
-
if source not in CODE_DIFF_SOURCES:
|
|
708
|
-
return []
|
|
709
|
-
continuing_recorded_range = (
|
|
710
|
-
gate_record.get("baseCommit") == code_diff.get("baseCommit")
|
|
711
|
-
and (
|
|
712
|
-
(gate_record.get("status") == "completed" and gate_record.get("decision") == "request_changes")
|
|
713
|
-
or gate_record.get("status") == "failed"
|
|
714
|
-
)
|
|
715
|
-
)
|
|
716
|
-
previous = normalize_code_diff_sources(gate_record) if continuing_recorded_range else []
|
|
717
|
-
return list(dict.fromkeys([*previous, source]))
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
def source_artifacts(gate: str, sources: list[str] | None) -> list[str]:
|
|
721
|
-
if gate != "code-diff":
|
|
722
|
-
return SOURCE_ARTIFACTS[gate]
|
|
723
|
-
return list(dict.fromkeys([
|
|
724
|
-
*SOURCE_ARTIFACTS["code-diff"],
|
|
725
|
-
*(
|
|
726
|
-
artifact
|
|
727
|
-
for source in (sources or [])
|
|
728
|
-
for artifact in CODE_DIFF_SOURCE_ARTIFACTS.get(source, [])
|
|
729
|
-
),
|
|
730
|
-
]))
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
def reviewable_code_diff(root: Path, base: str, head: str) -> dict | None:
|
|
734
|
-
all_commits = command_text(root, ["git", "log", "--oneline", "--reverse", f"{base}..{head}"]).splitlines()
|
|
735
|
-
if not all_commits:
|
|
736
|
-
return None
|
|
737
|
-
commits = []
|
|
738
|
-
commit_shas = []
|
|
739
|
-
changed_files = []
|
|
740
|
-
diff_stats = []
|
|
741
|
-
patches = []
|
|
742
|
-
for line in all_commits:
|
|
743
|
-
_, separator, subject = line.partition(" ")
|
|
744
|
-
if separator and subject.startswith(HARNESS_COMMIT_PREFIX):
|
|
745
|
-
continue
|
|
746
|
-
commit_sha = line.split(maxsplit=1)[0]
|
|
747
|
-
commits.append(line)
|
|
748
|
-
commit_shas.append(commit_sha)
|
|
749
|
-
for changed_file in command_text(root, ["git", "show", "--format=", "--name-only", "--find-renames", commit_sha]).splitlines():
|
|
750
|
-
if changed_file and changed_file not in changed_files:
|
|
751
|
-
changed_files.append(changed_file)
|
|
752
|
-
diff_stat = command_text(root, ["git", "show", "--format=", "--stat", "--find-renames", commit_sha])
|
|
753
|
-
if diff_stat:
|
|
754
|
-
diff_stats.append(diff_stat)
|
|
755
|
-
patch = command_output(root, ["git", "show", "--format=", "--binary", "--find-renames", commit_sha])
|
|
756
|
-
if patch:
|
|
757
|
-
patches.append(patch)
|
|
758
|
-
return {
|
|
759
|
-
"baseCommit": base,
|
|
760
|
-
"headCommit": head,
|
|
761
|
-
"commits": commits,
|
|
762
|
-
"commitShas": commit_shas,
|
|
763
|
-
"changedFiles": changed_files,
|
|
764
|
-
"diffStat": "\\n".join(diff_stats),
|
|
765
|
-
"diffHash": hashlib.sha256(b"\\n".join(patches)).hexdigest(),
|
|
571
|
+
function splitLines(text) {
|
|
572
|
+
return text ? text.split(/\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/) : [];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function printResult(status, fields) {
|
|
576
|
+
const lines = [status];
|
|
577
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
578
|
+
if (value !== null && value !== undefined) {
|
|
579
|
+
lines.push(key + "=" + (value === true ? "True" : value === false ? "False" : String(value)));
|
|
766
580
|
}
|
|
581
|
+
}
|
|
582
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function quoteSegment(value) {
|
|
586
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (character) => "%" + character.charCodeAt(0).toString(16).toUpperCase());
|
|
587
|
+
}
|
|
767
588
|
|
|
589
|
+
async function callVcmApi(gate, source) {
|
|
590
|
+
const baseUrl = process.env.VCM_API_URL;
|
|
591
|
+
const taskSlug = process.env.VCM_TASK_SLUG;
|
|
592
|
+
if (!baseUrl || !taskSlug) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const url = baseUrl.replace(/\/+$/, "")
|
|
597
|
+
+ "/api/tasks/" + quoteSegment(taskSlug)
|
|
598
|
+
+ "/gate-review/" + quoteSegment(gate)
|
|
599
|
+
+ "/request";
|
|
600
|
+
let response;
|
|
601
|
+
try {
|
|
602
|
+
response = await postJson(url, { codeDiffSource: source ?? null }, 10000);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
printResult("failed_to_start", { gate, reason: networkErrorText(error) });
|
|
605
|
+
return 2;
|
|
606
|
+
}
|
|
607
|
+
if (!response.ok) {
|
|
608
|
+
let reason = "HTTP Error " + response.status + ": " + response.statusText;
|
|
609
|
+
try {
|
|
610
|
+
const payload = JSON.parse(response.text);
|
|
611
|
+
const error = isObject(payload) && "error" in payload ? payload.error : {};
|
|
612
|
+
if (isObject(error) && "message" in error) {
|
|
613
|
+
reason = error.message;
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
// Keep the HTTP status message when the error body is not JSON.
|
|
617
|
+
}
|
|
618
|
+
printResult("failed_to_start", { gate, reason });
|
|
619
|
+
return 2;
|
|
620
|
+
}
|
|
621
|
+
let payload;
|
|
622
|
+
try {
|
|
623
|
+
payload = JSON.parse(response.text);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
printResult("failed_to_start", { gate, reason: error.message });
|
|
626
|
+
return 2;
|
|
627
|
+
}
|
|
628
|
+
payload = isObject(payload) ? payload : {};
|
|
629
|
+
const record = isObject(payload.record) ? payload.record : {};
|
|
630
|
+
printResult(payload.status ?? "failed_to_start", {
|
|
631
|
+
gate,
|
|
632
|
+
request: record.requestPath,
|
|
633
|
+
report: record.reportPath,
|
|
634
|
+
message: payload.message
|
|
635
|
+
});
|
|
636
|
+
return 0;
|
|
637
|
+
}
|
|
768
638
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
path = root / core_artifact
|
|
775
|
-
digest.update(core_artifact.encode())
|
|
776
|
-
digest.update(path.read_bytes())
|
|
777
|
-
|
|
778
|
-
common = [] if gate == "code-diff" else [
|
|
779
|
-
"CLAUDE.md",
|
|
780
|
-
".claude/agents/architect.md",
|
|
781
|
-
".claude/agents/coder.md",
|
|
782
|
-
".claude/agents/reviewer.md",
|
|
783
|
-
".claude/agents/tester.md",
|
|
784
|
-
".claude/skills/vcm-gate-review/SKILL.md",
|
|
785
|
-
".ai/tools/request-gate-review",
|
|
786
|
-
"docs/CODING_STANDARDS.md",
|
|
787
|
-
]
|
|
788
|
-
inputs = dict.fromkeys(relative for relative in common + source_artifacts(gate, sources) if relative != core_artifact)
|
|
789
|
-
for relative in inputs:
|
|
790
|
-
path = root / relative
|
|
791
|
-
digest.update(relative.encode())
|
|
792
|
-
if path.is_file():
|
|
793
|
-
digest.update(path.read_bytes())
|
|
794
|
-
else:
|
|
795
|
-
digest.update(b"<missing>")
|
|
796
|
-
if gate == "architecture-plan":
|
|
797
|
-
evidence_pathspec = ["--", ".", ":(exclude).ai/vcm/**"]
|
|
798
|
-
digest.update(b"head")
|
|
799
|
-
digest.update(command_output(root, ["git", "rev-parse", "HEAD"]))
|
|
800
|
-
digest.update(b"workingDiff")
|
|
801
|
-
digest.update(command_output(root, ["git", "diff", "--binary", *evidence_pathspec]))
|
|
802
|
-
digest.update(b"stagedDiff")
|
|
803
|
-
digest.update(command_output(root, ["git", "diff", "--cached", "--binary", *evidence_pathspec]))
|
|
804
|
-
untracked = command_text(root, ["git", "ls-files", "--others", "--exclude-standard", *evidence_pathspec]).splitlines()
|
|
805
|
-
for relative in untracked:
|
|
806
|
-
digest.update(b"untracked")
|
|
807
|
-
digest.update(relative.encode())
|
|
808
|
-
digest.update(command_output(root, ["git", "hash-object", "--", relative]))
|
|
809
|
-
if gate == "validation-adequacy":
|
|
810
|
-
evidence_pathspec = ["--", ".", ":(exclude).ai/vcm/**", ":(exclude)docs/**"]
|
|
811
|
-
digest.update(b"trackedEvidence")
|
|
812
|
-
digest.update(command_output(root, ["git", "ls-files", "-s", *evidence_pathspec]))
|
|
813
|
-
digest.update(b"workingEvidence")
|
|
814
|
-
digest.update(command_output(root, ["git", "diff", "--binary", *evidence_pathspec]))
|
|
815
|
-
digest.update(b"stagedEvidence")
|
|
816
|
-
digest.update(command_output(root, ["git", "diff", "--cached", "--binary", *evidence_pathspec]))
|
|
817
|
-
untracked = command_text(root, ["git", "ls-files", "--others", "--exclude-standard", *evidence_pathspec]).splitlines()
|
|
818
|
-
for relative in untracked:
|
|
819
|
-
digest.update(b"untrackedEvidence")
|
|
820
|
-
digest.update(relative.encode())
|
|
821
|
-
digest.update(command_output(root, ["git", "hash-object", "--", relative]))
|
|
822
|
-
if gate == "code-diff":
|
|
823
|
-
digest.update(("\\n".join(sources or []) or "<missing>").encode())
|
|
824
|
-
if code_diff:
|
|
825
|
-
digest.update("\\n".join(code_diff.get("commits", [])).encode())
|
|
826
|
-
digest.update("\\n".join(code_diff.get("changedFiles", [])).encode())
|
|
827
|
-
digest.update(code_diff.get("diffHash", "").encode())
|
|
828
|
-
return digest.hexdigest()
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
def core_input_status(root: Path, gate: str) -> tuple[str, str] | None:
|
|
832
|
-
core_artifact = CORE_INPUT_ARTIFACTS.get(gate)
|
|
833
|
-
if not core_artifact:
|
|
834
|
-
return None
|
|
835
|
-
path = root / core_artifact
|
|
836
|
-
if not path.is_file():
|
|
837
|
-
return (core_artifact, "missing")
|
|
838
|
-
if not path.read_text().strip():
|
|
839
|
-
return (core_artifact, "empty")
|
|
840
|
-
return (core_artifact, "ready")
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
def code_diff_prerequisite_error(root: Path, index: dict, check_validation_gate: bool = True) -> str | None:
|
|
844
|
-
report_path = root / ".ai/vcm/handoffs/test-report.md"
|
|
845
|
-
try:
|
|
846
|
-
report = report_path.read_text()
|
|
847
|
-
except OSError:
|
|
848
|
-
return "code-diff requires completed Tester validation. .ai/vcm/handoffs/test-report.md is missing."
|
|
849
|
-
result = re.search(r"^\\s*Test Result\\s*:\\s*(pass|fail|incomplete)\\s*$", report, re.IGNORECASE | re.MULTILINE)
|
|
850
|
-
if result is None:
|
|
851
|
-
return "code-diff requires completed Tester validation. Test Result must be exactly pass or fail."
|
|
852
|
-
if result.group(1).lower() == "incomplete":
|
|
853
|
-
return "code-diff requires completed Tester validation. Test Result is incomplete."
|
|
854
|
-
|
|
855
|
-
if not check_validation_gate:
|
|
856
|
-
return None
|
|
857
|
-
|
|
858
|
-
validation = index.get("gates", {}).get("validation-adequacy", {})
|
|
859
|
-
if not isinstance(validation, dict) or not validation.get("required", False):
|
|
860
|
-
return None
|
|
861
|
-
if validation.get("status") in ("skipped", "overridden"):
|
|
862
|
-
return None
|
|
863
|
-
if validation.get("status") != "completed" or validation.get("decision") != "approve":
|
|
864
|
-
return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence."
|
|
865
|
-
current_hash = input_hash(root, "validation-adequacy")
|
|
866
|
-
if not validation.get("inputHash") or validation.get("inputHash") != current_hash:
|
|
867
|
-
return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval."
|
|
868
|
-
return None
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
def request_id(gate: str) -> str:
|
|
872
|
-
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
873
|
-
return f"{stamp}-{gate}-{uuid.uuid4().hex[:8]}"
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
def record_code_diff_start_failure(index_path: Path, index: dict, gate: str, reason: str, code_diff=None) -> None:
|
|
877
|
-
code_diff = code_diff or {}
|
|
878
|
-
gate_record = index["gates"].setdefault(gate, {})
|
|
879
|
-
gate_record.update({
|
|
880
|
-
"required": True,
|
|
881
|
-
"status": "failed",
|
|
882
|
-
"decision": None,
|
|
883
|
-
"error": reason,
|
|
884
|
-
"exceptionReason": None,
|
|
885
|
-
"requestId": None,
|
|
886
|
-
"requestPath": None,
|
|
887
|
-
"inputHash": None,
|
|
888
|
-
"baseCommit": code_diff.get("baseCommit"),
|
|
889
|
-
"headCommit": code_diff.get("headCommit"),
|
|
890
|
-
"commits": code_diff.get("commits"),
|
|
891
|
-
"changedFiles": code_diff.get("changedFiles"),
|
|
892
|
-
"diffStat": code_diff.get("diffStat"),
|
|
893
|
-
"requestedAt": None,
|
|
894
|
-
"startedAt": None,
|
|
895
|
-
"completedAt": now_iso(),
|
|
896
|
-
"callbackStatus": "not_sent",
|
|
897
|
-
"callbackError": None,
|
|
898
|
-
"updatedAt": now_iso(),
|
|
899
|
-
})
|
|
900
|
-
if index.get("activeGate") == gate:
|
|
901
|
-
index["activeGate"] = None
|
|
902
|
-
write_json(index_path, index)
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
def local_request(gate: str, source: str | None) -> int:
|
|
906
|
-
root = root_dir()
|
|
907
|
-
index_path = root / ".ai/vcm/gate-reviews/index.json"
|
|
908
|
-
if not index_path.is_file():
|
|
909
|
-
# Reached only when the VCM API is unreachable (call_vcm_api returned None)
|
|
910
|
-
# AND no local index exists, so the authoritative enable state is unknown.
|
|
911
|
-
# Fail safe instead of silently treating an indeterminate state as disabled.
|
|
912
|
-
print_result(
|
|
913
|
-
"failed_to_start",
|
|
914
|
-
gate=gate,
|
|
915
|
-
reason="cannot reach VCM API and no local gate-review index; enable state is indeterminate",
|
|
916
|
-
)
|
|
917
|
-
return 2
|
|
918
|
-
index = read_json(index_path)
|
|
919
|
-
enabled = bool(index.get("enabled", False))
|
|
920
|
-
gate_record = index.get("gates", {}).get(gate, {}) if isinstance(index.get("gates"), dict) else {}
|
|
921
|
-
required = bool(gate_record.get("required", False)) if isinstance(gate_record, dict) else False
|
|
922
|
-
index.update({"version": 1, "enabled": enabled})
|
|
923
|
-
index.setdefault("gates", {})
|
|
924
|
-
|
|
925
|
-
if not enabled:
|
|
926
|
-
index["activeGate"] = None
|
|
927
|
-
write_json(index_path, index)
|
|
928
|
-
print_result("disabled", gate=gate)
|
|
929
|
-
return 0
|
|
930
|
-
|
|
931
|
-
if not required:
|
|
932
|
-
gate_record = index["gates"].setdefault(gate, {})
|
|
933
|
-
gate_record.update({"required": False, "status": "not_required", "updatedAt": now_iso()})
|
|
934
|
-
write_json(index_path, index)
|
|
935
|
-
print_result("not_required", gate=gate)
|
|
936
|
-
return 0
|
|
937
|
-
|
|
938
|
-
if gate == "code-diff":
|
|
939
|
-
dirty = command_text(root, ["git", "status", "--porcelain=v1"]).splitlines()
|
|
940
|
-
if dirty:
|
|
941
|
-
reason = "code-diff requires committed inputs; commit or clean these changes first: " + "; ".join(dirty[:8])
|
|
942
|
-
if len(dirty) > 8:
|
|
943
|
-
reason += f"; ... {len(dirty) - 8} more"
|
|
944
|
-
gate_record = index["gates"].setdefault(gate, {})
|
|
945
|
-
gate_record.update({
|
|
946
|
-
"required": True,
|
|
947
|
-
"status": "failed",
|
|
948
|
-
"decision": None,
|
|
949
|
-
"error": reason,
|
|
950
|
-
"exceptionReason": None,
|
|
951
|
-
"requestId": None,
|
|
952
|
-
"requestPath": None,
|
|
953
|
-
"inputHash": None,
|
|
954
|
-
"baseCommit": None,
|
|
955
|
-
"headCommit": None,
|
|
956
|
-
"commits": None,
|
|
957
|
-
"changedFiles": None,
|
|
958
|
-
"diffStat": None,
|
|
959
|
-
"requestedAt": None,
|
|
960
|
-
"startedAt": None,
|
|
961
|
-
"completedAt": now_iso(),
|
|
962
|
-
"callbackStatus": "not_sent",
|
|
963
|
-
"callbackError": None,
|
|
964
|
-
"updatedAt": now_iso(),
|
|
965
|
-
})
|
|
966
|
-
if index.get("activeGate") == gate:
|
|
967
|
-
index["activeGate"] = None
|
|
968
|
-
write_json(index_path, index)
|
|
969
|
-
print_result("failed_to_start", gate=gate, reason=reason)
|
|
970
|
-
return 2
|
|
971
|
-
|
|
972
|
-
core_status = core_input_status(root, gate)
|
|
973
|
-
if core_status and core_status[1] != "ready":
|
|
974
|
-
gate_record = index["gates"].setdefault(gate, {})
|
|
975
|
-
gate_record.update({
|
|
976
|
-
"status": "not_required",
|
|
977
|
-
"decision": None,
|
|
978
|
-
"error": None,
|
|
979
|
-
"exceptionReason": None,
|
|
980
|
-
"requestId": None,
|
|
981
|
-
"requestPath": None,
|
|
982
|
-
"inputHash": None,
|
|
983
|
-
"requestedAt": None,
|
|
984
|
-
"startedAt": None,
|
|
985
|
-
"completedAt": None,
|
|
986
|
-
"callbackStatus": "not_sent",
|
|
987
|
-
"callbackError": None,
|
|
988
|
-
"updatedAt": now_iso(),
|
|
989
|
-
})
|
|
990
|
-
if index.get("activeGate") == gate:
|
|
991
|
-
index["activeGate"] = None
|
|
992
|
-
write_json(index_path, index)
|
|
993
|
-
print_result("not_required", gate=gate, message=f"{core_status[0]} is {core_status[1]}.")
|
|
994
|
-
return 0
|
|
995
|
-
|
|
996
|
-
if gate == "code-diff":
|
|
997
|
-
evidence_error = code_diff_prerequisite_error(root, index, False)
|
|
998
|
-
if evidence_error:
|
|
999
|
-
record_code_diff_start_failure(index_path, index, gate, evidence_error)
|
|
1000
|
-
print_result("failed_to_start", gate=gate, reason=evidence_error)
|
|
1001
|
-
return 2
|
|
1002
|
-
|
|
1003
|
-
gate_record = index["gates"].get(gate, {})
|
|
1004
|
-
code_diff = {}
|
|
1005
|
-
if gate == "code-diff":
|
|
1006
|
-
base, head = code_diff_range(root, gate_record if isinstance(gate_record, dict) else {})
|
|
1007
|
-
if not base or not head or base == head:
|
|
1008
|
-
gate_record = index["gates"].setdefault(gate, {})
|
|
1009
|
-
gate_record.update({
|
|
1010
|
-
"required": True,
|
|
1011
|
-
"status": "not_required",
|
|
1012
|
-
"decision": None,
|
|
1013
|
-
"error": None,
|
|
1014
|
-
"exceptionReason": None,
|
|
1015
|
-
"requestId": None,
|
|
1016
|
-
"requestPath": None,
|
|
1017
|
-
"inputHash": None,
|
|
1018
|
-
"baseCommit": None,
|
|
1019
|
-
"headCommit": None,
|
|
1020
|
-
"commits": None,
|
|
1021
|
-
"changedFiles": None,
|
|
1022
|
-
"diffStat": None,
|
|
1023
|
-
"requestedAt": None,
|
|
1024
|
-
"startedAt": None,
|
|
1025
|
-
"completedAt": None,
|
|
1026
|
-
"callbackStatus": "not_sent",
|
|
1027
|
-
"callbackError": None,
|
|
1028
|
-
"updatedAt": now_iso(),
|
|
1029
|
-
})
|
|
1030
|
-
if index.get("activeGate") == gate:
|
|
1031
|
-
index["activeGate"] = None
|
|
1032
|
-
write_json(index_path, index)
|
|
1033
|
-
print_result("not_required", gate=gate, message="No new commits to review.")
|
|
1034
|
-
return 0
|
|
1035
|
-
code_diff = reviewable_code_diff(root, base, head)
|
|
1036
|
-
if code_diff is None:
|
|
1037
|
-
print_result("not_required", gate=gate, message="No new commits to review.")
|
|
1038
|
-
return 0
|
|
1039
|
-
if not code_diff["commits"]:
|
|
1040
|
-
gate_record = index["gates"].setdefault(gate, {})
|
|
1041
|
-
gate_record.update({
|
|
1042
|
-
"required": True,
|
|
1043
|
-
"status": "not_required",
|
|
1044
|
-
"decision": None,
|
|
1045
|
-
"error": None,
|
|
1046
|
-
"exceptionReason": None,
|
|
1047
|
-
"requestId": None,
|
|
1048
|
-
"requestPath": None,
|
|
1049
|
-
"inputHash": None,
|
|
1050
|
-
"baseCommit": base,
|
|
1051
|
-
"headCommit": head,
|
|
1052
|
-
"commits": [],
|
|
1053
|
-
"changedFiles": [],
|
|
1054
|
-
"diffStat": "",
|
|
1055
|
-
"requestedAt": None,
|
|
1056
|
-
"startedAt": None,
|
|
1057
|
-
"completedAt": now_iso(),
|
|
1058
|
-
"callbackStatus": "not_sent",
|
|
1059
|
-
"callbackError": None,
|
|
1060
|
-
"updatedAt": now_iso(),
|
|
1061
|
-
})
|
|
1062
|
-
if index.get("activeGate") == gate:
|
|
1063
|
-
index["activeGate"] = None
|
|
1064
|
-
write_json(index_path, index)
|
|
1065
|
-
print_result("not_required", gate=gate, message="No non-Harness commits to review.")
|
|
1066
|
-
return 0
|
|
1067
|
-
|
|
1068
|
-
prerequisite_error = code_diff_prerequisite_error(root, index)
|
|
1069
|
-
if prerequisite_error:
|
|
1070
|
-
record_code_diff_start_failure(index_path, index, gate, prerequisite_error, code_diff)
|
|
1071
|
-
print_result("failed_to_start", gate=gate, reason=prerequisite_error)
|
|
1072
|
-
return 2
|
|
1073
|
-
|
|
1074
|
-
sources = code_diff_sources(gate_record, source, code_diff) if gate == "code-diff" else None
|
|
1075
|
-
current_hash = input_hash(root, gate, sources, gate_record if isinstance(gate_record, dict) else {}, code_diff)
|
|
1076
|
-
if (
|
|
1077
|
-
gate_record.get("status") == "completed"
|
|
1078
|
-
and gate_record.get("decision") == "approve"
|
|
1079
|
-
and gate_record.get("inputHash") == current_hash
|
|
1080
|
-
):
|
|
1081
|
-
print_result("already_approved", gate=gate, report=gate_record.get("reportPath", LATEST_REPORTS[gate]))
|
|
1082
|
-
return 0
|
|
1083
|
-
|
|
1084
|
-
rid = request_id(gate)
|
|
1085
|
-
request_path = root / ".ai/vcm/gate-reviews/requests" / f"{rid}.json"
|
|
1086
|
-
prompt_path = f".ai/vcm/gate-reviews/requests/{rid}.prompt.md"
|
|
1087
|
-
report_path = f".ai/vcm/gate-reviews/requests/{rid}.report.md"
|
|
1088
|
-
latest_report_path = LATEST_REPORTS[gate]
|
|
1089
|
-
requested_at = now_iso()
|
|
1090
|
-
write_json(request_path, {
|
|
1091
|
-
"version": 1,
|
|
1092
|
-
"requestId": rid,
|
|
1093
|
-
"gate": gate,
|
|
1094
|
-
"status": "requested",
|
|
1095
|
-
"requestedAt": requested_at,
|
|
1096
|
-
"inputHash": current_hash,
|
|
1097
|
-
"codeDiffSource": source,
|
|
1098
|
-
"codeDiffSources": sources,
|
|
1099
|
-
"codeDiff": code_diff or None,
|
|
1100
|
-
"reportPath": report_path,
|
|
1101
|
-
"latestReportPath": latest_report_path,
|
|
1102
|
-
"promptPath": prompt_path,
|
|
1103
|
-
})
|
|
1104
|
-
|
|
1105
|
-
index["activeGate"] = gate
|
|
1106
|
-
index["gates"][gate] = {
|
|
1107
|
-
"gate": gate,
|
|
1108
|
-
"required": True,
|
|
1109
|
-
"status": "running",
|
|
1110
|
-
"decision": None,
|
|
1111
|
-
"reportPath": latest_report_path,
|
|
1112
|
-
"promptPath": prompt_path,
|
|
1113
|
-
"inputHash": current_hash,
|
|
1114
|
-
"baseCommit": code_diff.get("baseCommit"),
|
|
1115
|
-
"headCommit": code_diff.get("headCommit"),
|
|
1116
|
-
"commits": code_diff.get("commits"),
|
|
1117
|
-
"changedFiles": code_diff.get("changedFiles"),
|
|
1118
|
-
"diffStat": code_diff.get("diffStat"),
|
|
1119
|
-
"codeDiffSource": source,
|
|
1120
|
-
"codeDiffSources": sources,
|
|
1121
|
-
"requestId": rid,
|
|
1122
|
-
"requestPath": request_path.relative_to(root).as_posix(),
|
|
1123
|
-
"requestedAt": requested_at,
|
|
1124
|
-
"updatedAt": requested_at,
|
|
639
|
+
function readJson(target) {
|
|
640
|
+
try {
|
|
641
|
+
const data = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
642
|
+
if (isObject(data)) {
|
|
643
|
+
return data;
|
|
1125
644
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
645
|
+
} catch {
|
|
646
|
+
// Fall through to the empty index.
|
|
647
|
+
}
|
|
648
|
+
return { version: 1, enabled: false, activeGate: null, gates: {} };
|
|
649
|
+
}
|
|
1129
650
|
|
|
651
|
+
function sortKeys(value) {
|
|
652
|
+
if (Array.isArray(value)) {
|
|
653
|
+
return value.map(sortKeys);
|
|
654
|
+
}
|
|
655
|
+
if (isObject(value)) {
|
|
656
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])]));
|
|
657
|
+
}
|
|
658
|
+
return value;
|
|
659
|
+
}
|
|
1130
660
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
661
|
+
// Matches Python json.dumps(indent=2, sort_keys=True) with ensure_ascii.
|
|
662
|
+
function writeJson(target, data) {
|
|
663
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
664
|
+
const temporary = target + ".tmp";
|
|
665
|
+
const text = JSON.stringify(sortKeys(data), null, 2)
|
|
666
|
+
.replace(/[\u007f-\uffff]/g, (character) => "\\u" + character.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
667
|
+
fs.writeFileSync(temporary, text + "\n");
|
|
668
|
+
fs.renameSync(temporary, target);
|
|
669
|
+
}
|
|
1136
670
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
671
|
+
function run(root, command, stdout) {
|
|
672
|
+
return childProcess.spawnSync(command[0], command.slice(1), {
|
|
673
|
+
cwd: root,
|
|
674
|
+
stdio: ["inherit", stdout, "ignore"],
|
|
675
|
+
maxBuffer: MAX_COMMAND_OUTPUT_BYTES
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function commandOutput(root, command) {
|
|
680
|
+
const result = run(root, command, "pipe");
|
|
681
|
+
return result.status === 0 && result.stdout ? result.stdout : Buffer.alloc(0);
|
|
682
|
+
}
|
|
1143
683
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
return 2
|
|
684
|
+
function commandText(root, command) {
|
|
685
|
+
return commandOutput(root, command).toString("utf8").trim();
|
|
686
|
+
}
|
|
1148
687
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
688
|
+
function isAncestor(root, ancestor, descendant) {
|
|
689
|
+
return run(root, ["git", "merge-base", "--is-ancestor", String(ancestor), String(descendant)], "ignore").status === 0;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function codeDiffRange(root, gateRecord) {
|
|
693
|
+
const head = commandText(root, ["git", "rev-parse", "HEAD"]);
|
|
694
|
+
if (!head) {
|
|
695
|
+
return [null, null];
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
let base;
|
|
699
|
+
if (
|
|
700
|
+
gateRecord.status === "completed"
|
|
701
|
+
&& gateRecord.decision === "request_changes"
|
|
702
|
+
&& truthy(gateRecord.baseCommit)
|
|
703
|
+
&& isAncestor(root, gateRecord.baseCommit, head)
|
|
704
|
+
) {
|
|
705
|
+
base = gateRecord.baseCommit;
|
|
706
|
+
} else if (
|
|
707
|
+
gateRecord.status === "failed"
|
|
708
|
+
&& truthy(gateRecord.baseCommit)
|
|
709
|
+
&& isAncestor(root, gateRecord.baseCommit, head)
|
|
710
|
+
) {
|
|
711
|
+
base = gateRecord.baseCommit;
|
|
712
|
+
} else if (
|
|
713
|
+
gateRecord.status === "completed"
|
|
714
|
+
&& gateRecord.decision === "approve"
|
|
715
|
+
&& truthy(gateRecord.headCommit)
|
|
716
|
+
&& isAncestor(root, gateRecord.headCommit, head)
|
|
717
|
+
) {
|
|
718
|
+
base = gateRecord.headCommit;
|
|
719
|
+
} else if (
|
|
720
|
+
gateRecord.status === "not_required"
|
|
721
|
+
&& truthy(gateRecord.headCommit)
|
|
722
|
+
&& isAncestor(root, gateRecord.headCommit, head)
|
|
723
|
+
) {
|
|
724
|
+
base = gateRecord.headCommit;
|
|
725
|
+
} else {
|
|
726
|
+
base = (process.env.VCM_BASE_COMMIT || "").trim();
|
|
727
|
+
if (!base || !isAncestor(root, base, head)) {
|
|
728
|
+
const upstream = commandText(root, ["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
|
|
729
|
+
base = upstream ? commandText(root, ["git", "merge-base", "HEAD", upstream]) : "";
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return [base || head, head];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function normalizeCodeDiffSources(gateRecord) {
|
|
737
|
+
const sources = gateRecord.codeDiffSources;
|
|
738
|
+
const normalized = Array.isArray(sources) ? sources.filter((item) => CODE_DIFF_SOURCES.includes(item)) : [];
|
|
739
|
+
const source = gateRecord.codeDiffSource;
|
|
740
|
+
if (normalized.length === 0 && CODE_DIFF_SOURCES.includes(source)) {
|
|
741
|
+
normalized.push(source);
|
|
742
|
+
}
|
|
743
|
+
return unique(normalized);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function codeDiffSources(gateRecord, source, codeDiff) {
|
|
747
|
+
if (!CODE_DIFF_SOURCES.includes(source)) {
|
|
748
|
+
return [];
|
|
749
|
+
}
|
|
750
|
+
const continuingRecordedRange = (gateRecord.baseCommit ?? null) === (codeDiff.baseCommit ?? null)
|
|
751
|
+
&& (
|
|
752
|
+
(gateRecord.status === "completed" && gateRecord.decision === "request_changes")
|
|
753
|
+
|| gateRecord.status === "failed"
|
|
754
|
+
);
|
|
755
|
+
const previous = continuingRecordedRange ? normalizeCodeDiffSources(gateRecord) : [];
|
|
756
|
+
return unique([...previous, source]);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function sourceArtifacts(gate, sources) {
|
|
760
|
+
if (gate !== "code-diff") {
|
|
761
|
+
return SOURCE_ARTIFACTS[gate];
|
|
762
|
+
}
|
|
763
|
+
return unique([
|
|
764
|
+
...SOURCE_ARTIFACTS["code-diff"],
|
|
765
|
+
...(sources || []).flatMap((source) => CODE_DIFF_SOURCE_ARTIFACTS[source] || [])
|
|
766
|
+
]);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function reviewableCodeDiff(root, base, head) {
|
|
770
|
+
const allCommits = splitLines(commandText(root, ["git", "log", "--oneline", "--reverse", base + ".." + head]));
|
|
771
|
+
if (allCommits.length === 0) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const commits = [];
|
|
775
|
+
const commitShas = [];
|
|
776
|
+
const changedFiles = [];
|
|
777
|
+
const diffStats = [];
|
|
778
|
+
const patches = [];
|
|
779
|
+
for (const line of allCommits) {
|
|
780
|
+
const separator = line.indexOf(" ");
|
|
781
|
+
if (separator >= 0 && line.slice(separator + 1).startsWith(HARNESS_COMMIT_PREFIX)) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const commitSha = line.trim().split(/\s+/)[0];
|
|
785
|
+
commits.push(line);
|
|
786
|
+
commitShas.push(commitSha);
|
|
787
|
+
for (const changedFile of splitLines(commandText(root, ["git", "show", "--format=", "--name-only", "--find-renames", commitSha]))) {
|
|
788
|
+
if (changedFile && !changedFiles.includes(changedFile)) {
|
|
789
|
+
changedFiles.push(changedFile);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const diffStat = commandText(root, ["git", "show", "--format=", "--stat", "--find-renames", commitSha]);
|
|
793
|
+
if (diffStat) {
|
|
794
|
+
diffStats.push(diffStat);
|
|
795
|
+
}
|
|
796
|
+
const patch = commandOutput(root, ["git", "show", "--format=", "--binary", "--find-renames", commitSha]);
|
|
797
|
+
if (patch.length > 0) {
|
|
798
|
+
patches.push(patch);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const joined = Buffer.concat(patches.flatMap((patch, index) => index === 0 ? [patch] : [Buffer.from("\n"), patch]));
|
|
802
|
+
return {
|
|
803
|
+
baseCommit: base,
|
|
804
|
+
headCommit: head,
|
|
805
|
+
commits,
|
|
806
|
+
commitShas,
|
|
807
|
+
changedFiles,
|
|
808
|
+
diffStat: diffStats.join("\n"),
|
|
809
|
+
diffHash: crypto.createHash("sha256").update(joined).digest("hex")
|
|
810
|
+
};
|
|
811
|
+
}
|
|
1153
812
|
|
|
813
|
+
function isFile(target) {
|
|
814
|
+
try {
|
|
815
|
+
return fs.statSync(target).isFile();
|
|
816
|
+
} catch {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function hashEvidence(root, digest, untrackedLabel, pathspec, extraCommands) {
|
|
822
|
+
for (const [name, command] of extraCommands) {
|
|
823
|
+
digest.update(name);
|
|
824
|
+
digest.update(commandOutput(root, command));
|
|
825
|
+
}
|
|
826
|
+
for (const relative of splitLines(commandText(root, ["git", "ls-files", "--others", "--exclude-standard", ...pathspec]))) {
|
|
827
|
+
digest.update(untrackedLabel);
|
|
828
|
+
digest.update(relative);
|
|
829
|
+
digest.update(commandOutput(root, ["git", "hash-object", "--", relative]));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function inputHash(root, gate, sources, codeDiff) {
|
|
834
|
+
const digest = crypto.createHash("sha256");
|
|
835
|
+
const coreArtifact = CORE_INPUT_ARTIFACTS[gate];
|
|
836
|
+
if (coreArtifact) {
|
|
837
|
+
digest.update(coreArtifact);
|
|
838
|
+
digest.update(fs.readFileSync(path.join(root, coreArtifact)));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const common = gate === "code-diff" ? [] : [
|
|
842
|
+
"CLAUDE.md",
|
|
843
|
+
".claude/agents/architect.md",
|
|
844
|
+
".claude/agents/coder.md",
|
|
845
|
+
".claude/agents/reviewer.md",
|
|
846
|
+
".claude/agents/tester.md",
|
|
847
|
+
".claude/skills/vcm-gate-review/SKILL.md",
|
|
848
|
+
".ai/tools/request-gate-review",
|
|
849
|
+
"docs/CODING_STANDARDS.md"
|
|
850
|
+
];
|
|
851
|
+
const inputs = unique([...common, ...sourceArtifacts(gate, sources)].filter((relative) => relative !== coreArtifact));
|
|
852
|
+
for (const relative of inputs) {
|
|
853
|
+
const target = path.join(root, relative);
|
|
854
|
+
digest.update(relative);
|
|
855
|
+
digest.update(isFile(target) ? fs.readFileSync(target) : "<missing>");
|
|
856
|
+
}
|
|
857
|
+
if (gate === "architecture-plan") {
|
|
858
|
+
const pathspec = ["--", ".", ":(exclude).ai/vcm/**"];
|
|
859
|
+
hashEvidence(root, digest, "untracked", pathspec, [
|
|
860
|
+
["head", ["git", "rev-parse", "HEAD"]],
|
|
861
|
+
["workingDiff", ["git", "diff", "--binary", ...pathspec]],
|
|
862
|
+
["stagedDiff", ["git", "diff", "--cached", "--binary", ...pathspec]]
|
|
863
|
+
]);
|
|
864
|
+
}
|
|
865
|
+
if (gate === "validation-adequacy") {
|
|
866
|
+
const pathspec = ["--", ".", ":(exclude).ai/vcm/**", ":(exclude)docs/**"];
|
|
867
|
+
hashEvidence(root, digest, "untrackedEvidence", pathspec, [
|
|
868
|
+
["trackedEvidence", ["git", "ls-files", "-s", ...pathspec]],
|
|
869
|
+
["workingEvidence", ["git", "diff", "--binary", ...pathspec]],
|
|
870
|
+
["stagedEvidence", ["git", "diff", "--cached", "--binary", ...pathspec]]
|
|
871
|
+
]);
|
|
872
|
+
}
|
|
873
|
+
if (gate === "code-diff") {
|
|
874
|
+
digest.update((sources || []).join("\n") || "<missing>");
|
|
875
|
+
if (truthy(codeDiff)) {
|
|
876
|
+
digest.update((codeDiff.commits || []).join("\n"));
|
|
877
|
+
digest.update((codeDiff.changedFiles || []).join("\n"));
|
|
878
|
+
digest.update(codeDiff.diffHash || "");
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return digest.digest("hex");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function coreInputStatus(root, gate) {
|
|
885
|
+
const coreArtifact = CORE_INPUT_ARTIFACTS[gate];
|
|
886
|
+
if (!coreArtifact) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
const target = path.join(root, coreArtifact);
|
|
890
|
+
if (!isFile(target)) {
|
|
891
|
+
return [coreArtifact, "missing"];
|
|
892
|
+
}
|
|
893
|
+
if (!fs.readFileSync(target, "utf8").trim()) {
|
|
894
|
+
return [coreArtifact, "empty"];
|
|
895
|
+
}
|
|
896
|
+
return [coreArtifact, "ready"];
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function codeDiffPrerequisiteError(root, index, checkValidationGate = true) {
|
|
900
|
+
let report;
|
|
901
|
+
try {
|
|
902
|
+
report = fs.readFileSync(path.join(root, ".ai/vcm/handoffs/test-report.md"), "utf8");
|
|
903
|
+
} catch {
|
|
904
|
+
return "code-diff requires completed Tester validation. .ai/vcm/handoffs/test-report.md is missing.";
|
|
905
|
+
}
|
|
906
|
+
const result = /^\s*Test Result\s*:\s*(pass|fail|incomplete)\s*$/im.exec(report.replace(/\r\n?/g, "\n"));
|
|
907
|
+
if (result === null) {
|
|
908
|
+
return "code-diff requires completed Tester validation. Test Result must be exactly pass or fail.";
|
|
909
|
+
}
|
|
910
|
+
if (result[1].toLowerCase() === "incomplete") {
|
|
911
|
+
return "code-diff requires completed Tester validation. Test Result is incomplete.";
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (!checkValidationGate) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const gates = isObject(index.gates) ? index.gates : {};
|
|
919
|
+
const validation = "validation-adequacy" in gates ? gates["validation-adequacy"] : {};
|
|
920
|
+
if (!isObject(validation) || !truthy(validation.required)) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
if (validation.status === "skipped" || validation.status === "overridden") {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
if (validation.status !== "completed" || validation.decision !== "approve") {
|
|
927
|
+
return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence.";
|
|
928
|
+
}
|
|
929
|
+
const currentHash = inputHash(root, "validation-adequacy");
|
|
930
|
+
if (!truthy(validation.inputHash) || validation.inputHash !== currentHash) {
|
|
931
|
+
return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval.";
|
|
932
|
+
}
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function requestId(gate) {
|
|
937
|
+
const stamp = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
938
|
+
return stamp + "-" + gate + "-" + crypto.randomBytes(4).toString("hex");
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function gateEntry(index, gate) {
|
|
942
|
+
if (!isObject(index.gates[gate])) {
|
|
943
|
+
index.gates[gate] = {};
|
|
944
|
+
}
|
|
945
|
+
return index.gates[gate];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function clearActiveGate(index, gate) {
|
|
949
|
+
if (index.activeGate === gate) {
|
|
950
|
+
index.activeGate = null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function resetFields(status, completedAt, codeDiff) {
|
|
955
|
+
return {
|
|
956
|
+
required: true,
|
|
957
|
+
status,
|
|
958
|
+
decision: null,
|
|
959
|
+
error: null,
|
|
960
|
+
exceptionReason: null,
|
|
961
|
+
requestId: null,
|
|
962
|
+
requestPath: null,
|
|
963
|
+
inputHash: null,
|
|
964
|
+
baseCommit: codeDiff.baseCommit ?? null,
|
|
965
|
+
headCommit: codeDiff.headCommit ?? null,
|
|
966
|
+
commits: codeDiff.commits ?? null,
|
|
967
|
+
changedFiles: codeDiff.changedFiles ?? null,
|
|
968
|
+
diffStat: codeDiff.diffStat ?? null,
|
|
969
|
+
requestedAt: null,
|
|
970
|
+
startedAt: null,
|
|
971
|
+
completedAt,
|
|
972
|
+
callbackStatus: "not_sent",
|
|
973
|
+
callbackError: null,
|
|
974
|
+
updatedAt: nowIso()
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function recordCodeDiffStartFailure(indexPath, index, gate, reason, codeDiff) {
|
|
979
|
+
Object.assign(gateEntry(index, gate), resetFields("failed", nowIso(), codeDiff || {}), { error: reason });
|
|
980
|
+
clearActiveGate(index, gate);
|
|
981
|
+
writeJson(indexPath, index);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function localRequest(gate, source) {
|
|
985
|
+
const root = rootDir();
|
|
986
|
+
const indexPath = path.join(root, ".ai/vcm/gate-reviews/index.json");
|
|
987
|
+
if (!isFile(indexPath)) {
|
|
988
|
+
// Reached only when the VCM API is unreachable (callVcmApi returned null)
|
|
989
|
+
// AND no local index exists, so the authoritative enable state is unknown.
|
|
990
|
+
// Fail safe instead of silently treating an indeterminate state as disabled.
|
|
991
|
+
printResult("failed_to_start", {
|
|
992
|
+
gate,
|
|
993
|
+
reason: "cannot reach VCM API and no local gate-review index; enable state is indeterminate"
|
|
994
|
+
});
|
|
995
|
+
return 2;
|
|
996
|
+
}
|
|
997
|
+
const index = readJson(indexPath);
|
|
998
|
+
const enabled = truthy(index.enabled ?? false);
|
|
999
|
+
let gateRecord = isObject(index.gates) && gate in index.gates ? index.gates[gate] : {};
|
|
1000
|
+
const required = isObject(gateRecord) ? truthy(gateRecord.required ?? false) : false;
|
|
1001
|
+
Object.assign(index, { version: 1, enabled });
|
|
1002
|
+
if (!("gates" in index)) {
|
|
1003
|
+
index.gates = {};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (!enabled) {
|
|
1007
|
+
index.activeGate = null;
|
|
1008
|
+
writeJson(indexPath, index);
|
|
1009
|
+
printResult("disabled", { gate });
|
|
1010
|
+
return 0;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (!required) {
|
|
1014
|
+
Object.assign(gateEntry(index, gate), { required: false, status: "not_required", updatedAt: nowIso() });
|
|
1015
|
+
writeJson(indexPath, index);
|
|
1016
|
+
printResult("not_required", { gate });
|
|
1017
|
+
return 0;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (gate === "code-diff") {
|
|
1021
|
+
const dirty = splitLines(commandText(root, ["git", "status", "--porcelain=v1"]));
|
|
1022
|
+
if (dirty.length > 0) {
|
|
1023
|
+
let reason = "code-diff requires committed inputs; commit or clean these changes first: " + dirty.slice(0, 8).join("; ");
|
|
1024
|
+
if (dirty.length > 8) {
|
|
1025
|
+
reason += "; ... " + (dirty.length - 8) + " more";
|
|
1026
|
+
}
|
|
1027
|
+
recordCodeDiffStartFailure(indexPath, index, gate, reason);
|
|
1028
|
+
printResult("failed_to_start", { gate, reason });
|
|
1029
|
+
return 2;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const coreStatus = coreInputStatus(root, gate);
|
|
1034
|
+
if (coreStatus && coreStatus[1] !== "ready") {
|
|
1035
|
+
const { required: _required, baseCommit, headCommit, commits, changedFiles, diffStat, ...fields } = resetFields("not_required", null, {});
|
|
1036
|
+
Object.assign(gateEntry(index, gate), fields);
|
|
1037
|
+
clearActiveGate(index, gate);
|
|
1038
|
+
writeJson(indexPath, index);
|
|
1039
|
+
printResult("not_required", { gate, message: coreStatus[0] + " is " + coreStatus[1] + "." });
|
|
1040
|
+
return 0;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (gate === "code-diff") {
|
|
1044
|
+
const evidenceError = codeDiffPrerequisiteError(root, index, false);
|
|
1045
|
+
if (evidenceError) {
|
|
1046
|
+
recordCodeDiffStartFailure(indexPath, index, gate, evidenceError);
|
|
1047
|
+
printResult("failed_to_start", { gate, reason: evidenceError });
|
|
1048
|
+
return 2;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
gateRecord = gate in index.gates ? index.gates[gate] : {};
|
|
1053
|
+
let codeDiff = {};
|
|
1054
|
+
if (gate === "code-diff") {
|
|
1055
|
+
const [base, head] = codeDiffRange(root, isObject(gateRecord) ? gateRecord : {});
|
|
1056
|
+
if (!base || !head || base === head) {
|
|
1057
|
+
Object.assign(gateEntry(index, gate), resetFields("not_required", null, {}));
|
|
1058
|
+
clearActiveGate(index, gate);
|
|
1059
|
+
writeJson(indexPath, index);
|
|
1060
|
+
printResult("not_required", { gate, message: "No new commits to review." });
|
|
1061
|
+
return 0;
|
|
1062
|
+
}
|
|
1063
|
+
codeDiff = reviewableCodeDiff(root, base, head);
|
|
1064
|
+
if (codeDiff === null) {
|
|
1065
|
+
printResult("not_required", { gate, message: "No new commits to review." });
|
|
1066
|
+
return 0;
|
|
1067
|
+
}
|
|
1068
|
+
if (codeDiff.commits.length === 0) {
|
|
1069
|
+
Object.assign(gateEntry(index, gate), resetFields("not_required", nowIso(), {
|
|
1070
|
+
baseCommit: base,
|
|
1071
|
+
headCommit: head,
|
|
1072
|
+
commits: [],
|
|
1073
|
+
changedFiles: [],
|
|
1074
|
+
diffStat: ""
|
|
1075
|
+
}));
|
|
1076
|
+
clearActiveGate(index, gate);
|
|
1077
|
+
writeJson(indexPath, index);
|
|
1078
|
+
printResult("not_required", { gate, message: "No non-Harness commits to review." });
|
|
1079
|
+
return 0;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const prerequisiteError = codeDiffPrerequisiteError(root, index);
|
|
1083
|
+
if (prerequisiteError) {
|
|
1084
|
+
recordCodeDiffStartFailure(indexPath, index, gate, prerequisiteError, codeDiff);
|
|
1085
|
+
printResult("failed_to_start", { gate, reason: prerequisiteError });
|
|
1086
|
+
return 2;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const sources = gate === "code-diff" ? codeDiffSources(gateRecord, source, codeDiff) : null;
|
|
1091
|
+
const currentHash = inputHash(root, gate, sources, codeDiff);
|
|
1092
|
+
if (
|
|
1093
|
+
gateRecord.status === "completed"
|
|
1094
|
+
&& gateRecord.decision === "approve"
|
|
1095
|
+
&& gateRecord.inputHash === currentHash
|
|
1096
|
+
) {
|
|
1097
|
+
printResult("already_approved", { gate, report: "reportPath" in gateRecord ? gateRecord.reportPath : LATEST_REPORTS[gate] });
|
|
1098
|
+
return 0;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const rid = requestId(gate);
|
|
1102
|
+
const requestPath = ".ai/vcm/gate-reviews/requests/" + rid + ".json";
|
|
1103
|
+
const promptPath = ".ai/vcm/gate-reviews/requests/" + rid + ".prompt.md";
|
|
1104
|
+
const reportPath = ".ai/vcm/gate-reviews/requests/" + rid + ".report.md";
|
|
1105
|
+
const latestReportPath = LATEST_REPORTS[gate];
|
|
1106
|
+
const requestedAt = nowIso();
|
|
1107
|
+
writeJson(path.join(root, requestPath), {
|
|
1108
|
+
version: 1,
|
|
1109
|
+
requestId: rid,
|
|
1110
|
+
gate,
|
|
1111
|
+
status: "requested",
|
|
1112
|
+
requestedAt,
|
|
1113
|
+
inputHash: currentHash,
|
|
1114
|
+
codeDiffSource: source ?? null,
|
|
1115
|
+
codeDiffSources: sources,
|
|
1116
|
+
codeDiff: truthy(codeDiff) ? codeDiff : null,
|
|
1117
|
+
reportPath,
|
|
1118
|
+
latestReportPath,
|
|
1119
|
+
promptPath
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
index.activeGate = gate;
|
|
1123
|
+
index.gates[gate] = {
|
|
1124
|
+
gate,
|
|
1125
|
+
required: true,
|
|
1126
|
+
status: "running",
|
|
1127
|
+
decision: null,
|
|
1128
|
+
reportPath: latestReportPath,
|
|
1129
|
+
promptPath,
|
|
1130
|
+
inputHash: currentHash,
|
|
1131
|
+
baseCommit: codeDiff.baseCommit ?? null,
|
|
1132
|
+
headCommit: codeDiff.headCommit ?? null,
|
|
1133
|
+
commits: codeDiff.commits ?? null,
|
|
1134
|
+
changedFiles: codeDiff.changedFiles ?? null,
|
|
1135
|
+
diffStat: codeDiff.diffStat ?? null,
|
|
1136
|
+
codeDiffSource: source ?? null,
|
|
1137
|
+
codeDiffSources: sources,
|
|
1138
|
+
requestId: rid,
|
|
1139
|
+
requestPath,
|
|
1140
|
+
requestedAt,
|
|
1141
|
+
updatedAt: requestedAt
|
|
1142
|
+
};
|
|
1143
|
+
writeJson(indexPath, index);
|
|
1144
|
+
printResult("started", { gate, request: requestPath, report: reportPath });
|
|
1145
|
+
return 0;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function resolvedPath(target) {
|
|
1149
|
+
try {
|
|
1150
|
+
return fs.realpathSync(target);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return path.resolve(target);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async function main() {
|
|
1157
|
+
const args = parseToolArguments(process.argv.slice(2), {
|
|
1158
|
+
options: {
|
|
1159
|
+
"--gate": { dest: "gate", required: true, choices: GATES },
|
|
1160
|
+
"--source": { dest: "source", choices: CODE_DIFF_SOURCES }
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
if (args.gate === "code-diff" && !args.source) {
|
|
1165
|
+
printResult("failed_to_start", {
|
|
1166
|
+
gate: args.gate,
|
|
1167
|
+
reason: "code-diff requires --source coder, --source architect-debug, or --source architect-diagnosis"
|
|
1168
|
+
});
|
|
1169
|
+
return 2;
|
|
1170
|
+
}
|
|
1171
|
+
if (args.gate !== "code-diff" && args.source) {
|
|
1172
|
+
printResult("failed_to_start", { gate: args.gate, reason: "--source is valid only for code-diff" });
|
|
1173
|
+
return 2;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const expectedRoot = process.env.VCM_TASK_REPO_ROOT;
|
|
1177
|
+
if (expectedRoot && resolvedPath(expectedRoot) !== resolvedPath(process.cwd())) {
|
|
1178
|
+
printResult("failed_to_start", { gate: args.gate, reason: "cwd does not match VCM_TASK_REPO_ROOT" });
|
|
1179
|
+
return 2;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const apiResult = await callVcmApi(args.gate, args.source);
|
|
1183
|
+
if (apiResult !== null) {
|
|
1184
|
+
return apiResult;
|
|
1185
|
+
}
|
|
1186
|
+
return localRequest(args.gate, args.source);
|
|
1187
|
+
}
|
|
1154
1188
|
|
|
1155
|
-
|
|
1156
|
-
sys.exit(main())
|
|
1189
|
+
main().then((code) => process.exit(code));
|
|
1157
1190
|
`;
|
|
1158
1191
|
}
|