vibe-coding-master 0.7.42 → 0.7.43
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 +10 -5
- package/dist/backend/api/task-routes.js +31 -2
- package/dist/backend/api/workflow-control-routes.js +0 -31
- package/dist/backend/cli/install-vcm-harness.js +40 -6
- package/dist/backend/role-tool-policy.js +1 -1
- package/dist/backend/server.js +12 -5
- package/dist/backend/services/artifact-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +2 -2
- package/dist/backend/services/claude-hook-service.js +103 -4
- package/dist/backend/services/harness-feedback-service.js +105 -3
- package/dist/backend/services/harness-service.js +38 -6
- package/dist/backend/services/memory-review-paths.js +13 -0
- package/dist/backend/services/role-stall-detector-service.js +322 -0
- package/dist/backend/services/round-service.js +25 -0
- package/dist/backend/services/runtime-coordinator-service.js +10 -0
- package/dist/backend/services/session-service.js +70 -3
- package/dist/backend/services/workflow-control-service.js +439 -203
- package/dist/backend/templates/handoff.js +2 -3
- package/dist/backend/templates/harness/architect-agent.js +2 -2
- package/dist/backend/templates/harness/coder-agent.js +4 -5
- package/dist/backend/templates/harness/gate-review.js +7 -9
- package/dist/backend/templates/harness/harness-engineer-agent.js +18 -8
- package/dist/backend/templates/harness/project-manager-agent.js +5 -5
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +6 -7
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
- package/dist/shared/types/role-stall.js +1 -0
- package/dist/shared/types/workflow.js +14 -0
- package/dist/shared/validation/artifact-registry.js +1 -1
- package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
- package/dist-frontend/assets/{index-Bocc2DWF.js → index-VW9tYPP5.js} +38 -38
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-bash-guard +203 -13
package/dist-frontend/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>VibeCodingMaster</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-VW9tYPP5.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B0d4Z6ny.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ supervised foreground process group.
|
|
|
11
11
|
"""
|
|
12
12
|
import json
|
|
13
13
|
import re
|
|
14
|
+
import shlex
|
|
14
15
|
import sys
|
|
15
16
|
|
|
16
17
|
SKILL_HINT = (
|
|
@@ -32,6 +33,14 @@ PROTECTED_TOOL_INVOCATION = re.compile(
|
|
|
32
33
|
r"(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|()]+)\s+)*"
|
|
33
34
|
r"(?:[^\s;&|()]*/)?(?:run-long-check|watch-job)(?=\s|$)"
|
|
34
35
|
)
|
|
36
|
+
HEREDOC_START = re.compile(
|
|
37
|
+
r"<<(?!<)(-?)\s*(?:(['\"])([^'\"]+)\2|([^\s;&|()<>]+))"
|
|
38
|
+
)
|
|
39
|
+
EXECUTABLE_HEREDOC_RECEIVER = re.compile(
|
|
40
|
+
r"(?:^|[;&|()])\s*"
|
|
41
|
+
r"(?:(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|()]+\s+)*)"
|
|
42
|
+
r"(?:[^\s;&|()]*/)?(?:sh|bash|zsh|dash|ksh|python\d*(?:\.\d+)?|node)(?=\s|$)"
|
|
43
|
+
)
|
|
35
44
|
SHELL_CONTROL_OPERATOR = re.compile(r"(?:\|&?|&&|\|\||;|\n|\(|\)|`)")
|
|
36
45
|
RUN_LONG_CHECK_SHELL_STRING = re.compile(
|
|
37
46
|
r"(?:^|[;&|()\n])\s*"
|
|
@@ -50,11 +59,23 @@ MANAGED_ARTIFACT_PATH = re.compile(
|
|
|
50
59
|
r"harness-feedback[/\\](?:pending|task-retrospectives)[/\\].*\.md"
|
|
51
60
|
r")"
|
|
52
61
|
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
r"\
|
|
62
|
+
SHELL_SEPARATOR_TOKEN = re.compile(r"^[;&|()]+$")
|
|
63
|
+
SHELL_ASSIGNMENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
64
|
+
PYTHON_PATH_MUTATION = re.compile(
|
|
65
|
+
r"\bPath\s*\(\s*(['\"])(?P<path>[^'\"]+)\1\s*\)\s*\.\s*"
|
|
66
|
+
r"(?:write_text|write_bytes|unlink|rmdir|touch|rename|replace)\s*\("
|
|
67
|
+
)
|
|
68
|
+
PYTHON_OPEN = re.compile(
|
|
69
|
+
r"\bopen\s*\(\s*(['\"])(?P<path>[^'\"]+)\1\s*,\s*"
|
|
70
|
+
r"(['\"])(?P<mode>[^'\"]+)\3"
|
|
57
71
|
)
|
|
72
|
+
NODE_PATH_MUTATION = re.compile(
|
|
73
|
+
r"\b(?:writeFile|writeFileSync|appendFile|appendFileSync|unlink|unlinkSync|"
|
|
74
|
+
r"rm|rmSync|rmdir|rmdirSync|createWriteStream)\s*\(\s*"
|
|
75
|
+
r"(['\"])(?P<path>[^'\"]+)\1"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
58
79
|
def strip_quoted(command: str) -> str:
|
|
59
80
|
return QUOTED.sub(" ", command)
|
|
60
81
|
|
|
@@ -85,6 +106,179 @@ def strip_escaped_characters(command: str) -> str:
|
|
|
85
106
|
return "".join(cleaned)
|
|
86
107
|
|
|
87
108
|
|
|
109
|
+
def strip_heredoc_bodies(command: str) -> str:
|
|
110
|
+
"""Remove heredoc data while preserving executable lines and separators."""
|
|
111
|
+
pending: list[tuple[str, bool, bool]] = []
|
|
112
|
+
executable = []
|
|
113
|
+
for line in command.splitlines(keepends=True):
|
|
114
|
+
if pending:
|
|
115
|
+
delimiter, strip_tabs, preserve_body = pending[0]
|
|
116
|
+
candidate = line.rstrip("\r\n")
|
|
117
|
+
if strip_tabs:
|
|
118
|
+
candidate = candidate.lstrip("\t")
|
|
119
|
+
if candidate == delimiter:
|
|
120
|
+
pending.pop(0)
|
|
121
|
+
elif preserve_body:
|
|
122
|
+
executable.append(line)
|
|
123
|
+
elif line.endswith(("\n", "\r")):
|
|
124
|
+
executable.append("\n")
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
executable.append(line)
|
|
128
|
+
preserve_body = bool(EXECUTABLE_HEREDOC_RECEIVER.search(line))
|
|
129
|
+
for match in HEREDOC_START.finditer(line):
|
|
130
|
+
delimiter = match.group(3) or match.group(4)
|
|
131
|
+
pending.append((delimiter, bool(match.group(1)), preserve_body))
|
|
132
|
+
return "".join(executable)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def shell_segments(command: str) -> list[list[str]]:
|
|
136
|
+
normalized = strip_heredoc_bodies(command).replace("\\\n", " ").replace("\n", " ; ")
|
|
137
|
+
try:
|
|
138
|
+
lexer = shlex.shlex(normalized, posix=True, punctuation_chars=";&|()<>")
|
|
139
|
+
lexer.whitespace_split = True
|
|
140
|
+
lexer.commenters = ""
|
|
141
|
+
tokens = list(lexer)
|
|
142
|
+
except ValueError:
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
segments: list[list[str]] = []
|
|
146
|
+
current: list[str] = []
|
|
147
|
+
for token in tokens:
|
|
148
|
+
if SHELL_SEPARATOR_TOKEN.fullmatch(token):
|
|
149
|
+
if current:
|
|
150
|
+
segments.append(current)
|
|
151
|
+
current = []
|
|
152
|
+
continue
|
|
153
|
+
current.append(token)
|
|
154
|
+
if current:
|
|
155
|
+
segments.append(current)
|
|
156
|
+
return segments
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_managed_artifact_path(value: str) -> bool:
|
|
160
|
+
return bool(MANAGED_ARTIFACT_PATH.search(value))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def command_index(segment: list[str]):
|
|
164
|
+
index = 0
|
|
165
|
+
while index < len(segment) and SHELL_ASSIGNMENT.match(segment[index]):
|
|
166
|
+
index += 1
|
|
167
|
+
if index < len(segment) and segment[index].rsplit("/", 1)[-1] == "env":
|
|
168
|
+
index += 1
|
|
169
|
+
while index < len(segment) and (
|
|
170
|
+
segment[index].startswith("-") or SHELL_ASSIGNMENT.match(segment[index])
|
|
171
|
+
):
|
|
172
|
+
index += 1
|
|
173
|
+
return index if index < len(segment) else None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def redirection_details(segment: list[str]) -> tuple[list[str], set[int]]:
|
|
177
|
+
targets: list[str] = []
|
|
178
|
+
consumed: set[int] = set()
|
|
179
|
+
for index, token in enumerate(segment):
|
|
180
|
+
if ">" not in token or not all(character in "<>&|" for character in token):
|
|
181
|
+
continue
|
|
182
|
+
consumed.add(index)
|
|
183
|
+
if index > 0 and segment[index - 1].isdigit():
|
|
184
|
+
consumed.add(index - 1)
|
|
185
|
+
if index + 1 >= len(segment):
|
|
186
|
+
continue
|
|
187
|
+
consumed.add(index + 1)
|
|
188
|
+
target = segment[index + 1]
|
|
189
|
+
if token in (">&", "<&") and (target.isdigit() or target == "-"):
|
|
190
|
+
continue
|
|
191
|
+
targets.append(target)
|
|
192
|
+
return targets, consumed
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def command_operands(segment: list[str], start: int, consumed: set[int]) -> list[str]:
|
|
196
|
+
operands = []
|
|
197
|
+
options_done = False
|
|
198
|
+
for index in range(start, len(segment)):
|
|
199
|
+
if index in consumed:
|
|
200
|
+
continue
|
|
201
|
+
token = segment[index]
|
|
202
|
+
if token == "--":
|
|
203
|
+
options_done = True
|
|
204
|
+
continue
|
|
205
|
+
if not options_done and token.startswith("-"):
|
|
206
|
+
continue
|
|
207
|
+
operands.append(token)
|
|
208
|
+
return operands
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def explicit_interpreter_mutation(command_names: set[str], source: str) -> bool:
|
|
212
|
+
if any(re.fullmatch(r"python\d*(?:\.\d+)?", name) for name in command_names):
|
|
213
|
+
for match in PYTHON_PATH_MUTATION.finditer(source):
|
|
214
|
+
if is_managed_artifact_path(match.group("path")):
|
|
215
|
+
return True
|
|
216
|
+
for match in PYTHON_OPEN.finditer(source):
|
|
217
|
+
if is_managed_artifact_path(match.group("path")) and any(
|
|
218
|
+
flag in match.group("mode") for flag in "wax+"
|
|
219
|
+
):
|
|
220
|
+
return True
|
|
221
|
+
if "node" in command_names:
|
|
222
|
+
return any(
|
|
223
|
+
is_managed_artifact_path(match.group("path"))
|
|
224
|
+
for match in NODE_PATH_MUTATION.finditer(source)
|
|
225
|
+
)
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def mutates_managed_artifact(command: str) -> bool:
|
|
230
|
+
executable_command = strip_heredoc_bodies(command)
|
|
231
|
+
segments = shell_segments(command)
|
|
232
|
+
command_names = {
|
|
233
|
+
segment[index].rsplit("/", 1)[-1]
|
|
234
|
+
for segment in segments
|
|
235
|
+
if (index := command_index(segment)) is not None
|
|
236
|
+
}
|
|
237
|
+
if explicit_interpreter_mutation(command_names, executable_command):
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
for segment in segments:
|
|
241
|
+
redirects, consumed = redirection_details(segment)
|
|
242
|
+
if any(is_managed_artifact_path(target) for target in redirects):
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
executable_index = command_index(segment)
|
|
246
|
+
if executable_index is None:
|
|
247
|
+
continue
|
|
248
|
+
command_name = segment[executable_index].rsplit("/", 1)[-1]
|
|
249
|
+
argument_start = executable_index + 1
|
|
250
|
+
arguments = segment[argument_start:]
|
|
251
|
+
operands = command_operands(segment, argument_start, consumed)
|
|
252
|
+
|
|
253
|
+
target_directory = None
|
|
254
|
+
for index, argument in enumerate(arguments):
|
|
255
|
+
if argument in ("-t", "--target-directory") and index + 1 < len(arguments):
|
|
256
|
+
target_directory = arguments[index + 1]
|
|
257
|
+
elif argument.startswith("--target-directory="):
|
|
258
|
+
target_directory = argument.split("=", 1)[1]
|
|
259
|
+
|
|
260
|
+
if command_name == "cp":
|
|
261
|
+
destination = target_directory or (operands[-1] if len(operands) >= 2 else None)
|
|
262
|
+
if destination and is_managed_artifact_path(destination):
|
|
263
|
+
return True
|
|
264
|
+
elif command_name == "mv":
|
|
265
|
+
if target_directory and is_managed_artifact_path(target_directory):
|
|
266
|
+
return True
|
|
267
|
+
if any(is_managed_artifact_path(operand) for operand in operands):
|
|
268
|
+
return True
|
|
269
|
+
elif command_name in ("rm", "tee"):
|
|
270
|
+
if any(is_managed_artifact_path(operand) for operand in operands):
|
|
271
|
+
return True
|
|
272
|
+
elif command_name in ("sed", "perl"):
|
|
273
|
+
in_place = any(
|
|
274
|
+
argument.startswith("-") and "i" in argument[1:]
|
|
275
|
+
for argument in arguments
|
|
276
|
+
)
|
|
277
|
+
if in_place and any(is_managed_artifact_path(argument) for argument in arguments):
|
|
278
|
+
return True
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
|
|
88
282
|
def unquoted_ampersand(command_without_quotes: str) -> bool:
|
|
89
283
|
cleaned = re.sub(r"\d*>&\d*", " ", command_without_quotes)
|
|
90
284
|
cleaned = re.sub(r"<&\d*", " ", cleaned)
|
|
@@ -98,7 +292,8 @@ def protected_tool_invoked(command_without_quotes: str) -> bool:
|
|
|
98
292
|
|
|
99
293
|
def scan_shell_command(command: str, depth: int = 0) -> list[str]:
|
|
100
294
|
reasons = []
|
|
101
|
-
|
|
295
|
+
executable_command = strip_heredoc_bodies(command)
|
|
296
|
+
stripped = strip_escaped_characters(strip_quoted(executable_command))
|
|
102
297
|
if re.search(r"(?:^|[\s;&|(])(?:nohup|setsid)(?:\s|$)", stripped):
|
|
103
298
|
reasons.append("nohup/setsid detach is forbidden")
|
|
104
299
|
if re.search(r"(?:^|[\s;&|(])disown(?:\s|$)", stripped):
|
|
@@ -113,12 +308,12 @@ def scan_shell_command(command: str, depth: int = 0) -> list[str]:
|
|
|
113
308
|
|
|
114
309
|
# `sh -c '...'` quoted payloads are shell code, not plain strings.
|
|
115
310
|
if depth < MAX_NESTED_SHELL_DEPTH and SHELL_DASH_C.search(stripped):
|
|
116
|
-
for segment in quoted_segments(
|
|
311
|
+
for segment in quoted_segments(executable_command):
|
|
117
312
|
if protected_tool_invoked(strip_escaped_characters(strip_quoted(segment))):
|
|
118
313
|
reasons.append("run-long-check and watch-job must not be invoked through a shell command string")
|
|
119
314
|
reasons.extend(scan_shell_command(segment, depth + 1))
|
|
120
315
|
if depth < MAX_NESTED_SHELL_DEPTH:
|
|
121
|
-
for substitution in double_quoted_command_substitutions(
|
|
316
|
+
for substitution in double_quoted_command_substitutions(executable_command):
|
|
122
317
|
nested_stripped = strip_escaped_characters(strip_quoted(substitution))
|
|
123
318
|
if protected_tool_invoked(nested_stripped):
|
|
124
319
|
reasons.append("run-long-check and watch-job must not be invoked through command substitution")
|
|
@@ -135,12 +330,7 @@ def guard_reasons(tool_input: dict) -> list[str]:
|
|
|
135
330
|
command = command if isinstance(command, str) else ""
|
|
136
331
|
|
|
137
332
|
reasons.extend(scan_shell_command(command))
|
|
138
|
-
|
|
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
|
-
):
|
|
333
|
+
if mutates_managed_artifact(command):
|
|
144
334
|
reasons.append("direct Bash writes to VCM-managed Markdown are forbidden")
|
|
145
335
|
return list(dict.fromkeys(reasons))
|
|
146
336
|
|