vibe-coding-master 0.2.7 → 0.2.8

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.
@@ -5,13 +5,22 @@ import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- const HARNESS_VERSION = "0.2.1-fixed";
8
+ const HARNESS_VERSION = "0.3.0-fixed";
9
9
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
10
10
  const MANIFEST_PATH = ".ai/vcm-harness-manifest.json";
11
11
  const HTML_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=\d+)? -->[\s\S]*?<!-- VCM:END -->/m;
12
12
  const HASH_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=\d+)?\n[\s\S]*?# VCM:END/m;
13
13
  const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
14
+ const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
14
15
  const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
16
+ const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; exec python3 "\${CLAUDE_PROJECT_DIR:-.}/.ai/tools/vcm-bash-guard"'`;
17
+ const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
18
+ const VCM_HOOK_DEFINITIONS = [
19
+ { eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
20
+ { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
21
+ { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
22
+ { eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
23
+ ];
15
24
 
16
25
  const AGENT_FRONTMATTER = {
17
26
  "project-manager": {
@@ -44,12 +53,11 @@ const MANAGED_FILES = [
44
53
 
45
54
  ## VCM Background Jobs
46
55
 
47
- - Do not start background jobs.
48
- - The only allowed background job is \`.ai/tools/run-long-check\` when used through the \`vcm-long-running-validation\` skill.
49
- - \`vcm-long-running-validation\` has a hard maximum timeout of 60 minutes.
50
- - Do not run or suggest operations expected to exceed 60 minutes without user approval.
51
- - Design every single validation/build operation to complete within 60 minutes; split anything larger before running it.
52
- - Do not end the current turn only to wait for a long-running shell callback.
56
+ - Never run the Bash tool with \`run_in_background: true\`. Never detach a process with \`nohup\`, \`setsid\`, \`disown\`, or a trailing \`&\`. VCM denies these calls.
57
+ - The only sanctioned long-running mechanism is the \`vcm-long-running-validation\` skill: \`.ai/tools/run-long-check\` plus \`.ai/tools/watch-job\`.
58
+ - The moment a command might run longer than 2 minutes, switch to that skill instead of running the command directly.
59
+ - While a job is running, stay in the current turn and keep calling \`.ai/tools/watch-job\` until it reports a terminal result; VCM blocks turn-end while a job is running, and a job without a live watcher is killed automatically.
60
+ - Hard ceiling: 60 minutes per job, enforced by the job worker. Do not run or suggest operations expected to exceed 60 minutes without user approval; split larger work first.
53
61
 
54
62
  ## VCM Durable Project Docs
55
63
 
@@ -94,6 +102,7 @@ const MANAGED_FILES = [
94
102
  content: `# VCM runtime task metadata, handoffs, session records, logs, and task worktrees.
95
103
  .ai/vcm/
96
104
  .claude/worktrees/
105
+ .ai/tools/__pycache__/
97
106
  `
98
107
  },
99
108
  {
@@ -171,6 +180,11 @@ const MANAGED_FILES = [
171
180
  - Fill the PR body from final acceptance, review report, docs-sync report, known-issues disposition, and commits.
172
181
  - Do not perform technical review or validation during PR preparation; route missing evidence to the responsible role.
173
182
  - Create a draft PR by default unless the user requests a ready PR.
183
+
184
+ ### Background Jobs
185
+
186
+ - Never background a Bash command: no \`run_in_background\`, \`nohup\`, \`setsid\`, \`disown\`, or trailing \`&\`.
187
+ - For any command that may exceed 2 minutes, use the \`vcm-long-running-validation\` skill and stay in the turn, re-running \`.ai/tools/watch-job\` until it reports a terminal result.
174
188
  `
175
189
  },
176
190
  {
@@ -231,6 +245,11 @@ const MANAGED_FILES = [
231
245
  - When crate-external public APIs change, require \`.ai/tools/generate-public-surface --check\` or regeneration.
232
246
  - Read \`.ai/vcm/handoffs/known-issues.md\` and promote confirmed unresolved issues to \`docs/known-issues.md\`.
233
247
  - Write \`.ai/vcm/handoffs/docs-sync-report.md\` with decision, evidence reviewed, architecture drift check, docs updated, docs left unchanged, remaining documentation risks, and handoff notes.
248
+
249
+ ### Background Jobs
250
+
251
+ - Never background a Bash command: no \`run_in_background\`, \`nohup\`, \`setsid\`, \`disown\`, or trailing \`&\`.
252
+ - For any command that may exceed 2 minutes, use the \`vcm-long-running-validation\` skill and stay in the turn, re-running \`.ai/tools/watch-job\` until it reports a terminal result.
234
253
  `
235
254
  },
236
255
  {
@@ -280,6 +299,11 @@ const MANAGED_FILES = [
280
299
  - Do not request Replan because of workload, session length, or context size.
281
300
  - If the plan remains valid but the assigned work cannot be finished in this turn, include completed work, remaining work, validation state, and next continuation step in the route message, then ask project-manager for continuation.
282
301
  - If implementation exposes a broad testing gap beyond baseline unit tests, report it to project-manager for reviewer follow-up.
302
+
303
+ ### Background Jobs
304
+
305
+ - Never background a Bash command: no \`run_in_background\`, \`nohup\`, \`setsid\`, \`disown\`, or trailing \`&\`.
306
+ - For any command that may exceed 2 minutes, use the \`vcm-long-running-validation\` skill and stay in the turn, re-running \`.ai/tools/watch-job\` until it reports a terminal result.
283
307
  `
284
308
  },
285
309
  {
@@ -326,6 +350,11 @@ const MANAGED_FILES = [
326
350
 
327
351
  - Write \`.ai/vcm/handoffs/review-report.md\` with decision, evidence reviewed, tests added or updated, commands run or checked, validation results, failed expectations, reproduction steps, skipped checks with reasons, coverage gaps, and required follow-ups.
328
352
  - Record confirmed unresolved issues in \`.ai/vcm/handoffs/known-issues.md\` only when they should survive current-task cleanup.
353
+
354
+ ### Background Jobs
355
+
356
+ - Never background a Bash command: no \`run_in_background\`, \`nohup\`, \`setsid\`, \`disown\`, or trailing \`&\`.
357
+ - For any command that may exceed 2 minutes, use the \`vcm-long-running-validation\` skill and stay in the turn, re-running \`.ai/tools/watch-job\` until it reports a terminal result.
329
358
  `
330
359
  },
331
360
  {
@@ -619,32 +648,53 @@ description: Use for builds, browser checks, E2E tests, release suites, or any v
619
648
 
620
649
  # VCM Long-Running Validation Skill
621
650
 
622
- Use this skill for builds, browser checks, E2E tests, release suites, or any command that may take long enough for shell-completion callbacks to become unreliable.
651
+ Use this skill for builds, browser checks, E2E tests, release suites, or any command that may run longer than 2 minutes.
623
652
 
624
653
  ## Rule
625
654
 
626
- Do not start background jobs.
655
+ Never run the Bash tool with \`run_in_background: true\`, and never detach a process with \`nohup\`, \`setsid\`, \`disown\`, or a trailing \`&\`. VCM denies these calls.
627
656
 
628
- The only allowed background job is \`.ai/tools/run-long-check\` when used through this skill.
657
+ The only sanctioned long-running mechanism is \`.ai/tools/run-long-check\` plus \`.ai/tools/watch-job\` through this skill.
629
658
 
630
- This skill has a hard maximum timeout of 60 minutes. Do not run or suggest operations expected to exceed 60 minutes without user approval.
659
+ The hard ceiling is 60 minutes per job, enforced by the job worker itself. Do not run or suggest operations expected to exceed 60 minutes without user approval; split larger work first.
631
660
 
632
661
  ## Protocol
633
662
 
634
- 1. Start the command through \`.ai/tools/run-long-check\`.
635
- 2. Write job state under \`.ai/vcm/jobs/<job-id>/\`.
636
- 3. Run \`.ai/tools/watch-job <job-id> --timeout <duration>\` in the same turn.
637
- 4. Treat success, failure, and timeout as explicit results.
638
- 5. Read the final status and relevant log tail.
663
+ 1. Start the command with an explicit ceiling: \`.ai/tools/run-long-check --timeout <duration> -- <command>\`. Pick the ceiling from \`docs/TESTING.md\` guidance or a realistic estimate, never above 60m. The tool prints the job id and creates job state under \`.ai/vcm/jobs/<job-id>/\`.
664
+ 2. In the same turn, run \`.ai/tools/watch-job <job-id>\`. The default watch window is 8 minutes.
665
+ 3. If watch-job exits 125, the job is still running: run \`.ai/tools/watch-job <job-id>\` again immediately. Do not end the turn between windows.
666
+ 4. Repeat until watch-job reports a terminal result.
667
+ 5. Read the final status and the relevant log tail.
639
668
  6. Record command, result, duration, and required follow-up wherever the caller normally records command evidence.
640
669
 
641
670
  Example:
642
671
 
643
672
  \`\`\`bash
644
- .ai/tools/run-long-check -- cargo test --workspace
645
- .ai/tools/watch-job <job-id> --timeout 20m
673
+ .ai/tools/run-long-check --timeout 30m -- cargo test --workspace
674
+ .ai/tools/watch-job <job-id>
675
+ .ai/tools/watch-job <job-id> # repeat while the exit code is 125
646
676
  \`\`\`
647
677
 
678
+ ## Exit Codes
679
+
680
+ Treat watch-job exit codes as explicit results:
681
+
682
+ - \`0\`: success.
683
+ - \`1\`: failed; read the log tails.
684
+ - \`124\`: timeout; the job hit its ceiling and was killed; not passed.
685
+ - \`125\`: still running; call watch-job again now.
686
+ - \`4\`: orphaned or stale; the job lost foreground supervision and was killed, or its worker died; not passed.
687
+ - \`2\`: usage error or unknown job id.
688
+
689
+ ## Supervision
690
+
691
+ A running job requires a live foreground watcher:
692
+
693
+ - watch-job renews the job supervision lease while it runs.
694
+ - If no watcher renews the lease for about 2 minutes, the worker kills the command process group and records \`orphaned\`. A job cannot keep running unsupervised.
695
+ - VCM also blocks ending the turn while a job is running. Stay in the turn and keep watching.
696
+ - Only one validation job may run at a time; run-long-check refuses to start a second one.
697
+
648
698
  ## Job Files
649
699
 
650
700
  \`\`\`text
@@ -652,23 +702,19 @@ Example:
652
702
  .ai/vcm/jobs/<job-id>/status.json
653
703
  .ai/vcm/jobs/<job-id>/stdout.log
654
704
  .ai/vcm/jobs/<job-id>/stderr.log
705
+ .ai/vcm/jobs/<job-id>/lease
655
706
  \`\`\`
656
707
 
657
708
  ## Timeout
658
709
 
659
710
  Timeout is not "unknown". It is a command result.
660
711
 
661
- \`watch-job\` rejects timeouts over 60 minutes.
662
-
663
- On timeout:
712
+ On timeout the worker stops the command process group and records \`timeout\` in \`status.json\`; watch-job reports it with exit code 124:
664
713
 
665
714
  - summarize the latest log tail
666
- - record the timeout in \`status.json\`
667
715
  - report whether the timed-out process was stopped
668
716
  - do not mark the command as passed
669
- - do not continue the job in the background
670
-
671
- \`watch-job\` should attempt to stop the timed-out command process group. If termination cannot be confirmed, say so in the summary.
717
+ - do not retry in the background
672
718
 
673
719
  ## Cleanup
674
720
 
@@ -775,366 +821,19 @@ If delivery is manual, blocked, or the target role is busy, leave the route file
775
821
  path: ".ai/tools/run-long-check",
776
822
  category: "runtime-tool",
777
823
  mode: 0o755,
778
- content: `#!/usr/bin/env python3
779
- import json
780
- import os
781
- import subprocess
782
- import sys
783
- import time
784
- import uuid
785
- from datetime import datetime, timezone
786
- from pathlib import Path
787
-
788
-
789
- def root_dir() -> Path:
790
- return Path(__file__).resolve().parents[2]
791
-
792
-
793
- def now_iso() -> str:
794
- return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
795
-
796
-
797
- def write_json(path: Path, data: dict) -> None:
798
- tmp = path.with_suffix(path.suffix + ".tmp")
799
- tmp.write_text(json.dumps(data, indent=2, sort_keys=True) + "\\n")
800
- tmp.replace(path)
801
-
802
-
803
- def job_id() -> str:
804
- timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
805
- return f"{timestamp}-{uuid.uuid4().hex[:8]}"
806
-
807
-
808
- def start_job(command: list[str]) -> int:
809
- root = root_dir()
810
- job = job_id()
811
- directory = root / ".ai/vcm/jobs" / job
812
- directory.mkdir(parents=True, exist_ok=False)
813
-
814
- (directory / "command.json").write_text(
815
- json.dumps({"command": command, "cwd": "."}, indent=2, sort_keys=True) + "\\n"
816
- )
817
- write_json(
818
- directory / "status.json",
819
- {
820
- "jobId": job,
821
- "status": "queued",
822
- "command": command,
823
- "cwd": ".",
824
- "startedAt": None,
825
- "finishedAt": None,
826
- "exitCode": None,
827
- "durationSeconds": None,
828
- "workerPid": None,
829
- "processId": None,
830
- },
831
- )
832
-
833
- subprocess.Popen(
834
- [sys.executable, str(Path(__file__).resolve()), "--worker", job],
835
- cwd=root,
836
- stdin=subprocess.DEVNULL,
837
- stdout=subprocess.DEVNULL,
838
- stderr=subprocess.DEVNULL,
839
- start_new_session=True,
840
- )
841
-
842
- print(f"job: {job}")
843
- print(f"watch: .ai/tools/watch-job {job}")
844
- return 0
845
-
846
-
847
- def run_worker(job: str) -> int:
848
- root = root_dir()
849
- directory = root / ".ai/vcm/jobs" / job
850
- command_path = directory / "command.json"
851
- status_path = directory / "status.json"
852
-
853
- payload = json.loads(command_path.read_text())
854
- command = payload["command"]
855
- started = time.time()
856
- started_at = now_iso()
857
-
858
- with (directory / "stdout.log").open("wb") as stdout, (directory / "stderr.log").open("wb") as stderr:
859
- process = subprocess.Popen(
860
- command,
861
- cwd=root,
862
- stdout=stdout,
863
- stderr=stderr,
864
- start_new_session=True,
865
- )
866
- write_json(
867
- status_path,
868
- {
869
- "jobId": job,
870
- "status": "running",
871
- "command": command,
872
- "cwd": ".",
873
- "startedAt": started_at,
874
- "finishedAt": None,
875
- "exitCode": None,
876
- "durationSeconds": None,
877
- "workerPid": os.getpid(),
878
- "processId": process.pid,
879
- },
880
- )
881
- exit_code = process.wait()
882
-
883
- duration = round(time.time() - started, 3)
884
- current = json.loads(status_path.read_text())
885
- if current.get("status") == "timeout":
886
- current["processExitCode"] = exit_code
887
- current["processFinishedAt"] = now_iso()
888
- current["processDurationSeconds"] = duration
889
- write_json(status_path, current)
890
- return 0
891
-
892
- write_json(
893
- status_path,
894
- {
895
- "jobId": job,
896
- "status": "success" if exit_code == 0 else "failed",
897
- "command": command,
898
- "cwd": ".",
899
- "startedAt": started_at,
900
- "finishedAt": now_iso(),
901
- "exitCode": exit_code,
902
- "durationSeconds": duration,
903
- "workerPid": os.getpid(),
904
- "processId": process.pid,
905
- },
906
- )
907
- return 0
908
-
909
-
910
- def main() -> int:
911
- if len(sys.argv) >= 3 and sys.argv[1] == "--worker":
912
- return run_worker(sys.argv[2])
913
-
914
- if len(sys.argv) < 3 or sys.argv[1] != "--":
915
- print("Usage: .ai/tools/run-long-check -- <command> [args...]", file=sys.stderr)
916
- return 2
917
-
918
- return start_job(sys.argv[2:])
919
-
920
-
921
- if __name__ == "__main__":
922
- raise SystemExit(main())
923
- `
824
+ templatePath: "scripts/harness-tools/run-long-check"
924
825
  },
925
826
  {
926
827
  path: ".ai/tools/watch-job",
927
828
  category: "runtime-tool",
928
829
  mode: 0o755,
929
- content: `#!/usr/bin/env python3
930
- import argparse
931
- import json
932
- import os
933
- import signal
934
- import sys
935
- import time
936
- from datetime import datetime, timezone
937
- from pathlib import Path
938
-
939
-
940
- MAX_TIMEOUT_SECONDS = 60 * 60
941
-
942
-
943
- def root_dir() -> Path:
944
- return Path(__file__).resolve().parents[2]
945
-
946
-
947
- def parse_duration(value: str) -> float:
948
- value = value.strip().lower()
949
- if value.endswith("ms"):
950
- return float(value[:-2]) / 1000
951
- if value.endswith("s"):
952
- return float(value[:-1])
953
- if value.endswith("m"):
954
- return float(value[:-1]) * 60
955
- if value.endswith("h"):
956
- return float(value[:-1]) * 3600
957
- return float(value)
958
-
959
-
960
- def validate_duration(name: str, value: float, maximum: float | None = None) -> bool:
961
- if value <= 0:
962
- print(f"error: {name} must be positive", file=sys.stderr)
963
- return False
964
- if maximum is not None and value > maximum:
965
- print(f"error: {name} exceeds maximum 60m", file=sys.stderr)
966
- return False
967
- return True
968
-
969
-
970
- def read_status(path: Path) -> dict:
971
- return json.loads(path.read_text())
972
-
973
-
974
- def now_iso() -> str:
975
- return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
976
-
977
-
978
- def write_json(path: Path, data: dict) -> None:
979
- tmp = path.with_suffix(path.suffix + ".tmp")
980
- tmp.write_text(json.dumps(data, indent=2, sort_keys=True) + "\\n")
981
- tmp.replace(path)
982
-
983
-
984
- def started_duration(status: dict) -> float | None:
985
- started_at = status.get("startedAt")
986
- if not started_at:
987
- return None
988
- try:
989
- started = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
990
- except ValueError:
991
- return None
992
- return round((datetime.now(timezone.utc) - started).total_seconds(), 3)
993
-
994
-
995
- def process_group_exists(pid: int) -> bool:
996
- try:
997
- os.killpg(pid, 0)
998
- return True
999
- except ProcessLookupError:
1000
- return False
1001
- except PermissionError:
1002
- return True
1003
-
1004
-
1005
- def process_exists(pid: int) -> bool:
1006
- try:
1007
- os.kill(pid, 0)
1008
- return True
1009
- except ProcessLookupError:
1010
- return False
1011
- except PermissionError:
1012
- return True
1013
-
1014
-
1015
- def stop_process(pid: int, signal_value: signal.Signals) -> str:
1016
- try:
1017
- os.killpg(pid, signal_value)
1018
- return "process-group"
1019
- except ProcessLookupError:
1020
- return "not-running"
1021
- except PermissionError:
1022
- try:
1023
- os.kill(pid, signal_value)
1024
- return "process"
1025
- except ProcessLookupError:
1026
- return "not-running"
1027
- except PermissionError:
1028
- return "permission-denied"
1029
-
1030
-
1031
- def stop_process_group(pid: int) -> str:
1032
- if not process_group_exists(pid) and not process_exists(pid):
1033
- return "not-running"
1034
-
1035
- mode = stop_process(pid, signal.SIGTERM)
1036
- if mode in {"not-running", "permission-denied"}:
1037
- return mode
1038
-
1039
- deadline = time.time() + 2
1040
- while time.time() < deadline:
1041
- still_running = process_group_exists(pid) if mode == "process-group" else process_exists(pid)
1042
- if not still_running:
1043
- return f"terminated-{mode}"
1044
- time.sleep(0.1)
1045
-
1046
- kill_mode = stop_process(pid, signal.SIGKILL)
1047
- if kill_mode == "not-running":
1048
- return f"terminated-{mode}"
1049
- if kill_mode == "permission-denied":
1050
- return "permission-denied"
1051
- return f"killed-{kill_mode}"
1052
-
1053
-
1054
- def tail(path: Path, lines: int = 40) -> str:
1055
- if not path.is_file():
1056
- return ""
1057
- content = path.read_text(errors="replace").splitlines()
1058
- return "\\n".join(content[-lines:])
1059
-
1060
-
1061
- def print_summary(job_id: str, status: dict, directory: Path) -> None:
1062
- print(f"job: {job_id}")
1063
- print(f"status: {status.get('status')}")
1064
- print(f"exitCode: {status.get('exitCode')}")
1065
- print(f"durationSeconds: {status.get('durationSeconds')}")
1066
- if status.get("status") == "timeout":
1067
- print(f"timeoutSeconds: {status.get('timeoutSeconds')}")
1068
- print(f"processStopResult: {status.get('processStopResult')}")
1069
-
1070
- if status.get("status") in {"failed", "timeout"}:
1071
- stdout_tail = tail(directory / "stdout.log")
1072
- stderr_tail = tail(directory / "stderr.log")
1073
- if stdout_tail:
1074
- print("\\nstdout tail:")
1075
- print(stdout_tail)
1076
- if stderr_tail:
1077
- print("\\nstderr tail:")
1078
- print(stderr_tail)
1079
-
1080
-
1081
- def main() -> int:
1082
- parser = argparse.ArgumentParser(description="Watch a file-backed long-running validation job.")
1083
- parser.add_argument("job_id")
1084
- parser.add_argument("--timeout", default="10m")
1085
- parser.add_argument("--interval", default="1s")
1086
- args = parser.parse_args()
1087
-
1088
- try:
1089
- timeout = parse_duration(args.timeout)
1090
- interval = parse_duration(args.interval)
1091
- except ValueError as exc:
1092
- print(f"error: invalid duration: {exc}", file=sys.stderr)
1093
- return 2
1094
- if not validate_duration("timeout", timeout, MAX_TIMEOUT_SECONDS):
1095
- return 2
1096
- if not validate_duration("interval", interval):
1097
- return 2
1098
- directory = root_dir() / ".ai/vcm/jobs" / args.job_id
1099
- status_path = directory / "status.json"
1100
-
1101
- deadline = time.time() + timeout
1102
- last_status: dict | None = None
1103
-
1104
- while time.time() <= deadline:
1105
- if status_path.is_file():
1106
- last_status = read_status(status_path)
1107
- if last_status.get("status") in {"success", "failed"}:
1108
- print_summary(args.job_id, last_status, directory)
1109
- return 0 if last_status.get("status") == "success" else 1
1110
- time.sleep(interval)
1111
-
1112
- timeout_status = dict(last_status or {})
1113
- process_id = timeout_status.get("processId")
1114
- stop_result = "no-process-id"
1115
- if isinstance(process_id, int):
1116
- stop_result = stop_process_group(process_id)
1117
-
1118
- timeout_status.update(
1119
- {
1120
- "jobId": args.job_id,
1121
- "status": "timeout",
1122
- "finishedAt": now_iso(),
1123
- "exitCode": None,
1124
- "durationSeconds": started_duration(timeout_status),
1125
- "timeoutSeconds": timeout,
1126
- "processStopResult": stop_result,
1127
- }
1128
- )
1129
- if directory.is_dir():
1130
- write_json(status_path, timeout_status)
1131
- print_summary(args.job_id, timeout_status, directory)
1132
- return 124
1133
-
1134
-
1135
- if __name__ == "__main__":
1136
- raise SystemExit(main())
1137
- `
830
+ templatePath: "scripts/harness-tools/watch-job"
831
+ },
832
+ {
833
+ path: ".ai/tools/vcm-bash-guard",
834
+ category: "runtime-tool",
835
+ mode: 0o755,
836
+ templatePath: "scripts/harness-tools/vcm-bash-guard"
1138
837
  }
1139
838
  ];
1140
839
 
@@ -1262,8 +961,9 @@ async function buildManifest(projectRoot) {
1262
961
  source: "vcm-template",
1263
962
  lifecycle: "long-term",
1264
963
  jsonOwnership: {
1265
- topLevelKeys: ["hooks"],
1266
- hookMatchers: ["VCM"]
964
+ topLevelKeys: ["hooks", "env"],
965
+ hookMatchers: ["VCM"],
966
+ envKeys: ["BASH_DEFAULT_TIMEOUT_MS"]
1267
967
  },
1268
968
  uninstall: {
1269
969
  action: "remove-owned-json-keys",
@@ -1481,15 +1181,16 @@ function mergeVcmHooks(settings) {
1481
1181
  }
1482
1182
  }
1483
1183
 
1484
- for (const eventName of ["UserPromptSubmit", "Stop", "PermissionRequest"]) {
1485
- hooks[eventName] = [
1486
- ...(Array.isArray(hooks[eventName]) ? hooks[eventName] : []),
1184
+ for (const definition of VCM_HOOK_DEFINITIONS) {
1185
+ hooks[definition.eventName] = [
1186
+ ...(Array.isArray(hooks[definition.eventName]) ? hooks[definition.eventName] : []),
1487
1187
  {
1188
+ ...(definition.matcher ? { matcher: definition.matcher } : {}),
1488
1189
  hooks: [
1489
1190
  {
1490
1191
  type: "command",
1491
- command: eventName === "PermissionRequest" ? VCM_PERMISSION_REQUEST_HOOK_COMMAND : VCM_HOOK_COMMAND,
1492
- timeout: 5
1192
+ command: definition.command,
1193
+ timeout: definition.timeout
1493
1194
  }
1494
1195
  ]
1495
1196
  }
@@ -1497,6 +1198,11 @@ function mergeVcmHooks(settings) {
1497
1198
  }
1498
1199
 
1499
1200
  next.hooks = hooks;
1201
+
1202
+ const env = isPlainObject(next.env) ? { ...next.env } : {};
1203
+ env.BASH_DEFAULT_TIMEOUT_MS = VCM_BASH_DEFAULT_TIMEOUT_MS;
1204
+ next.env = env;
1205
+
1500
1206
  return next;
1501
1207
  }
1502
1208
 
@@ -232,7 +232,7 @@ async function removeOwnedJson({ projectRoot, entry, dryRun, operations, warning
232
232
  return;
233
233
  }
234
234
 
235
- const nextValue = removeVcmHookMatchers(value, entry.jsonOwnership?.hookMatchers ?? ["VCM"]);
235
+ const nextValue = removeVcmOwnedSettings(value, entry.jsonOwnership ?? {});
236
236
  if (deepEqual(value, nextValue)) {
237
237
  operations.push(skip(entry.path, "no VCM-owned JSON values found"));
238
238
  return;
@@ -247,6 +247,23 @@ async function removeOwnedJson({ projectRoot, entry, dryRun, operations, warning
247
247
  operations.push(done(entry.path, "removed VCM-owned JSON values"));
248
248
  }
249
249
 
250
+ function removeVcmOwnedSettings(settings, jsonOwnership) {
251
+ const nextSettings = removeVcmHookMatchers(settings, jsonOwnership.hookMatchers ?? ["VCM"]);
252
+ const envKeys = Array.isArray(jsonOwnership.envKeys) ? jsonOwnership.envKeys : [];
253
+ if (envKeys.length > 0 && isPlainObject(nextSettings.env)) {
254
+ const env = { ...nextSettings.env };
255
+ for (const key of envKeys) {
256
+ delete env[key];
257
+ }
258
+ if (Object.keys(env).length > 0) {
259
+ nextSettings.env = env;
260
+ } else {
261
+ delete nextSettings.env;
262
+ }
263
+ }
264
+ return nextSettings;
265
+ }
266
+
250
267
  function removeVcmHookMatchers(settings, hookMatchers) {
251
268
  const nextSettings = structuredClone(settings);
252
269
  if (!isPlainObject(nextSettings.hooks)) {
@@ -8,6 +8,9 @@ const requiredFiles = [
8
8
  "scripts/fix-node-pty-spawn-helper.mjs",
9
9
  "scripts/harness-tools/generate-module-index",
10
10
  "scripts/harness-tools/generate-public-surface",
11
+ "scripts/harness-tools/run-long-check",
12
+ "scripts/harness-tools/watch-job",
13
+ "scripts/harness-tools/vcm-bash-guard",
11
14
  "scripts/install-vcm-harness.mjs",
12
15
  "scripts/uninstall-vcm-harness.mjs",
13
16
  "dist/main.js",
@@ -1,22 +0,0 @@
1
- export function renderKnownIssuesDocHarnessRules() {
2
- return `## VCM Known Issues Policy
3
-
4
- - Use this file only for confirmed unresolved issues that must survive across tasks.
5
- - Do not record current-task scratch notes, guesses, resolved issues, or ordinary TODOs here.
6
- - During a task, record unresolved findings in \`.ai/vcm/handoffs/known-issues.md\` first.
7
- - At task close, promote only still-relevant confirmed issues from the task-local file into this document.
8
- - Remove entries when they are fixed, rejected, obsolete, or moved into a concrete plan.
9
-
10
- ## Entry Format
11
-
12
- \`\`\`md
13
- ## YYYY-MM-DD <short issue title>
14
-
15
- - discovered in: <task or PR>
16
- - type: bug | limitation | technical-debt | validation-gap | docs-gap | decision-needed
17
- - impact: low | medium | high
18
- - status: open | planned | accepted
19
- - proposed action: <next useful step>
20
- \`\`\`
21
- `;
22
- }