@caddis/cli 0.4.55 → 0.4.57

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 (58) hide show
  1. package/bundles/antigravity-plugin/caddis_statusline.py +7 -3
  2. package/bundles/antigravity-plugin/guard_agy.py +1056 -83
  3. package/bundles/antigravity-plugin/hooks.json +2 -1
  4. package/bundles/antigravity-plugin/plugin.json +1 -1
  5. package/bundles/antigravity-plugin/repo_freshness.py +182 -4
  6. package/bundles/antigravity-plugin/scripts/caddis_adhoc.py +20 -12
  7. package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +63 -12
  8. package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +14 -5
  9. package/bundles/antigravity-plugin/scripts/caddis_gate.py +27 -1
  10. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +93 -6
  11. package/bundles/antigravity-plugin/scripts/caddis_merge_chain.py +67 -22
  12. package/bundles/antigravity-plugin/scripts/caddis_relay_git.py +8 -2
  13. package/bundles/antigravity-plugin/scripts/caddis_review.py +290 -176
  14. package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +346 -50
  15. package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +82 -8
  16. package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +89 -24
  17. package/bundles/antigravity-plugin/scripts/caddis_statusline.py +7 -3
  18. package/bundles/antigravity-plugin/scripts/caddis_tidy.py +10 -6
  19. package/bundles/antigravity-plugin/scripts/caddis_todo.py +7 -1
  20. package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +12 -5
  21. package/bundles/antigravity-plugin/scripts/caddis_worktree_safety.py +182 -0
  22. package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +2 -0
  23. package/bundles/antigravity-plugin/scripts/claudster_doctor.py +6 -2
  24. package/bundles/antigravity-plugin/scripts/no_window.py +49 -0
  25. package/bundles/antigravity-plugin/scripts/oss_review.py +303 -79
  26. package/bundles/antigravity-plugin/scripts/secret_filter.py +190 -22
  27. package/bundles/antigravity-plugin/scripts/setup_project_ai.py +6 -1
  28. package/bundles/antigravity-plugin/session_end_agy.py +27 -11
  29. package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +55 -9
  30. package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +11 -8
  31. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +1 -1
  32. package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +10 -6
  33. package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +2 -3
  34. package/bundles/antigravity-plugin/skills/implement/SKILL.md +31 -17
  35. package/bundles/antigravity-plugin/skills/mmr/SKILL.md +1 -1
  36. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +45 -5
  37. package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +4 -2
  38. package/bundles/antigravity-plugin/skills/review-pr/SKILL.md +13 -4
  39. package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +28 -6
  40. package/bundles/antigravity-plugin/skills/roadmap/assets/review-protocol.md +16 -13
  41. package/bundles/antigravity-plugin/skills/ship/SKILL.md +32 -10
  42. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +32 -1
  43. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +44 -6
  44. package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +1 -1
  45. package/bundles/antigravity-plugin/warm_start_agy.py +40 -16
  46. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  47. package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +5 -5
  48. package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +1 -1
  49. package/bundles/antigravity-plugin-extras/skills/deploy-local/SKILL.md +2 -1
  50. package/bundles/antigravity-plugin-extras/skills/update-readme/SKILL.md +1 -1
  51. package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +4 -2
  52. package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +10 -6
  53. package/bundles/codex/.codex/skills/devops/git-commit/SKILL.md +2 -3
  54. package/bundles/codex/.codex/skills/docs/writing-plans/SKILL.md +1 -1
  55. package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +28 -6
  56. package/bundles/codex/.codex/skills/workflow/roadmap/assets/review-protocol.md +16 -13
  57. package/bundles/manifest.json +3 -3
  58. package/package.json +1 -1
@@ -33,9 +33,11 @@ except ModuleNotFoundError as exc:
33
33
  import caddis_exit
34
34
  from caddis_heavy_slot import HeavySlotBusy, heavy_slot
35
35
 
36
+ import caddis_checkout_guard
36
37
  import caddis_routing
37
38
  import caddis_redact
38
39
  import caddis_spend
40
+ import caddis_worktree_safety
39
41
 
40
42
  if hasattr(sys.stdout, "reconfigure"):
41
43
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
@@ -265,7 +267,14 @@ def remove_phase_worktree(
265
267
  if delete_branch and not wt.branch.startswith("lane/"):
266
268
  raise LaneConfigError(f"refusing to delete non-lane branch: {wt.branch}")
267
269
 
268
- _git_output(repo_root, "worktree", "remove", "--force", str(resolved))
270
+ try:
271
+ problem = caddis_worktree_safety.safe_remove_worktree(repo_root, resolved)
272
+ except OSError as exc:
273
+ raise LaneConfigError(f"worktree remove failed: {exc}") from exc
274
+ if not problem and os.path.lexists(resolved):
275
+ problem = f"worktree remove left {resolved} on disk"
276
+ if problem:
277
+ raise LaneConfigError(problem)
269
278
  _git_output(repo_root, "worktree", "prune")
270
279
  if delete_branch:
271
280
  _git_output(repo_root, "branch", "-D", "--", wt.branch)
@@ -1244,8 +1253,19 @@ def validate_codex_argv(argv: list[str]) -> None:
1244
1253
  )
1245
1254
 
1246
1255
 
1256
+ # Robustness #4: a lane agent can be prompt-injected through a plan or a diff, so it gets no publish token and no
1257
+ # credential prompt, and cannot switch the guard off. The same list as caddis_roadmap_run.SESSION_SECRET_VARS. The
1258
+ # gh keyring login stays on the machine; the guard denies push, merge and publish when CADDIS_HEADLESS=1.
1259
+ LANE_SECRET_VARS = ("GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "NPM_TOKEN", "NODE_AUTH_TOKEN",
1260
+ "CADDIS_ALLOW_PUBLISH", "CADDIS_GUARD_DISABLED")
1261
+ LANE_NO_PROMPT_ENV = {"GH_PROMPT_DISABLED": "1", "GIT_TERMINAL_PROMPT": "0"}
1262
+
1263
+
1247
1264
  def child_env(base: dict, lane: str, model: str) -> dict:
1248
1265
  env = base.copy()
1266
+ for name in LANE_SECRET_VARS:
1267
+ env.pop(name, None)
1268
+ env.update(LANE_NO_PROMPT_ENV)
1249
1269
  env["CADDIS_HEADLESS"] = "1"
1250
1270
  if lane == "glm":
1251
1271
  env["OSS_MODEL"] = model
@@ -2132,6 +2152,42 @@ def _check_command(
2132
2152
  return exit_code
2133
2153
 
2134
2154
 
2155
+ _TERMINAL_ESCAPES = re.compile(
2156
+ r"\x1b\[[0-?]*[ -/]*[@-~]" # CSI (colours, cursor)
2157
+ r"|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?" # OSC (titles, links)
2158
+ r"|\x1b[@-Z\\-_]" # other two-byte escapes
2159
+ )
2160
+ _CONTROL_CHARS = re.compile(r"[\x00-\x09\x0b-\x1f\x7f]")
2161
+
2162
+
2163
+ def _printable(value: object) -> str:
2164
+ """Text safe to print: no ANSI escapes and no C0 control characters except newline."""
2165
+ text = _TERMINAL_ESCAPES.sub("", str(value if value is not None else ""))
2166
+ return _CONTROL_CHARS.sub("", text)
2167
+
2168
+
2169
+ # Accept fast-forwards the SESSION checkout. In a checkout a dev service serves from, or one that holds a peer's
2170
+ # uncommitted edits, that moves files under them (robustness #11). The lane's own scaffolding is not a peer's change.
2171
+ _LANE_OWN_PREFIX = ".caddis/orchestrator/"
2172
+
2173
+
2174
+ def shared_checkout_reason(repo: Path) -> str:
2175
+ """Why accept must not fast-forward `repo`, or ''. Shared (AGENTS.md) and/or foreign changes (git status):
2176
+ every reason that holds, joined with "; ", so an --allow-shared audit records all of them."""
2177
+ reasons = []
2178
+ if caddis_checkout_guard.checkout_is_shared(repo):
2179
+ reasons.append("AGENTS.md says Shared checkout: yes")
2180
+ try:
2181
+ dirty = caddis_checkout_guard.foreign_changes(repo)
2182
+ except caddis_checkout_guard.CheckoutStatusError:
2183
+ reasons.append("git status failed, so changes by other sessions cannot be ruled out")
2184
+ dirty = []
2185
+ foreign = [path for path in dirty if not path.replace("\\", "/").startswith(_LANE_OWN_PREFIX)]
2186
+ if foreign:
2187
+ reasons.append("changes not made by this lane: " + ", ".join(foreign[:3]))
2188
+ return "; ".join(reasons)
2189
+
2190
+
2135
2191
  def _accept_command(
2136
2192
  args,
2137
2193
  repo: Path,
@@ -2151,12 +2207,26 @@ def _accept_command(
2151
2207
  if packet.get("next") != "review":
2152
2208
  print(f"refusing: packet next={packet.get('next')}")
2153
2209
  return EXIT_USAGE
2154
- review_status = packet.get("review", {}).get("status")
2155
- if review_status == "BLOCKING" and not getattr(args, "override_review", None):
2156
- print("refusing: review BLOCKING; pass --override-review with a reason")
2210
+ # Fail closed: only an exact CLEAN verdict merges without an owner override.
2211
+ review = packet.get("review") or {}
2212
+ if not isinstance(review, dict):
2213
+ review = {}
2214
+ review_status = review.get("status")
2215
+ override_reason = (getattr(args, "override_review", None) or "").strip()
2216
+ if review_status != "CLEAN" and not override_reason:
2217
+ if review_status == "BLOCKING":
2218
+ print("refusing: review BLOCKING; pass --override-review with a reason")
2219
+ else:
2220
+ # MISSING = no verdict (provider down, 429, secret-filter refusal). Anything else
2221
+ # that is not CLEAN (absent, null, unknown, lowercase) is also no valid verdict.
2222
+ cause = " ".join(_printable(review.get("tail")).split())[-300:] or "no reason recorded"
2223
+ label = (
2224
+ "MISSING" if review_status == "MISSING"
2225
+ else f"status {_printable(review_status)!r}"
2226
+ )
2227
+ print(f"refusing: review {label} (no verdict: {cause}); re-run the review, "
2228
+ "or pass --override-review with a reason")
2157
2229
  return EXIT_USAGE
2158
- if review_status == "MISSING":
2159
- print("WARNING: review MISSING")
2160
2230
  if _git_output(repo, "rev-parse", "HEAD") != packet.get("base_sha"):
2161
2231
  print("refusing: programme branch moved since the attempt started")
2162
2232
  return EXIT_FAIL
@@ -2174,10 +2244,25 @@ def _accept_command(
2174
2244
  or packet["worktree_digest"] != worktree_digest(wt.path, files)):
2175
2245
  print("refusing: worktree changed since check")
2176
2246
  return EXIT_FAIL
2247
+ unsafe = shared_checkout_reason(repo)
2248
+ if unsafe and not getattr(args, "allow_shared", False):
2249
+ print(f"refusing: the session checkout is not safe to fast-forward ({_printable(unsafe)}). "
2250
+ "Run the programme from a worktree of origin/<default> under %TEMP% (git worktree add), "
2251
+ "or pass --allow-shared to fast-forward here anyway")
2252
+ return EXIT_USAGE
2253
+ if unsafe:
2254
+ packet["allow_shared"] = unsafe
2255
+ write_packet(packet_path, packet)
2256
+ print(f"ALLOW SHARED: {_printable(unsafe)}")
2177
2257
  if args.override_review is not None:
2178
2258
  packet["override_review"] = args.override_review
2179
2259
  write_packet(packet_path, packet)
2180
2260
  print(f"OVERRIDE REVIEW: {args.override_review}")
2261
+ if review_status != "CLEAN":
2262
+ print(
2263
+ f"WARNING: merged without a vendor review (status {_printable(review_status)}): "
2264
+ f"{args.override_review}"
2265
+ )
2181
2266
  _git_output(wt.path, "add", "--", *(str(path) for path in files))
2182
2267
  _git_output(wt.path, "commit", "-m", args.message)
2183
2268
  accepted_sha = _git_output(wt.path, "rev-parse", "HEAD")
@@ -2406,6 +2491,8 @@ def main(argv: list[str] | None = None) -> int:
2406
2491
  _add_phase_arguments(accept)
2407
2492
  accept.add_argument("--message", required=True)
2408
2493
  accept.add_argument("--override-review")
2494
+ accept.add_argument("--allow-shared", action="store_true",
2495
+ help="fast-forward a shared checkout or one with foreign changes anyway (recorded)")
2409
2496
  abandon = subparsers.add_parser("abandon")
2410
2497
  _add_phase_arguments(abandon)
2411
2498
  finish = subparsers.add_parser("finish")
@@ -1,14 +1,16 @@
1
1
  """Merge pull requests one at a time, from green CI to a verified deploy, and stop the line at the first problem.
2
2
 
3
3
  For each PR (design section 11): when BEHIND, update the branch and cancel the old head's runs (force-cancel after
4
- 2 minutes); a cancelled run on the head is re-run, never merged on; "no checks reported" waits; a failed check stops;
5
- no merge while a push run is in progress on the default branch; squash merge with --match-head-commit; then the merge
6
- SHA must get a push run, and `caddis_prod_check.py` must pass before the next PR starts. A STOP file is checked before
7
- each update-branch and each merge. --dry-run makes read-only `gh` calls and prints what it would do.
4
+ 2 minutes); a cancelled run on the head is re-run, never merged on; "checks green" is ci-watch's verdict on the
5
+ `statusCheckRollup` of the PR's `headRefOid` (never the `gh pr checks` table, which can show an older head); "no
6
+ checks reported" waits, then stops; every check skipped stops (no job ran), both unless --allow-no-checks; a failed
7
+ check stops; no merge while a push run is in progress on the default branch; squash merge with --match-head-commit;
8
+ then the merge SHA must get a push run, and `caddis_prod_check.py` must pass before the next PR starts. A STOP file is
9
+ checked before each update-branch and each merge. --dry-run makes read-only `gh` calls and prints what it would do.
8
10
 
9
11
  Exit 0 when every PR merged and verified; 1 at the first stop; 3 when the profile cannot be read.
10
12
 
11
- Usage: caddis_merge_chain.py [--repo PATH] [--gh-repo OWNER/NAME] [--docs-only] [--dry-run]
13
+ Usage: caddis_merge_chain.py [--repo PATH] [--gh-repo OWNER/NAME] [--docs-only] [--dry-run] [--allow-no-checks]
12
14
  [--stop-file PATH ...] <pr> [<pr> ...]
13
15
  """
14
16
 
@@ -21,13 +23,15 @@ from pathlib import Path
21
23
 
22
24
  try:
23
25
  import caddis_exit
26
+ import caddis_lanes
24
27
  import caddis_prod_check as prod
25
28
  except ModuleNotFoundError as exc:
26
- if exc.name not in {"caddis_exit", "caddis_prod_check"}:
29
+ if exc.name not in {"caddis_exit", "caddis_lanes", "caddis_prod_check", "caddis_heavy_slot", "caddis_routing"}:
27
30
  raise
28
31
  sys.path.insert(0, str(Path(__file__).resolve().parent))
29
32
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "claude-harness" / "scripts"))
30
33
  import caddis_exit
34
+ import caddis_lanes
31
35
  import caddis_prod_check as prod
32
36
 
33
37
  MAX_ATTEMPTS = 8
@@ -45,6 +49,7 @@ class Stop(Exception):
45
49
  class ChainContext(prod.Context):
46
50
  stop_files: list[Path] = field(default_factory=list)
47
51
  docs_only: bool = False
52
+ allow_no_checks: bool = False
48
53
 
49
54
 
50
55
  def _stop_check(ctx: ChainContext, before: str) -> None:
@@ -90,26 +95,63 @@ def _head_run(ctx: ChainContext, branch: str, head: str) -> dict | None:
90
95
  return next((r for r in runs if r.get("headSha") == head), None) # type: ignore[union-attr]
91
96
 
92
97
 
93
- def _wait_checks(ctx: ChainContext, pr: int) -> str:
94
- """Return 'pass', 'fail' or 'cancel'. Raise Stop when no check reports in time."""
98
+ def _rollup_state(rollup: object, allow_no_checks: bool) -> tuple[str, str]:
99
+ """(state, detail) from the head's `statusCheckRollup`, by ci-watch's own verdict (`caddis_lanes._rollup_verdict`).
100
+
101
+ state is 'pass', 'fail', 'cancel', 'pending', 'none' (no check reported) or 'skipped' (every check skipped).
102
+ A check that only SKIPPED or NEUTRAL ran no job, so a rollup with no SUCCESS proves nothing: 'skipped', unless
103
+ the owner passed --allow-no-checks for a repo that has no CI at all.
104
+ """
105
+ verdict = caddis_lanes._rollup_verdict(rollup)
106
+ if verdict is None:
107
+ return ("pass", "no checks reported (--allow-no-checks)") if allow_no_checks else ("none", "")
108
+ _green, failed, unfinished = verdict
109
+ if failed:
110
+ cancelled = {str(i.get("name") or i.get("context") or "?") for i in rollup # type: ignore[union-attr]
111
+ if isinstance(i, dict) and str(i.get("conclusion") or "").upper() == "CANCELLED"}
112
+ return ("cancel" if set(failed) <= cancelled else "fail"), ", ".join(failed)
113
+ if unfinished:
114
+ return "pending", ", ".join(unfinished)
115
+ ran = sum(1 for i in rollup if isinstance(i, dict) # type: ignore[union-attr]
116
+ and str(i.get("conclusion") or i.get("state") or "").upper() == "SUCCESS")
117
+ if not ran:
118
+ return ("pass", "every check skipped (--allow-no-checks)") if allow_no_checks else ("skipped", "")
119
+ return "pass", ""
120
+
121
+
122
+ def _wait_checks(ctx: ChainContext, pr: int, head: str) -> str:
123
+ """Return 'pass', 'fail', 'cancel' or 'moved' for the commit `head`. Raise Stop when no proof comes in time.
124
+
125
+ The verdict is ci-watch's: the `statusCheckRollup` of the PR's `headRefOid`, read in one `gh pr view`. The
126
+ `gh pr checks` table is never read: it can still show an older head's checks.
127
+ """
95
128
  deadline = ctx.clock.now() + CHECKS_WAIT_MINUTES * 60
96
129
  while True:
97
- proc = ctx.gh("pr", "checks", str(pr), "--json", "name,bucket")
98
- if proc.rc != 0 and not proc.out.strip() and "no checks reported" not in proc.err + proc.out:
130
+ proc = ctx.gh("pr", "view", "--json", "headRefOid,statusCheckRollup", str(pr))
131
+ if proc.rc != 0:
99
132
  # Auth or network failure: say so now instead of waiting 30 minutes as if no check had started.
100
- raise Stop(f"#{pr}: gh pr checks failed: {(proc.err or proc.out).strip()[:300]}")
101
- buckets: list[str] = []
133
+ raise Stop(f"#{pr}: gh pr view failed: {(proc.err or proc.out).strip()[:300]}")
102
134
  try:
103
- buckets = [c.get("bucket", "") for c in prod.json.loads(proc.out)] if proc.out.strip() else []
135
+ data = prod.json.loads(proc.out) if proc.out.strip() else {}
104
136
  except ValueError:
105
- buckets = []
106
- if "fail" in buckets:
107
- return "fail"
108
- if "cancel" in buckets:
109
- return "cancel"
110
- if buckets and "pending" not in buckets:
137
+ data = {}
138
+ if not isinstance(data, dict):
139
+ data = {}
140
+ if data.get("headRefOid") != head:
141
+ ctx.out(f" #{pr}: the head is now {str(data.get('headRefOid'))[:12]}, not {head[:12]}; reading again")
142
+ return "moved"
143
+ state, detail = _rollup_state(data.get("statusCheckRollup"), ctx.allow_no_checks)
144
+ if state == "pass":
145
+ if detail:
146
+ ctx.out(f" #{pr}: WARNING: {detail} on head {head[:12]}")
111
147
  return "pass"
112
- waiting = "no checks reported yet" if not buckets else "checks pending"
148
+ if state in ("fail", "cancel"):
149
+ return state
150
+ if state == "skipped":
151
+ raise Stop(f"#{pr}: every check on head {head[:12]} was skipped: no job ran, so this is not a pass. "
152
+ "If this repo has no CI at all, run again with --allow-no-checks")
153
+ waiting = (f"checks pending ({detail})" if state == "pending" else
154
+ "no checks reported yet (if this repo has no CI at all, run again with --allow-no-checks)")
113
155
  if ctx.clock.now() >= deadline:
114
156
  raise Stop(f"#{pr}: {waiting} after {CHECKS_WAIT_MINUTES} min")
115
157
  if ctx.dry_run:
@@ -159,10 +201,10 @@ def merge_one(ctx: ChainContext, pr: int) -> str | None:
159
201
  ctx.out(f" WARNING: re-run of {run['databaseId']} refused: {(rerun.err or rerun.out).strip()[:200]}")
160
202
  ctx.clock.sleep(60)
161
203
  continue
162
- result = _wait_checks(ctx, pr)
204
+ result = _wait_checks(ctx, pr, head)
163
205
  if result == "fail":
164
206
  raise Stop(f"#{pr}: CI failed on head {head[:12]}")
165
- if result == "cancel":
207
+ if result in ("cancel", "moved"):
166
208
  continue
167
209
  if ctx.dry_run:
168
210
  ctx.out(f"#{pr} ({merge_state}): checks green; would wait for idle push runs, then squash-merge {head[:12]}")
@@ -219,6 +261,8 @@ def main(argv: list[str] | None = None) -> int:
219
261
  parser.add_argument("--profile", type=Path, help="default: <repo>/.caddis/prod-check.toml")
220
262
  parser.add_argument("--docs-only", action="store_true", help="the PRs change docs only: expect no deploy")
221
263
  parser.add_argument("--dry-run", action="store_true", help="read-only gh calls; print the steps")
264
+ parser.add_argument("--allow-no-checks", action="store_true",
265
+ help="the repo has no CI: accept a head with no check, or with every check skipped")
222
266
  parser.add_argument("--stop-file", type=Path, action="append", default=[],
223
267
  help="extra STOP file (~/.caddis/STOP is always checked)")
224
268
  args = parser.parse_args(argv)
@@ -229,6 +273,7 @@ def main(argv: list[str] | None = None) -> int:
229
273
  return caddis_exit.NOT_RUN
230
274
  ctx = ChainContext(run=prod.real_runner(args.repo), clock=prod.Clock(), profile=profile, repo=args.repo,
231
275
  gh_repo=args.gh_repo, dry_run=args.dry_run, docs_only=args.docs_only,
276
+ allow_no_checks=args.allow_no_checks,
232
277
  stop_files=[Path.home() / ".caddis" / "STOP", *args.stop_file])
233
278
  return merge_chain(ctx, args.prs)
234
279
 
@@ -20,13 +20,18 @@ except ModuleNotFoundError as exc:
20
20
  import secret_filter
21
21
 
22
22
 
23
+ # No child process opens a console window on Windows (this runs under hooks and status lines that
24
+ # have no console). Inline, not scripts/no_window.py: in the source tree this file does not sit beside it.
25
+ _NO_WINDOW = 0x08000000 if os.name == "nt" else 0
26
+
23
27
  MAX_NOTE_CHARS = 4000
24
28
 
25
29
 
26
30
  def _git(repo: Path, *args: str, input_data: bytes | None = None,
27
31
  env: dict[str, str] | None = None) -> bytes:
28
32
  result = subprocess.run(["git", *args], cwd=repo, env=env, input=input_data,
29
- stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
33
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False,
34
+ creationflags=_NO_WINDOW)
30
35
  if result.returncode:
31
36
  # Name the subcommand, not a leading "-c key=value" option.
32
37
  command = next((arg for i, arg in enumerate(args) if not arg.startswith("-") and (i == 0 or args[i - 1] != "-c")), "?")
@@ -53,7 +58,8 @@ def _pre_send(note: str) -> None:
53
58
  "--- /dev/null\n+++ b/relay.md\n"
54
59
  f"@@ -0,0 +1,{len(lines)} @@\n" +
55
60
  "".join("+" + line + "\n" for line in lines))
56
- result = secret_filter.filter_diff(diff, list(secret_filter.DEFAULT_DENY_GLOBS), [])
61
+ result = secret_filter.filter_diff(diff, list(secret_filter.DEFAULT_DENY_GLOBS), [],
62
+ env_assignment_everywhere=True)
57
63
  if result.hits or result.dropped_paths or not result.kept_diff:
58
64
  raise ValueError("relay refused by pre-send filter")
59
65