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.
- package/README.md +7 -3
- package/dist/backend/cli/install-vcm-harness.js +57 -18
- 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 +46 -0
- package/dist/backend/services/gate-review-service.js +47 -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/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-CShYYQQa.css +32 -0
- package/dist-frontend/assets/{index-DqANomt1.js → index-DOvvS76Q.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,258 +1,333 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
the
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
221
|
+
function usageError(message) {
|
|
222
|
+
process.stderr.write(USAGE + "\nwatch-job: error: " + message + "\n");
|
|
223
|
+
process.exit(2);
|
|
224
|
+
}
|
|
253
225
|
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
raise SystemExit(main())
|
|
333
|
+
process.exitCode = main(process.argv.slice(2));
|