vibe-coding-master 0.7.39 → 0.7.41
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 +11 -6
- package/dist/backend/adapters/claude-adapter.js +8 -6
- package/dist/backend/api/artifact-routes.js +72 -1
- package/dist/backend/api/runtime-state-routes.js +17 -5
- package/dist/backend/api/task-routes.js +6 -0
- package/dist/backend/api/workflow-control-routes.js +56 -0
- package/dist/backend/cli/install-vcm-harness.js +42 -47
- package/dist/backend/role-tool-policy.js +60 -0
- package/dist/backend/server.js +21 -5
- package/dist/backend/services/artifact-service.js +309 -5
- package/dist/backend/services/auto-memory-service.js +2 -2
- package/dist/backend/services/gate-review-service.js +81 -33
- package/dist/backend/services/harness-feedback-service.js +11 -4
- package/dist/backend/services/harness-service.js +35 -45
- package/dist/backend/services/lsp-plugin.js +1 -4
- package/dist/backend/services/message-service.js +105 -37
- package/dist/backend/services/status-service.js +6 -0
- package/dist/backend/services/workflow-control-service.js +854 -0
- package/dist/backend/templates/handoff.js +158 -3
- package/dist/backend/templates/harness/architect-agent.js +3 -2
- package/dist/backend/templates/harness/check-scaffold-ledger.js +105 -39
- package/dist/backend/templates/harness/claude-root.js +9 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -2
- package/dist/backend/templates/harness/coder-worker-agent.js +1 -1
- package/dist/backend/templates/harness/gate-review.js +3 -4
- package/dist/backend/templates/harness/harness-engineer-agent.js +18 -1
- package/dist/backend/templates/harness/project-manager-agent.js +3 -2
- package/dist/backend/templates/harness/tester-agent.js +1 -0
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +5 -0
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +3 -4
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +4 -2
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -2
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +2 -1
- package/dist/backend/templates/harness/vcm-route-message-skill.js +4 -7
- package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -13
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +62 -0
- package/dist/backend/templates/message-envelope.js +4 -4
- package/dist/shared/types/workflow.js +7 -1
- package/dist/shared/validation/artifact-check.js +99 -95
- package/dist/shared/validation/artifact-contract.js +9 -0
- package/dist/shared/validation/artifact-registry.js +211 -0
- package/dist-frontend/assets/{index-DkM0mnQD.js → index-Bocc2DWF.js} +34 -34
- package/dist-frontend/assets/{index-CXSOe-NN.css → index-C_XHGNBD.css} +1 -1
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +2 -1
- package/scripts/harness-tools/run-long-check +38 -8
- package/scripts/harness-tools/vcm-artifact +148 -0
- package/scripts/harness-tools/vcm-bash-guard +42 -47
- package/scripts/harness-tools/watch-job +58 -4
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
"""VCM PreToolUse guard for supervised tools inside VCM role sessions.
|
|
3
3
|
|
|
4
4
|
Reads the Claude Code PreToolUse hook payload on stdin. It enforces supervised
|
|
5
|
-
Bash execution for every VCM role
|
|
6
|
-
semantic code navigation with the Grep tool or shell text-search commands.
|
|
5
|
+
Bash execution for every VCM role.
|
|
7
6
|
|
|
8
7
|
Quoted payloads of `sh -c` / `bash -lc` style invocations are executable
|
|
9
8
|
shell code, so they are scanned recursively. `.ai/tools/run-long-check` is the
|
|
@@ -11,7 +10,6 @@ only sanctioned detached worker; the command it runs must still stay in the
|
|
|
11
10
|
supervised foreground process group.
|
|
12
11
|
"""
|
|
13
12
|
import json
|
|
14
|
-
import os
|
|
15
13
|
import re
|
|
16
14
|
import sys
|
|
17
15
|
|
|
@@ -21,12 +19,11 @@ SKILL_HINT = (
|
|
|
21
19
|
"`.ai/tools/watch-job <job-id>` in the same turn, repeating watch-job "
|
|
22
20
|
"until it reports a terminal result."
|
|
23
21
|
)
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
22
|
+
ARTIFACT_HINT = (
|
|
23
|
+
"VCM-managed Markdown cannot be written directly. Write a candidate outside "
|
|
24
|
+
".ai/vcm, then run .ai/tools/vcm-artifact <kind> --file <candidate> "
|
|
25
|
+
"--mode <draft|final>."
|
|
27
26
|
)
|
|
28
|
-
LSP_ROLES = {"architect", "coder", "reviewer"}
|
|
29
|
-
|
|
30
27
|
MAX_NESTED_SHELL_DEPTH = 3
|
|
31
28
|
QUOTED = re.compile(r"'([^']*)'|\"([^\"]*)\"")
|
|
32
29
|
SHELL_DASH_C = re.compile(r"(?:^|[\s;&|(])(?:sh|bash|zsh|dash|ksh)\s+(?:-\w+\s+)*-\w*c\w*(?:\s|$)")
|
|
@@ -44,22 +41,20 @@ RUN_LONG_CHECK_SHELL_STRING = re.compile(
|
|
|
44
41
|
r"(?:-\w+\s+)*-\w*c\w*(?:\s|$)"
|
|
45
42
|
)
|
|
46
43
|
COMMAND_SUBSTITUTION = re.compile(r"\$\(([^()]*)\)|`([^`]*)`")
|
|
47
|
-
|
|
48
|
-
r"(?:^|[
|
|
49
|
-
r"
|
|
50
|
-
r"
|
|
51
|
-
r"
|
|
52
|
-
r"
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
r"(?:^|[;&|()\n`])\s*(?:git)(?:\s+-[^\s]+)*\s+grep(?=\s|$|[;&|()])"
|
|
44
|
+
MANAGED_ARTIFACT_PATH = re.compile(
|
|
45
|
+
r"(?:^|[\s/\\])\.ai[/\\]vcm[/\\](?:"
|
|
46
|
+
r"handoffs[/\\].*\.md|"
|
|
47
|
+
r"gate-reviews[/\\]requests[/\\].*\.report\.md|"
|
|
48
|
+
r"memory-review[/\\].*\.md|"
|
|
49
|
+
r"coder-workers[/\\]reports[/\\].*\.md|"
|
|
50
|
+
r"harness-feedback[/\\](?:pending|task-retrospectives)[/\\].*\.md"
|
|
51
|
+
r")"
|
|
56
52
|
)
|
|
57
|
-
|
|
58
|
-
r"(
|
|
59
|
-
r"
|
|
53
|
+
SHELL_WRITE = re.compile(
|
|
54
|
+
r"(?:>>?|\btee\b|\brm\b|\bmv\b|\bcp\b|"
|
|
55
|
+
r"\bsed\b[^\n]*\s-i\b|\bperl\b[^\n]*\s-i\b|"
|
|
56
|
+
r"\bpython\d*\b|\bnode\b)"
|
|
60
57
|
)
|
|
61
|
-
|
|
62
|
-
|
|
63
58
|
def strip_quoted(command: str) -> str:
|
|
64
59
|
return QUOTED.sub(" ", command)
|
|
65
60
|
|
|
@@ -101,7 +96,7 @@ def protected_tool_invoked(command_without_quotes: str) -> bool:
|
|
|
101
96
|
return bool(PROTECTED_TOOL_INVOCATION.search(command_without_quotes))
|
|
102
97
|
|
|
103
98
|
|
|
104
|
-
def scan_shell_command(command: str, depth: int = 0
|
|
99
|
+
def scan_shell_command(command: str, depth: int = 0) -> list[str]:
|
|
105
100
|
reasons = []
|
|
106
101
|
stripped = strip_escaped_characters(strip_quoted(command))
|
|
107
102
|
if re.search(r"(?:^|[\s;&|(])(?:nohup|setsid)(?:\s|$)", stripped):
|
|
@@ -110,13 +105,6 @@ def scan_shell_command(command: str, depth: int = 0, forbid_text_search: bool =
|
|
|
110
105
|
reasons.append("disown is forbidden")
|
|
111
106
|
if unquoted_ampersand(stripped):
|
|
112
107
|
reasons.append("'&' background execution is forbidden")
|
|
113
|
-
if forbid_text_search and (
|
|
114
|
-
TEXT_SEARCH_COMMAND.search(stripped)
|
|
115
|
-
or GIT_GREP_COMMAND.search(stripped)
|
|
116
|
-
or WRAPPED_TEXT_SEARCH_COMMAND.search(stripped)
|
|
117
|
-
):
|
|
118
|
-
reasons.append("text-search commands are forbidden for this LSP-enabled role")
|
|
119
|
-
|
|
120
108
|
if protected_tool_invoked(stripped):
|
|
121
109
|
if SHELL_CONTROL_OPERATOR.search(stripped):
|
|
122
110
|
reasons.append("run-long-check and watch-job must be standalone Bash commands")
|
|
@@ -128,17 +116,17 @@ def scan_shell_command(command: str, depth: int = 0, forbid_text_search: bool =
|
|
|
128
116
|
for segment in quoted_segments(command):
|
|
129
117
|
if protected_tool_invoked(strip_escaped_characters(strip_quoted(segment))):
|
|
130
118
|
reasons.append("run-long-check and watch-job must not be invoked through a shell command string")
|
|
131
|
-
reasons.extend(scan_shell_command(segment, depth + 1
|
|
119
|
+
reasons.extend(scan_shell_command(segment, depth + 1))
|
|
132
120
|
if depth < MAX_NESTED_SHELL_DEPTH:
|
|
133
121
|
for substitution in double_quoted_command_substitutions(command):
|
|
134
122
|
nested_stripped = strip_escaped_characters(strip_quoted(substitution))
|
|
135
123
|
if protected_tool_invoked(nested_stripped):
|
|
136
124
|
reasons.append("run-long-check and watch-job must not be invoked through command substitution")
|
|
137
|
-
reasons.extend(scan_shell_command(substitution, depth + 1
|
|
125
|
+
reasons.extend(scan_shell_command(substitution, depth + 1))
|
|
138
126
|
return reasons
|
|
139
127
|
|
|
140
128
|
|
|
141
|
-
def guard_reasons(tool_input: dict
|
|
129
|
+
def guard_reasons(tool_input: dict) -> list[str]:
|
|
142
130
|
reasons = []
|
|
143
131
|
if tool_input.get("run_in_background"):
|
|
144
132
|
reasons.append("Bash run_in_background is forbidden")
|
|
@@ -146,7 +134,14 @@ def guard_reasons(tool_input: dict, forbid_text_search: bool = False) -> list[st
|
|
|
146
134
|
command = tool_input.get("command")
|
|
147
135
|
command = command if isinstance(command, str) else ""
|
|
148
136
|
|
|
149
|
-
reasons.extend(scan_shell_command(command
|
|
137
|
+
reasons.extend(scan_shell_command(command))
|
|
138
|
+
unquoted_path_command = command.replace('"', "").replace("'", "")
|
|
139
|
+
if (
|
|
140
|
+
MANAGED_ARTIFACT_PATH.search(unquoted_path_command)
|
|
141
|
+
and "vcm-artifact" not in command
|
|
142
|
+
and SHELL_WRITE.search(strip_quoted(command))
|
|
143
|
+
):
|
|
144
|
+
reasons.append("direct Bash writes to VCM-managed Markdown are forbidden")
|
|
150
145
|
return list(dict.fromkeys(reasons))
|
|
151
146
|
|
|
152
147
|
|
|
@@ -157,25 +152,24 @@ def main() -> int:
|
|
|
157
152
|
except ValueError:
|
|
158
153
|
return 0
|
|
159
154
|
tool_name = payload.get("tool_name")
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
reasons =
|
|
164
|
-
elif tool_name
|
|
155
|
+
if tool_name == "Bash":
|
|
156
|
+
tool_input = payload.get("tool_input")
|
|
157
|
+
tool_input = tool_input if isinstance(tool_input, dict) else {}
|
|
158
|
+
reasons = guard_reasons(tool_input)
|
|
159
|
+
elif tool_name in ("Write", "Edit"):
|
|
165
160
|
tool_input = payload.get("tool_input")
|
|
166
161
|
tool_input = tool_input if isinstance(tool_input, dict) else {}
|
|
167
|
-
|
|
162
|
+
file_path = tool_input.get("file_path") or tool_input.get("path")
|
|
163
|
+
reasons = (
|
|
164
|
+
["direct Write/Edit of VCM-managed Markdown is forbidden"]
|
|
165
|
+
if isinstance(file_path, str) and MANAGED_ARTIFACT_PATH.search(file_path)
|
|
166
|
+
else []
|
|
167
|
+
)
|
|
168
168
|
else:
|
|
169
169
|
return 0
|
|
170
170
|
if not reasons:
|
|
171
171
|
return 0
|
|
172
172
|
|
|
173
|
-
hints = []
|
|
174
|
-
if any("text-search" in reason or "Grep tool" in reason for reason in reasons):
|
|
175
|
-
hints.append(CODE_NAV_HINT)
|
|
176
|
-
if any("text-search" not in reason and "Grep tool" not in reason for reason in reasons):
|
|
177
|
-
hints.append(SKILL_HINT)
|
|
178
|
-
|
|
179
173
|
print(
|
|
180
174
|
json.dumps(
|
|
181
175
|
{
|
|
@@ -183,7 +177,8 @@ def main() -> int:
|
|
|
183
177
|
"hookEventName": "PreToolUse",
|
|
184
178
|
"permissionDecision": "deny",
|
|
185
179
|
"permissionDecisionReason": (
|
|
186
|
-
"VCM denied this tool call (" + "; ".join(reasons) + "). "
|
|
180
|
+
"VCM denied this tool call (" + "; ".join(reasons) + "). "
|
|
181
|
+
+ (ARTIFACT_HINT if any("Markdown" in reason for reason in reasons) else SKILL_HINT)
|
|
187
182
|
),
|
|
188
183
|
}
|
|
189
184
|
}
|
|
@@ -6,8 +6,9 @@ Usage:
|
|
|
6
6
|
|
|
7
7
|
watch-job renews the job supervision lease while it runs. When the watch
|
|
8
8
|
window elapses and the job is still running, it exits 125 WITHOUT stopping
|
|
9
|
-
the job
|
|
10
|
-
|
|
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.
|
|
11
12
|
|
|
12
13
|
Exit codes:
|
|
13
14
|
0 success
|
|
@@ -19,13 +20,16 @@ Exit codes:
|
|
|
19
20
|
"""
|
|
20
21
|
import argparse
|
|
21
22
|
import json
|
|
23
|
+
import os
|
|
22
24
|
import sys
|
|
23
25
|
import time
|
|
26
|
+
from datetime import datetime, timedelta, timezone
|
|
24
27
|
from pathlib import Path
|
|
25
28
|
|
|
26
29
|
MAX_WINDOW_SECONDS = 8 * 60
|
|
27
30
|
DEFAULT_WINDOW = "8m"
|
|
28
31
|
STATUS_WAIT_SECONDS = 10
|
|
32
|
+
DEFAULT_NEXT_WATCH_HANDOFF_GRACE_SECONDS = 5 * 60
|
|
29
33
|
TERMINAL_EXIT_CODES = {
|
|
30
34
|
"success": 0,
|
|
31
35
|
"failed": 1,
|
|
@@ -59,6 +63,42 @@ def read_optional_json(path: Path) -> dict | None:
|
|
|
59
63
|
return None
|
|
60
64
|
|
|
61
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
|
+
|
|
62
102
|
def renew_lease(lease_path: Path) -> None:
|
|
63
103
|
try:
|
|
64
104
|
lease_path.touch()
|
|
@@ -105,6 +145,9 @@ def print_summary(job_id: str, status: dict, directory: Path) -> None:
|
|
|
105
145
|
print(f"processStopResult: {status.get('processStopResult')}")
|
|
106
146
|
if state == "orphaned":
|
|
107
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')}")
|
|
108
151
|
print(f"processStopResult: {status.get('processStopResult')}")
|
|
109
152
|
if state == "stale":
|
|
110
153
|
print(f"staleReason: {status.get('staleReason')}")
|
|
@@ -120,7 +163,7 @@ def print_summary(job_id: str, status: dict, directory: Path) -> None:
|
|
|
120
163
|
print(stderr_tail)
|
|
121
164
|
|
|
122
165
|
|
|
123
|
-
def print_progress(job_id: str, status: dict, directory: Path, window: float) -> None:
|
|
166
|
+
def print_progress(job_id: str, status: dict, directory: Path, window: float, next_watch_due_at: str) -> None:
|
|
124
167
|
print(f"job: {job_id}")
|
|
125
168
|
print("status: still-running")
|
|
126
169
|
print(f"jobStatus: {status.get('status')}")
|
|
@@ -131,6 +174,7 @@ def print_progress(job_id: str, status: dict, directory: Path, window: float) ->
|
|
|
131
174
|
if isinstance(ceiling, (int, float)):
|
|
132
175
|
print(f"ceilingRemainingSeconds: {max(0, round(ceiling - elapsed))}")
|
|
133
176
|
print(f"watchWindowSeconds: {int(window)}")
|
|
177
|
+
print(f"nextWatchDueAt: {next_watch_due_at}")
|
|
134
178
|
stdout_tail = tail(directory / "stdout.log", lines=5)
|
|
135
179
|
if stdout_tail:
|
|
136
180
|
print("\nstdout tail:")
|
|
@@ -173,6 +217,11 @@ def main() -> int:
|
|
|
173
217
|
directory = root_dir() / ".ai/vcm/jobs" / args.job_id
|
|
174
218
|
status_path = directory / "status.json"
|
|
175
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
|
+
)
|
|
176
225
|
|
|
177
226
|
found_deadline = time.time() + STATUS_WAIT_SECONDS
|
|
178
227
|
while not status_path.is_file():
|
|
@@ -181,6 +230,9 @@ def main() -> int:
|
|
|
181
230
|
return 2
|
|
182
231
|
time.sleep(0.2)
|
|
183
232
|
|
|
233
|
+
renew_lease(lease_path)
|
|
234
|
+
clear_handoff(handoff_path)
|
|
235
|
+
|
|
184
236
|
deadline = time.time() + window
|
|
185
237
|
last_status: dict = {}
|
|
186
238
|
while True:
|
|
@@ -190,11 +242,13 @@ def main() -> int:
|
|
|
190
242
|
last_status = status
|
|
191
243
|
state = status.get("status")
|
|
192
244
|
if state in TERMINAL_EXIT_CODES:
|
|
245
|
+
clear_handoff(handoff_path)
|
|
193
246
|
print_summary(args.job_id, status, directory)
|
|
194
247
|
return TERMINAL_EXIT_CODES[state]
|
|
195
248
|
|
|
196
249
|
if time.time() >= deadline:
|
|
197
|
-
|
|
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)
|
|
198
252
|
return 125
|
|
199
253
|
|
|
200
254
|
time.sleep(interval)
|