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.
Files changed (51) hide show
  1. package/README.md +11 -6
  2. package/dist/backend/adapters/claude-adapter.js +8 -6
  3. package/dist/backend/api/artifact-routes.js +72 -1
  4. package/dist/backend/api/runtime-state-routes.js +17 -5
  5. package/dist/backend/api/task-routes.js +6 -0
  6. package/dist/backend/api/workflow-control-routes.js +56 -0
  7. package/dist/backend/cli/install-vcm-harness.js +42 -47
  8. package/dist/backend/role-tool-policy.js +60 -0
  9. package/dist/backend/server.js +21 -5
  10. package/dist/backend/services/artifact-service.js +309 -5
  11. package/dist/backend/services/auto-memory-service.js +2 -2
  12. package/dist/backend/services/gate-review-service.js +81 -33
  13. package/dist/backend/services/harness-feedback-service.js +11 -4
  14. package/dist/backend/services/harness-service.js +35 -45
  15. package/dist/backend/services/lsp-plugin.js +1 -4
  16. package/dist/backend/services/message-service.js +105 -37
  17. package/dist/backend/services/status-service.js +6 -0
  18. package/dist/backend/services/workflow-control-service.js +854 -0
  19. package/dist/backend/templates/handoff.js +158 -3
  20. package/dist/backend/templates/harness/architect-agent.js +3 -2
  21. package/dist/backend/templates/harness/check-scaffold-ledger.js +105 -39
  22. package/dist/backend/templates/harness/claude-root.js +9 -1
  23. package/dist/backend/templates/harness/coder-agent.js +1 -2
  24. package/dist/backend/templates/harness/coder-worker-agent.js +1 -1
  25. package/dist/backend/templates/harness/gate-review.js +3 -4
  26. package/dist/backend/templates/harness/harness-engineer-agent.js +18 -1
  27. package/dist/backend/templates/harness/project-manager-agent.js +3 -2
  28. package/dist/backend/templates/harness/tester-agent.js +1 -0
  29. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +5 -0
  30. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +3 -4
  31. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -2
  32. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +4 -2
  33. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -2
  34. package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +2 -1
  35. package/dist/backend/templates/harness/vcm-route-message-skill.js +4 -7
  36. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -13
  37. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +62 -0
  38. package/dist/backend/templates/message-envelope.js +4 -4
  39. package/dist/shared/types/workflow.js +7 -1
  40. package/dist/shared/validation/artifact-check.js +99 -95
  41. package/dist/shared/validation/artifact-contract.js +9 -0
  42. package/dist/shared/validation/artifact-registry.js +211 -0
  43. package/dist-frontend/assets/{index-DkM0mnQD.js → index-Bocc2DWF.js} +34 -34
  44. package/dist-frontend/assets/{index-CXSOe-NN.css → index-C_XHGNBD.css} +1 -1
  45. package/dist-frontend/index.html +2 -2
  46. package/package.json +1 -1
  47. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +2 -1
  48. package/scripts/harness-tools/run-long-check +38 -8
  49. package/scripts/harness-tools/vcm-artifact +148 -0
  50. package/scripts/harness-tools/vcm-bash-guard +42 -47
  51. 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 and prevents LSP-enabled roles from replacing
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
- CODE_NAV_HINT = (
25
- "Use LSP, Glob, Read, generated context, architecture documents, or runtime "
26
- "evidence; do not substitute text search."
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
- TEXT_SEARCH_COMMAND = re.compile(
48
- r"(?:^|[;&|()\n`])\s*"
49
- r"(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|()]+)\s+)*"
50
- r"(?:env(?:\s+(?:-[^\s]+|[A-Za-z_][A-Za-z0-9_]*=[^\s]+))*\s+)?"
51
- r"(?:command\s+|sudo(?:\s+-[^\s]+)*\s+)?"
52
- r"(?:[^\s;&|()]*/)?(?:grep|egrep|fgrep|rg|ripgrep)(?=\s|$|[;&|()])"
53
- )
54
- GIT_GREP_COMMAND = re.compile(
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
- WRAPPED_TEXT_SEARCH_COMMAND = re.compile(
58
- r"(?:^|\s)(?:xargs(?:\s+-[^\s]+)*|-exec|-execdir)\s+"
59
- r"(?:[^\s;&|()]*/)?(?:grep|egrep|fgrep|rg|ripgrep)(?=\s|$|[;&|()])"
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, forbid_text_search: bool = False) -> list[str]:
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, forbid_text_search))
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, forbid_text_search))
125
+ reasons.extend(scan_shell_command(substitution, depth + 1))
138
126
  return reasons
139
127
 
140
128
 
141
- def guard_reasons(tool_input: dict, forbid_text_search: bool = False) -> list[str]:
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, forbid_text_search=forbid_text_search))
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
- role = os.environ.get("VCM_ROLE", "").strip()
161
- forbid_text_search = role in LSP_ROLES
162
- if tool_name == "Grep" and forbid_text_search:
163
- reasons = ["the Grep tool is forbidden for this LSP-enabled role"]
164
- elif tool_name == "Bash":
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
- reasons = guard_reasons(tool_input, forbid_text_search=forbid_text_search)
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) + "). " + " ".join(hints)
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; call watch-job again immediately in the same turn. The job ceiling
10
- itself is enforced by the run-long-check worker, not by watch-job.
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
- print_progress(args.job_id, last_status, directory, window)
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)