vibe-coding-master 0.8.13 → 0.9.0

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.
Files changed (42) hide show
  1. package/README.md +7 -3
  2. package/dist/backend/cli/install-vcm-harness.js +57 -18
  3. package/dist/backend/gateway/gateway-service.js +5 -3
  4. package/dist/backend/runtime/terminal-submit.js +1 -0
  5. package/dist/backend/server.js +14 -4
  6. package/dist/backend/services/app-settings-service.js +3 -2
  7. package/dist/backend/services/auto-memory-service.js +4 -3
  8. package/dist/backend/services/claude-hook-service.js +6 -0
  9. package/dist/backend/services/claude-transcript-service.js +2 -13
  10. package/dist/backend/services/claude-version-check.js +46 -0
  11. package/dist/backend/services/gate-review-service.js +47 -8
  12. package/dist/backend/services/harness-service.js +41 -20
  13. package/dist/backend/services/message-service.js +30 -6
  14. package/dist/backend/services/project-service.js +8 -2
  15. package/dist/backend/services/runtime-recovery-service.js +36 -9
  16. package/dist/backend/services/session-service.js +10 -19
  17. package/dist/backend/services/task-close-service.js +1 -0
  18. package/dist/backend/services/terminal-process-exit-service.js +4 -1
  19. package/dist/backend/services/workflow-control-service.js +55 -11
  20. package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
  21. package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
  22. package/dist/backend/templates/harness/gate-review.js +676 -643
  23. package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
  24. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
  25. package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
  26. package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
  27. package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
  28. package/dist/shared/types/app-settings.js +31 -0
  29. package/dist-frontend/assets/index-CShYYQQa.css +32 -0
  30. package/dist-frontend/assets/{index-DqANomt1.js → index-DOvvS76Q.js} +41 -41
  31. package/dist-frontend/index.html +2 -2
  32. package/package.json +1 -1
  33. package/scripts/harness-tools/check-durable-docs +332 -298
  34. package/scripts/harness-tools/generate-module-index +1047 -512
  35. package/scripts/harness-tools/generate-public-surface +1231 -1069
  36. package/scripts/harness-tools/package.json +3 -0
  37. package/scripts/harness-tools/run-long-check +608 -531
  38. package/scripts/harness-tools/vcm-artifact +181 -113
  39. package/scripts/harness-tools/vcm-bash-guard +839 -605
  40. package/scripts/harness-tools/vcm-subagent-guard +67 -63
  41. package/scripts/harness-tools/watch-job +313 -238
  42. package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
@@ -1,258 +1,333 @@
1
- #!/usr/bin/env python3
2
- """Watch one VCM long-running validation job in the foreground.
3
-
4
- Usage:
5
- .ai/tools/watch-job <job-id> [--window <duration>] [--interval <duration>]
6
-
7
- watch-job renews the job supervision lease while it runs. When the watch
8
- window elapses and the job is still running, it exits 125 WITHOUT stopping
9
- the job and grants a bounded handoff for the next watcher; call watch-job
10
- again immediately in the same turn. The job ceiling itself is enforced by
11
- the run-long-check worker, not by watch-job.
12
-
13
- Exit codes:
14
- 0 success
15
- 1 failed
16
- 124 timeout (job hit its ceiling and was killed by the worker)
17
- 125 still running; call watch-job again now
18
- 4 orphaned or stale (job lost supervision and was killed, or its worker died)
19
- 2 usage error or unknown job id
20
- """
21
- import argparse
22
- import json
23
- import os
24
- import sys
25
- import time
26
- from datetime import datetime, timedelta, timezone
27
- from pathlib import Path
28
-
29
- MAX_WINDOW_SECONDS = 8 * 60
30
- DEFAULT_WINDOW = "8m"
31
- STATUS_WAIT_SECONDS = 10
32
- DEFAULT_NEXT_WATCH_HANDOFF_GRACE_SECONDS = 5 * 60
33
- TERMINAL_EXIT_CODES = {
34
- "success": 0,
35
- "failed": 1,
36
- "timeout": 124,
37
- "orphaned": 4,
38
- "stale": 4,
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Watch one VCM long-running validation job in the foreground.
4
+ //
5
+ // Usage:
6
+ // .ai/tools/watch-job <job-id> [--window <duration>] [--interval <duration>]
7
+ //
8
+ // watch-job renews the job supervision lease while it runs. When the watch
9
+ // window elapses and the job is still running, it exits 125 WITHOUT stopping
10
+ // the job and grants a bounded handoff for the next watcher; call watch-job
11
+ // again immediately in the same turn. The job ceiling itself is enforced by
12
+ // the run-long-check worker, not by watch-job.
13
+ //
14
+ // Exit codes:
15
+ // 0 success
16
+ // 1 failed
17
+ // 124 timeout (job hit its ceiling and was killed by the worker)
18
+ // 125 still running; call watch-job again now
19
+ // 4 orphaned or stale (job lost supervision and was killed, or its worker died)
20
+ // 2 usage error or unknown job id
21
+
22
+ const fs = require("node:fs");
23
+ const path = require("node:path");
24
+
25
+ const MAX_WINDOW_SECONDS = 8 * 60;
26
+ const DEFAULT_WINDOW = "8m";
27
+ const STATUS_WAIT_SECONDS = 10;
28
+ const DEFAULT_NEXT_WATCH_HANDOFF_GRACE_SECONDS = 5 * 60;
29
+ const TERMINAL_EXIT_CODES = { success: 0, failed: 1, timeout: 124, orphaned: 4, stale: 4 };
30
+ const USAGE = "usage: watch-job [-h] [--window WINDOW] [--interval INTERVAL] job_id";
31
+
32
+ function rootDir() {
33
+ return path.resolve(path.dirname(fs.realpathSync(__filename)), "..", "..");
39
34
  }
40
35
 
36
+ function parseNumber(value) {
37
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(value)) {
38
+ throw new Error("could not convert string to float: '" + value + "'");
39
+ }
40
+ return Number(value);
41
+ }
41
42
 
42
- def root_dir() -> Path:
43
- return Path(__file__).resolve().parents[2]
44
-
45
-
46
- def parse_duration(value: str) -> float:
47
- value = value.strip().lower()
48
- if value.endswith("ms"):
49
- return float(value[:-2]) / 1000
50
- if value.endswith("s"):
51
- return float(value[:-1])
52
- if value.endswith("m"):
53
- return float(value[:-1]) * 60
54
- if value.endswith("h"):
55
- return float(value[:-1]) * 3600
56
- return float(value)
57
-
58
-
59
- def read_optional_json(path: Path) -> dict | None:
60
- try:
61
- return json.loads(path.read_text())
62
- except (OSError, ValueError):
63
- return None
64
-
65
-
66
- def write_json(path: Path, data: dict) -> None:
67
- tmp = path.with_suffix(path.suffix + ".tmp")
68
- tmp.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
69
- tmp.replace(path)
70
-
71
-
72
- def env_seconds(name: str, default: float) -> float:
73
- raw = os.environ.get(name)
74
- if not raw:
75
- return default
76
- try:
77
- return max(1.0, float(raw))
78
- except ValueError:
79
- return default
80
-
81
-
82
- def clear_handoff(handoff_path: Path) -> None:
83
- try:
84
- handoff_path.unlink()
85
- except FileNotFoundError:
86
- pass
87
-
88
-
89
- def begin_handoff(handoff_path: Path, grace_seconds: float) -> str:
90
- now = datetime.now(timezone.utc)
91
- due_at = (now + timedelta(seconds=grace_seconds)).isoformat().replace("+00:00", "Z")
92
- write_json(
93
- handoff_path,
94
- {
95
- "createdAt": now.isoformat().replace("+00:00", "Z"),
96
- "nextWatchDueAt": due_at,
97
- },
98
- )
99
- return due_at
100
-
101
-
102
- def renew_lease(lease_path: Path) -> None:
103
- try:
104
- lease_path.touch()
105
- except OSError:
106
- pass
107
-
108
-
109
- def tail(path: Path, lines: int = 40, max_bytes: int = 65536) -> str:
110
- try:
111
- size = path.stat().st_size
112
- except OSError:
113
- return ""
114
- try:
115
- with path.open("rb") as handle:
116
- handle.seek(max(0, size - max_bytes))
117
- data = handle.read(max_bytes)
118
- except OSError:
119
- return ""
120
- text = data.decode("utf-8", errors="replace")
121
- return "\n".join(text.splitlines()[-lines:])
122
-
123
-
124
- def elapsed_seconds(status: dict) -> float | None:
125
- started_at = status.get("startedAt")
126
- if not started_at:
127
- return None
128
- from datetime import datetime, timezone
129
-
130
- try:
131
- started = datetime.fromisoformat(str(started_at).replace("Z", "+00:00"))
132
- except ValueError:
133
- return None
134
- return round((datetime.now(timezone.utc) - started).total_seconds(), 3)
43
+ function parseDuration(value) {
44
+ const normalized = value.trim().toLowerCase();
45
+ if (normalized.endsWith("ms")) {
46
+ return parseNumber(normalized.slice(0, -2)) / 1000;
47
+ }
48
+ if (normalized.endsWith("s")) {
49
+ return parseNumber(normalized.slice(0, -1));
50
+ }
51
+ if (normalized.endsWith("m")) {
52
+ return parseNumber(normalized.slice(0, -1)) * 60;
53
+ }
54
+ if (normalized.endsWith("h")) {
55
+ return parseNumber(normalized.slice(0, -1)) * 3600;
56
+ }
57
+ return parseNumber(normalized);
58
+ }
135
59
 
60
+ function sortKeys(value) {
61
+ if (Array.isArray(value)) {
62
+ return value.map(sortKeys);
63
+ }
64
+ if (value && typeof value === "object") {
65
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])]));
66
+ }
67
+ return value;
68
+ }
136
69
 
137
- def print_summary(job_id: str, status: dict, directory: Path) -> None:
138
- state = status.get("status")
139
- print(f"job: {job_id}")
140
- print(f"status: {state}")
141
- print(f"exitCode: {status.get('exitCode')}")
142
- print(f"durationSeconds: {status.get('durationSeconds')}")
143
- if state == "timeout":
144
- print(f"timeoutSeconds: {status.get('timeoutSeconds')}")
145
- print(f"processStopResult: {status.get('processStopResult')}")
146
- if state == "orphaned":
147
- print(f"orphanReason: {status.get('orphanReason')}")
148
- print(f"lastWatchedAt: {status.get('lastWatchedAt')}")
149
- if status.get("nextWatchDueAt"):
150
- print(f"nextWatchDueAt: {status.get('nextWatchDueAt')}")
151
- print(f"processStopResult: {status.get('processStopResult')}")
152
- if state == "stale":
153
- print(f"staleReason: {status.get('staleReason')}")
70
+ function readOptionalJson(target) {
71
+ try {
72
+ return JSON.parse(fs.readFileSync(target, "utf8"));
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
154
77
 
155
- if state in {"failed", "timeout", "orphaned", "stale"}:
156
- stdout_tail = tail(directory / "stdout.log")
157
- stderr_tail = tail(directory / "stderr.log")
158
- if stdout_tail:
159
- print("\nstdout tail:")
160
- print(stdout_tail)
161
- if stderr_tail:
162
- print("\nstderr tail:")
163
- print(stderr_tail)
78
+ function writeJson(target, data) {
79
+ const temporary = target + ".tmp";
80
+ fs.writeFileSync(temporary, JSON.stringify(sortKeys(data), null, 2) + "\n");
81
+ fs.renameSync(temporary, target);
82
+ }
164
83
 
84
+ function envSeconds(name, fallback) {
85
+ const raw = process.env[name];
86
+ if (!raw) {
87
+ return fallback;
88
+ }
89
+ try {
90
+ return Math.max(1, parseNumber(raw));
91
+ } catch {
92
+ return fallback;
93
+ }
94
+ }
165
95
 
166
- def print_progress(job_id: str, status: dict, directory: Path, window: float, next_watch_due_at: str) -> None:
167
- print(f"job: {job_id}")
168
- print("status: still-running")
169
- print(f"jobStatus: {status.get('status')}")
170
- elapsed = elapsed_seconds(status)
171
- if elapsed is not None:
172
- print(f"elapsedSeconds: {elapsed}")
173
- ceiling = status.get("timeoutSeconds")
174
- if isinstance(ceiling, (int, float)):
175
- print(f"ceilingRemainingSeconds: {max(0, round(ceiling - elapsed))}")
176
- print(f"watchWindowSeconds: {int(window)}")
177
- print(f"nextWatchDueAt: {next_watch_due_at}")
178
- stdout_tail = tail(directory / "stdout.log", lines=5)
179
- if stdout_tail:
180
- print("\nstdout tail:")
181
- print(stdout_tail)
182
- print("\njob still running - call .ai/tools/watch-job again now; do not end the turn.")
96
+ function sleepSeconds(seconds) {
97
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, seconds * 1000));
98
+ }
183
99
 
100
+ function clearHandoff(handoffPath) {
101
+ fs.rmSync(handoffPath, { force: true });
102
+ }
184
103
 
185
- def main() -> int:
186
- parser = argparse.ArgumentParser(
187
- description="Watch a file-backed long-running validation job."
188
- )
189
- parser.add_argument("job_id")
190
- parser.add_argument("--window", default=DEFAULT_WINDOW)
191
- parser.add_argument("--interval", default="1s")
192
- parser.add_argument("--timeout", dest="legacy_timeout", default=None, help=argparse.SUPPRESS)
193
- args = parser.parse_args()
104
+ function beginHandoff(handoffPath, graceSeconds) {
105
+ const now = new Date();
106
+ const dueAt = new Date(now.getTime() + graceSeconds * 1000).toISOString();
107
+ writeJson(handoffPath, { createdAt: now.toISOString(), nextWatchDueAt: dueAt });
108
+ return dueAt;
109
+ }
194
110
 
195
- if args.legacy_timeout is not None:
196
- print(
197
- "error: watch-job no longer takes --timeout; the job ceiling is set by"
198
- " run-long-check --timeout. Use --window (max 8m) and call watch-job"
199
- " repeatedly until it reports a terminal result.",
200
- file=sys.stderr,
201
- )
202
- return 2
111
+ function renewLease(leasePath) {
112
+ try {
113
+ const now = new Date();
114
+ try {
115
+ fs.utimesSync(leasePath, now, now);
116
+ } catch {
117
+ fs.closeSync(fs.openSync(leasePath, "a"));
118
+ }
119
+ } catch {
120
+ // A lease that cannot be renewed lets the worker reap the job.
121
+ }
122
+ }
203
123
 
204
- try:
205
- window = parse_duration(args.window)
206
- interval = parse_duration(args.interval)
207
- except ValueError as exc:
208
- print(f"error: invalid duration: {exc}", file=sys.stderr)
209
- return 2
210
- if window <= 0 or interval <= 0:
211
- print("error: window and interval must be positive", file=sys.stderr)
212
- return 2
213
- if window > MAX_WINDOW_SECONDS:
214
- print("error: window exceeds maximum 8m; call watch-job repeatedly instead", file=sys.stderr)
215
- return 2
124
+ function tail(target, lines = 40, maxBytes = 65536) {
125
+ let size;
126
+ try {
127
+ size = fs.statSync(target).size;
128
+ } catch {
129
+ return "";
130
+ }
131
+ let data;
132
+ try {
133
+ const descriptor = fs.openSync(target, "r");
134
+ const length = Math.min(size, maxBytes);
135
+ data = Buffer.alloc(length);
136
+ fs.readSync(descriptor, data, 0, length, Math.max(0, size - maxBytes));
137
+ fs.closeSync(descriptor);
138
+ } catch {
139
+ return "";
140
+ }
141
+ return data.toString("utf8").split(/\r\n|\n|\r/).filter((line, index, all) => index < all.length - 1 || line !== "").slice(-lines).join("\n");
142
+ }
216
143
 
217
- directory = root_dir() / ".ai/vcm/jobs" / args.job_id
218
- status_path = directory / "status.json"
219
- lease_path = directory / "lease"
220
- handoff_path = directory / "watch-handoff.json"
221
- handoff_grace = env_seconds(
222
- "VCM_JOB_NEXT_WATCH_HANDOFF_GRACE_SECONDS",
223
- DEFAULT_NEXT_WATCH_HANDOFF_GRACE_SECONDS,
224
- )
144
+ // Matches the Python repr used in earlier tool output for status values.
145
+ function display(value) {
146
+ if (value === null || value === undefined) {
147
+ return "None";
148
+ }
149
+ if (value === true) {
150
+ return "True";
151
+ }
152
+ if (value === false) {
153
+ return "False";
154
+ }
155
+ return String(value);
156
+ }
225
157
 
226
- found_deadline = time.time() + STATUS_WAIT_SECONDS
227
- while not status_path.is_file():
228
- if time.time() >= found_deadline:
229
- print(f"error: unknown job id: {args.job_id} (no {status_path})", file=sys.stderr)
230
- return 2
231
- time.sleep(0.2)
158
+ function elapsedSeconds(status) {
159
+ if (!status.startedAt) {
160
+ return null;
161
+ }
162
+ const started = Date.parse(String(status.startedAt));
163
+ return Number.isNaN(started) ? null : Math.round(Date.now() - started) / 1000;
164
+ }
232
165
 
233
- renew_lease(lease_path)
234
- clear_handoff(handoff_path)
166
+ function printSummary(jobId, status, directory) {
167
+ const state = status.status;
168
+ const lines = [
169
+ "job: " + jobId,
170
+ "status: " + display(state),
171
+ "exitCode: " + display(status.exitCode),
172
+ "durationSeconds: " + display(status.durationSeconds)
173
+ ];
174
+ if (state === "timeout") {
175
+ lines.push("timeoutSeconds: " + display(status.timeoutSeconds));
176
+ lines.push("processStopResult: " + display(status.processStopResult));
177
+ }
178
+ if (state === "orphaned") {
179
+ lines.push("orphanReason: " + display(status.orphanReason));
180
+ lines.push("lastWatchedAt: " + display(status.lastWatchedAt));
181
+ if (status.nextWatchDueAt) {
182
+ lines.push("nextWatchDueAt: " + status.nextWatchDueAt);
183
+ }
184
+ lines.push("processStopResult: " + display(status.processStopResult));
185
+ }
186
+ if (state === "stale") {
187
+ lines.push("staleReason: " + display(status.staleReason));
188
+ }
189
+ if (["failed", "timeout", "orphaned", "stale"].includes(state)) {
190
+ const stdoutTail = tail(path.join(directory, "stdout.log"));
191
+ const stderrTail = tail(path.join(directory, "stderr.log"));
192
+ if (stdoutTail) {
193
+ lines.push("\nstdout tail:", stdoutTail);
194
+ }
195
+ if (stderrTail) {
196
+ lines.push("\nstderr tail:", stderrTail);
197
+ }
198
+ }
199
+ process.stdout.write(lines.join("\n") + "\n");
200
+ }
235
201
 
236
- deadline = time.time() + window
237
- last_status: dict = {}
238
- while True:
239
- renew_lease(lease_path)
240
- status = read_optional_json(status_path)
241
- if status:
242
- last_status = status
243
- state = status.get("status")
244
- if state in TERMINAL_EXIT_CODES:
245
- clear_handoff(handoff_path)
246
- print_summary(args.job_id, status, directory)
247
- return TERMINAL_EXIT_CODES[state]
202
+ function printProgress(jobId, status, directory, windowSeconds, nextWatchDueAt) {
203
+ const lines = ["job: " + jobId, "status: still-running", "jobStatus: " + display(status.status)];
204
+ const elapsed = elapsedSeconds(status);
205
+ if (elapsed !== null) {
206
+ lines.push("elapsedSeconds: " + elapsed);
207
+ if (typeof status.timeoutSeconds === "number") {
208
+ lines.push("ceilingRemainingSeconds: " + Math.max(0, Math.round(status.timeoutSeconds - elapsed)));
209
+ }
210
+ }
211
+ lines.push("watchWindowSeconds: " + Math.trunc(windowSeconds));
212
+ lines.push("nextWatchDueAt: " + nextWatchDueAt);
213
+ const stdoutTail = tail(path.join(directory, "stdout.log"), 5);
214
+ if (stdoutTail) {
215
+ lines.push("\nstdout tail:", stdoutTail);
216
+ }
217
+ lines.push("\njob still running - call .ai/tools/watch-job again now; do not end the turn.");
218
+ process.stdout.write(lines.join("\n") + "\n");
219
+ }
248
220
 
249
- if time.time() >= deadline:
250
- next_watch_due_at = begin_handoff(handoff_path, handoff_grace)
251
- print_progress(args.job_id, last_status, directory, window, next_watch_due_at)
252
- return 125
221
+ function usageError(message) {
222
+ process.stderr.write(USAGE + "\nwatch-job: error: " + message + "\n");
223
+ process.exit(2);
224
+ }
253
225
 
254
- time.sleep(interval)
226
+ function parseArgs(argv) {
227
+ const args = { jobId: undefined, window: DEFAULT_WINDOW, interval: "1s", legacyTimeout: undefined };
228
+ const optionNames = { "--window": "window", "--interval": "interval", "--timeout": "legacyTimeout" };
229
+ for (let index = 0; index < argv.length; index += 1) {
230
+ const argument = argv[index];
231
+ if (argument === "-h" || argument === "--help") {
232
+ process.stdout.write(USAGE + "\n\nWatch a file-backed long-running validation job.\n");
233
+ process.exit(0);
234
+ }
235
+ const separator = argument.indexOf("=");
236
+ const name = argument.startsWith("--") && separator >= 0 ? argument.slice(0, separator) : argument;
237
+ if (optionNames[name]) {
238
+ let value;
239
+ if (separator >= 0 && argument.startsWith("--")) {
240
+ value = argument.slice(separator + 1);
241
+ } else {
242
+ if (index + 1 >= argv.length) {
243
+ usageError("argument " + name + ": expected one argument");
244
+ }
245
+ index += 1;
246
+ value = argv[index];
247
+ }
248
+ args[optionNames[name]] = value;
249
+ continue;
250
+ }
251
+ if (argument.startsWith("--") || args.jobId !== undefined) {
252
+ usageError("unrecognized arguments: " + argument);
253
+ }
254
+ args.jobId = argument;
255
+ }
256
+ if (args.jobId === undefined) {
257
+ usageError("the following arguments are required: job_id");
258
+ }
259
+ return args;
260
+ }
255
261
 
262
+ function main(argv) {
263
+ const args = parseArgs(argv);
264
+ if (args.legacyTimeout !== undefined) {
265
+ process.stderr.write(
266
+ "error: watch-job no longer takes --timeout; the job ceiling is set by"
267
+ + " run-long-check --timeout. Use --window (max 8m) and call watch-job"
268
+ + " repeatedly until it reports a terminal result.\n"
269
+ );
270
+ return 2;
271
+ }
272
+
273
+ let windowSeconds;
274
+ let intervalSeconds;
275
+ try {
276
+ windowSeconds = parseDuration(args.window);
277
+ intervalSeconds = parseDuration(args.interval);
278
+ } catch (error) {
279
+ process.stderr.write("error: invalid duration: " + error.message + "\n");
280
+ return 2;
281
+ }
282
+ if (windowSeconds <= 0 || intervalSeconds <= 0) {
283
+ process.stderr.write("error: window and interval must be positive\n");
284
+ return 2;
285
+ }
286
+ if (windowSeconds > MAX_WINDOW_SECONDS) {
287
+ process.stderr.write("error: window exceeds maximum 8m; call watch-job repeatedly instead\n");
288
+ return 2;
289
+ }
290
+
291
+ const directory = path.join(rootDir(), ".ai/vcm/jobs", args.jobId);
292
+ const statusPath = path.join(directory, "status.json");
293
+ const leasePath = path.join(directory, "lease");
294
+ const handoffPath = path.join(directory, "watch-handoff.json");
295
+ const handoffGrace = envSeconds("VCM_JOB_NEXT_WATCH_HANDOFF_GRACE_SECONDS", DEFAULT_NEXT_WATCH_HANDOFF_GRACE_SECONDS);
296
+
297
+ const foundDeadline = Date.now() + STATUS_WAIT_SECONDS * 1000;
298
+ while (!fs.existsSync(statusPath) || !fs.statSync(statusPath).isFile()) {
299
+ if (Date.now() >= foundDeadline) {
300
+ process.stderr.write("error: unknown job id: " + args.jobId + " (no " + statusPath + ")\n");
301
+ return 2;
302
+ }
303
+ sleepSeconds(0.2);
304
+ }
305
+
306
+ renewLease(leasePath);
307
+ clearHandoff(handoffPath);
308
+
309
+ const deadline = Date.now() + windowSeconds * 1000;
310
+ let lastStatus = {};
311
+ while (true) {
312
+ renewLease(leasePath);
313
+ const status = readOptionalJson(statusPath);
314
+ if (status) {
315
+ lastStatus = status;
316
+ if (Object.hasOwn(TERMINAL_EXIT_CODES, status.status)) {
317
+ clearHandoff(handoffPath);
318
+ printSummary(args.jobId, status, directory);
319
+ return TERMINAL_EXIT_CODES[status.status];
320
+ }
321
+ }
322
+
323
+ if (Date.now() >= deadline) {
324
+ const nextWatchDueAt = beginHandoff(handoffPath, handoffGrace);
325
+ printProgress(args.jobId, lastStatus, directory, windowSeconds, nextWatchDueAt);
326
+ return 125;
327
+ }
328
+
329
+ sleepSeconds(intervalSeconds);
330
+ }
331
+ }
256
332
 
257
- if __name__ == "__main__":
258
- raise SystemExit(main())
333
+ process.exitCode = main(process.argv.slice(2));