@joshuafolkken/kit 1.1010.0 → 1.1035.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.
Files changed (272) hide show
  1. package/.claude/settings.json +1 -5
  2. package/.claude/skills/dependency-update/SKILL.md +6 -3
  3. package/.claude/skills/epic-commands/SKILL.md +46 -17
  4. package/.claude/skills/workflow-commands/SKILL.md +8 -23
  5. package/.claude/skills/workflow-commands/backlogrun-child.md +4 -2
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -12
  7. package/.claude/skills/workflow-commands/fullrun-steps.md +4 -3
  8. package/.claude/skills/workflow-commands/fullrun.md +1 -1
  9. package/.claude/skills/workflow-commands/halfrun.md +2 -1
  10. package/.claude/skills/workflow-commands/into-target.md +1 -1
  11. package/.claude/skills/workflow-commands/observation-filing.md +42 -43
  12. package/.claude/skills/workflow-commands/prerequisite.md +1 -1
  13. package/.codex/hooks.json +1 -5
  14. package/.github/workflows/sonar-qube.yml +1 -1
  15. package/AGENTS.md +4 -25
  16. package/CLAUDE.md +24 -38
  17. package/GEMINI.md +4 -24
  18. package/README.md +22 -56
  19. package/dist/CLAUDE.md +24 -38
  20. package/dist/hooks/batch-guard.js +4 -4
  21. package/dist/hooks/{chunk-WMIR5SNX.js → chunk-D67KVGGY.js} +3 -3
  22. package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-EIFCAJRB.js} +70 -27
  23. package/dist/hooks/{chunk-KPZN42RT.js → chunk-GJT4BNVE.js} +3 -3
  24. package/dist/hooks/{chunk-PAXMLGSS.js → chunk-GLBDBJOK.js} +44 -24
  25. package/dist/hooks/{chunk-GBJUERYK.js → chunk-HOYJU5RM.js} +1 -1
  26. package/dist/hooks/{chunk-6PSTVOLH.js → chunk-HU66PWWL.js} +33 -57
  27. package/dist/hooks/{chunk-A2H5I2VD.js → chunk-KG6PZD72.js} +2 -2
  28. package/dist/hooks/{chunk-AWTFKKFW.js → chunk-XPLP27WA.js} +3 -8
  29. package/dist/hooks/{chunk-4GDR2YTV.js → chunk-Y27M6A7X.js} +4 -4
  30. package/dist/hooks/{chunk-56LGMJJV.js → chunk-ZXNE6NY3.js} +2 -2
  31. package/dist/hooks/codex-hook-adapter.js +10 -10
  32. package/dist/hooks/duplicate-read-guard.js +5 -5
  33. package/dist/hooks/format-edited.js +2 -2
  34. package/dist/hooks/investigation-guard.js +5 -5
  35. package/dist/hooks/pretool-guard.js +9 -9
  36. package/dist/hooks/stop-guard.js +11 -15
  37. package/dist/josh.js +38 -19
  38. package/dist/version/index.js +93 -41
  39. package/eslint/base.js +2 -11
  40. package/package.json +31 -5
  41. package/prompts/coding-standards.md +15 -290
  42. package/prompts/collaboration-workflow/cross-repo-epic.md +2 -51
  43. package/prompts/collaboration-workflow/epic-audit.md +2 -53
  44. package/prompts/collaboration-workflow/epic-bundle.md +2 -106
  45. package/prompts/collaboration-workflow/issue-template.md +4 -4
  46. package/prompts/collaboration-workflow/operating-rules.md +18 -75
  47. package/prompts/collaboration-workflow/principles.md +74 -0
  48. package/prompts/collaboration-workflow/residency.md +54 -14
  49. package/prompts/collaboration-workflow/rule-delivery.md +1 -11
  50. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  51. package/prompts/collaboration-workflow.md +22 -28
  52. package/scripts/adopt/adopt-logic.ts +2 -2
  53. package/scripts/adopt/adopt-toolkits.ts +1 -1
  54. package/scripts/adopt/adopt.ts +2 -2
  55. package/scripts/agent/agent-event.ts +2 -9
  56. package/scripts/agent/agent-exit-record.ts +2 -9
  57. package/scripts/backlog/backlog-budget-cli.ts +2 -6
  58. package/scripts/backlog/backlog-drive-cli.ts +20 -10
  59. package/scripts/backlog/backlog-drive.ts +50 -16
  60. package/scripts/backlog/backlog-offer-cli.ts +6 -5
  61. package/scripts/backlog/backlog-plan.ts +1 -1
  62. package/scripts/build/build-bin.ts +2 -1
  63. package/scripts/build/build-config-merge.ts +2 -1
  64. package/scripts/build/build-hooks.ts +24 -4
  65. package/scripts/build/build-managed-marker.ts +2 -1
  66. package/scripts/build/build-self-sync-guard.ts +2 -1
  67. package/scripts/build/build-version.ts +2 -1
  68. package/scripts/cases/cases-logic.ts +2 -6
  69. package/scripts/ci/pr-classification.ts +13 -11
  70. package/scripts/cost-runtime/cost-transcript.ts +4 -11
  71. package/scripts/doctor/doctor-consumer.ts +5 -12
  72. package/scripts/doctor/doctor-ports.ts +2 -2
  73. package/scripts/document/document-byte-budget.ts +8 -16
  74. package/scripts/document/document-section.ts +2 -6
  75. package/scripts/document/entry-read-budget.ts +1 -1
  76. package/scripts/{git/git-epic-add-body.ts → epic/epic-add-body.ts} +23 -23
  77. package/scripts/{git/git-epic-add-plan.ts → epic/epic-add-plan.ts} +23 -26
  78. package/scripts/{git/git-epic-add.ts → epic/epic-add.ts} +17 -17
  79. package/scripts/epic/epic-audit-cli.ts +4 -4
  80. package/scripts/epic/epic-audit-orphans.ts +2 -2
  81. package/scripts/epic/epic-audit-rationale.ts +1 -1
  82. package/scripts/epic/epic-audit.ts +1 -1
  83. package/scripts/{git/git-epic-body.ts → epic/epic-body.ts} +6 -6
  84. package/scripts/epic/epic-bundle-cli.ts +1 -1
  85. package/scripts/epic/epic-bundle.ts +2 -2
  86. package/scripts/epic/epic-candidate-confirm.ts +2 -1
  87. package/scripts/{git/git-epic-chains.ts → epic/epic-chains.ts} +4 -4
  88. package/{scripts-ai → scripts/epic}/epic-check.ts +5 -4
  89. package/{scripts-ai → scripts/epic}/epic-cli.ts +4 -4
  90. package/scripts/{git/git-epic-close-comment.ts → epic/epic-close-comment.ts} +9 -9
  91. package/scripts/{git/git-epic-close.ts → epic/epic-close.ts} +23 -19
  92. package/scripts/epic/epic-cross-repo.ts +21 -38
  93. package/scripts/{git/git-epic-decision.ts → epic/epic-decision.ts} +12 -14
  94. package/scripts/epic/epic-fetch.ts +3 -3
  95. package/scripts/epic/epic-graph.ts +3 -3
  96. package/scripts/epic/epic-index.ts +2 -2
  97. package/scripts/epic/epic-issue.ts +1 -1
  98. package/scripts/epic/epic-next.ts +3 -3
  99. package/scripts/{git/git-epic-parse.ts → epic/epic-parse.ts} +3 -3
  100. package/scripts/{git/git-epic-promote.ts → epic/epic-promote.ts} +10 -10
  101. package/scripts/{git/git-epic-read.ts → epic/epic-read.ts} +9 -9
  102. package/scripts/{git/git-epic-reconcile.ts → epic/epic-reconcile.ts} +18 -20
  103. package/scripts/{git/git-epic-reference.ts → epic/epic-reference.ts} +4 -23
  104. package/scripts/epic/epic-relation-recheck.ts +4 -3
  105. package/scripts/{git/git-epic-relations.ts → epic/epic-relations.ts} +5 -5
  106. package/scripts/{git/git-epic-remove-plan.ts → epic/epic-remove-plan.ts} +17 -19
  107. package/scripts/{git/git-epic-remove.ts → epic/epic-remove.ts} +13 -13
  108. package/scripts/{git/git-epic-run.ts → epic/epic-run.ts} +27 -27
  109. package/scripts/{git/git-epic-sections.ts → epic/epic-sections.ts} +6 -6
  110. package/scripts/{git/git-epic-shape.ts → epic/epic-shape.ts} +7 -7
  111. package/scripts/{git/git-epic-validate.ts → epic/epic-validate.ts} +13 -13
  112. package/{scripts-ai → scripts/epic}/epic.ts +17 -16
  113. package/scripts/gh/fix-gh-packages.ts +2 -1
  114. package/scripts/gh/gh-spawn.ts +12 -10
  115. package/scripts/git/constants.ts +1 -1
  116. package/scripts/git/git-branch.ts +10 -1
  117. package/scripts/git/git-command.ts +1 -1
  118. package/scripts/git/git-error.ts +2 -5
  119. package/{scripts-ai → scripts/git}/git-followup-finish.ts +7 -7
  120. package/scripts/git/git-followup-flush.ts +74 -48
  121. package/scripts/git/git-followup-issue-close.ts +2 -2
  122. package/scripts/git/git-followup-label.ts +1 -1
  123. package/scripts/git/git-followup-pending.ts +1 -1
  124. package/{scripts-ai → scripts/git}/git-followup-workflow.ts +23 -11
  125. package/scripts/git/git-gh-exec.ts +85 -26
  126. package/scripts/git/git-gh-issue-read.ts +2 -2
  127. package/scripts/git/git-gh-issue-rest.ts +2 -2
  128. package/scripts/git/git-gh-issue-write.ts +6 -6
  129. package/scripts/git/git-gh-pr-read.ts +0 -21
  130. package/scripts/git/git-gh-pr.ts +2 -23
  131. package/scripts/git/git-pr-followup-wrapup.ts +2 -6
  132. package/scripts/git/git-preflight.ts +3 -6
  133. package/scripts/git/git-staging.ts +21 -9
  134. package/{scripts-ai → scripts/git}/git-workflow.ts +11 -11
  135. package/scripts/git/github-issue-url.ts +1 -1
  136. package/scripts/git/issue-labels.ts +1 -1
  137. package/scripts/git/issue-reference.ts +19 -0
  138. package/scripts/git/main-merge.ts +2 -1
  139. package/scripts/git/main-sync.ts +3 -2
  140. package/scripts/git/schemas.ts +1 -1
  141. package/scripts/git/stash-pop-args.ts +7 -13
  142. package/scripts/git/stash-pop-cli.ts +2 -1
  143. package/{scripts-ai → scripts/git}/telegram-test-logic.ts +2 -2
  144. package/{scripts-ai → scripts/git}/telegram-test.ts +7 -7
  145. package/scripts/hooks/format-rewrite-notice.ts +2 -8
  146. package/scripts/init/init-ai-copy.ts +2 -5
  147. package/scripts/init/init.ts +8 -4
  148. package/scripts/init/start-steps.ts +2 -1
  149. package/scripts/init/start.ts +2 -1
  150. package/scripts/issue/issue-comment-cli.ts +2 -1
  151. package/scripts/issue/issue-file-cli.ts +2 -1
  152. package/scripts/issue/issue-fold-cli.ts +9 -16
  153. package/scripts/issue/issue-scout-cli.ts +8 -13
  154. package/scripts/issue/measure-rerun-cli.ts +7 -6
  155. package/scripts/josh/josh-commands-ai.ts +2 -2
  156. package/scripts/josh/josh-commands-backlog.ts +1 -1
  157. package/scripts/josh/josh-commands-development.ts +1 -1
  158. package/scripts/josh/josh-commands-workflow.ts +4 -4
  159. package/scripts/lane/lane-cli.ts +2 -1
  160. package/scripts/lane/lane-open.ts +3 -2
  161. package/scripts/lane/lane-registry.ts +3 -6
  162. package/scripts/lib/cli-flags.ts +20 -2
  163. package/scripts/lib/error-message.ts +9 -0
  164. package/scripts/lib/read-file.ts +26 -2
  165. package/scripts/observations/observation-ledger-home.ts +98 -26
  166. package/scripts/observations/observation-ledger-migrate.ts +26 -21
  167. package/scripts/observations/observation-ledger-prepare.ts +10 -20
  168. package/scripts/observations/observation-ledger.ts +58 -25
  169. package/scripts/observations/observations-flush-cli.ts +6 -23
  170. package/scripts/observations/observations-flush-landing.ts +2 -1
  171. package/scripts/observations/observations-flush.ts +29 -20
  172. package/scripts/package/package-scout-cli.ts +8 -16
  173. package/scripts/ports/port-command.ts +2 -1
  174. package/scripts/propagate/propagate-publish.ts +6 -7
  175. package/scripts/propagate/propagate-steps.ts +2 -1
  176. package/scripts/propagate/propagate.ts +3 -2
  177. package/scripts/refactor/import-graph.ts +2 -6
  178. package/scripts/registry-migration/migrate.ts +3 -2
  179. package/scripts/registry-migration/user-npmrc.ts +2 -2
  180. package/scripts/release/release-cli.ts +2 -1
  181. package/scripts/repo/repo-setting.ts +10 -8
  182. package/scripts/review/review-finding-ledger.ts +3 -3
  183. package/scripts/review/review-findings-cli.ts +4 -11
  184. package/scripts/review/review-record-cli.ts +14 -11
  185. package/scripts/review/review-record.ts +6 -16
  186. package/scripts/rules/decision-oracle.ts +2 -2
  187. package/scripts/rules/rule-body-guard.ts +2 -2
  188. package/scripts/rules/rule-value-cache.ts +99 -0
  189. package/scripts/rules/rule-value-cli.ts +10 -9
  190. package/scripts/rules/rule-value.ts +1 -1
  191. package/scripts/run/run-carry-args.ts +2 -6
  192. package/scripts/run/run-carry-cli.ts +5 -7
  193. package/scripts/run/run-cut-args.ts +7 -13
  194. package/scripts/run/run-ending-cli.ts +4 -7
  195. package/scripts/run/run-event-stream.ts +2 -5
  196. package/scripts/run/run-liveness-cli.ts +4 -7
  197. package/scripts/run/run-progress-args.ts +2 -6
  198. package/scripts/run/run-progress-cli.ts +2 -1
  199. package/scripts/run/run-tail-cli.ts +3 -3
  200. package/scripts/run/run-tidy-stashes.ts +7 -14
  201. package/scripts/run/run-wake-cli.ts +2 -6
  202. package/scripts/run/run-wake-driver.ts +15 -2
  203. package/scripts/run/run-wake-session.ts +14 -4
  204. package/scripts/safe-chain/project-config.ts +3 -6
  205. package/scripts/security/security-audit-provision.ts +6 -7
  206. package/scripts/sonar/sonar-hotspots-cli.ts +2 -1
  207. package/scripts/sync/directory-copy-guard.ts +2 -1
  208. package/scripts/sync/sync-ai-files.ts +3 -3
  209. package/scripts/sync/template-source-logic.ts +3 -4
  210. package/scripts/test/e2e-retry-check.ts +3 -6
  211. package/scripts/ui/ui-routes-cli.ts +3 -6
  212. package/scripts/version/bump-version.ts +2 -1
  213. package/scripts/version/version-remote.ts +18 -27
  214. package/scripts/version/version-targets.ts +3 -3
  215. package/.claude/skills/workflow-commands/rule-residency.md +0 -267
  216. package/evals/scenarios/consult-not-execute.json +0 -20
  217. package/evals/scenarios/no-implicit-workflow.json +0 -26
  218. package/evals/scenarios/overrides-both-locations.json +0 -29
  219. package/evals/scenarios/upstream-interrupt.json +0 -23
  220. package/evals/scenarios/workflow-skill-loaded.json +0 -16
  221. package/prompts/agent-lint-protocol.json +0 -79
  222. package/prompts/agent-rules.md +0 -63
  223. package/prompts/collaboration-workflow/consultation-vs-execution.md +0 -8
  224. package/prompts/collaboration-workflow/distributed-docs.md +0 -7
  225. package/prompts/collaboration-workflow/durable-rules.md +0 -24
  226. package/prompts/collaboration-workflow/latest-first.md +0 -14
  227. package/prompts/collaboration-workflow/no-clones.md +0 -11
  228. package/prompts/collaboration-workflow/simplicity-first.md +0 -9
  229. package/prompts/collaboration-workflow/single-source-rules.md +0 -13
  230. package/scripts/build/shim-shell.ts +0 -25
  231. package/scripts/claude/skill-meta.ts +0 -37
  232. package/scripts/document/document-scan.ts +0 -108
  233. package/scripts/document/generate-catalog.ts +0 -69
  234. package/scripts/document/retired-phrases.ts +0 -143
  235. package/scripts/document/review-agent-type.ts +0 -46
  236. package/scripts/document/single-source.ts +0 -73
  237. package/scripts/git/git-fixture-workspace.ts +0 -88
  238. package/scripts/hooks/format-edited-file-fixtures.ts +0 -109
  239. package/scripts/hooks/prompt-hooks.ts +0 -38
  240. package/scripts/init/hook-command-bootstrap.ts +0 -83
  241. package/scripts/issue/issue-logic.ts +0 -50
  242. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +0 -1227
  243. package/scripts/josh/josh-script-reader.ts +0 -11
  244. package/scripts/package/production-dependency-graph.ts +0 -68
  245. package/scripts/retrospective/retrospective-cli.ts +0 -97
  246. package/scripts/retrospective/retrospective.ts +0 -127
  247. package/scripts/rules/delivered-rules-harness.ts +0 -72
  248. package/scripts/rules/top-level-scripts-guard.ts +0 -47
  249. package/scripts/run/run-carry-flush.ts +0 -58
  250. package/scripts/run/run-transcript.golden.txt +0 -57
  251. package/scripts/run/run-wake-work.ts +0 -108
  252. package/scripts/sync/dependabot-pin-logic.ts +0 -135
  253. package/scripts/test/classify-isolation.ts +0 -149
  254. package/scripts/test/http-fault-injection.ts +0 -79
  255. package/scripts/test/josh-harness-environment.ts +0 -260
  256. package/scripts/test/josh-harness.ts +0 -96
  257. package/scripts/test/pilot-files.ts +0 -452
  258. package/scripts/test/pilot-measure.ts +0 -78
  259. package/scripts/test/test-network-guard.ts +0 -453
  260. package/scripts/test/test-repository-guard.ts +0 -317
  261. package/scripts/test/test-state-guard.ts +0 -85
  262. package/scripts/test/test-stdout-guard.ts +0 -54
  263. package/scripts/test/test-telegram-guard.ts +0 -90
  264. package/scripts/test/unit-guard-environment.ts +0 -12
  265. package/scripts/test/unit-projects.ts +0 -139
  266. package/scripts/test/vitest-include-globs.ts +0 -12
  267. package/scripts/time-runtime/time-batch-guard-fixtures.ts +0 -66
  268. package/scripts-ai/completion-gate.ts +0 -13
  269. package/scripts-ai/environment-loader.ts +0 -9
  270. /package/{scripts-ai → scripts/epic}/epic-cli-argv.ts +0 -0
  271. /package/{scripts-ai → scripts/epic}/epic-cli-remove.ts +0 -0
  272. /package/{scripts-ai → scripts/git}/followup-issue-number.ts +0 -0
@@ -1,317 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
- import { shim_shell } from '#scripts/build/shim-shell'
3
- import {
4
- git_location_environment,
5
- GIT_LOCATION_VARIABLES,
6
- } from '#scripts/git/git-location-environment'
7
-
8
- // **A unit test must not write into the repository the suite is running in**, and until
9
- // joshuafolkken/kit#1530 nothing said so out loud.
10
- //
11
- // `lane-change-base.test.ts` builds a real git topology in a temp directory and hands its helper
12
- // `{ cwd }` and nothing else. Run from the pre-push hook it inherited `GIT_DIR` and `GIT_INDEX_FILE`
13
- // from the hook environment — both of which beat `cwd` — so the fixture's own commits landed on the
14
- // branch being pushed. `1085-lane`'s reflog carries three of them, titled `base` and
15
- // `another lane merged`, 26 seconds after the run's real commit; `1080-lane` took one. The same
16
- // helper's `git config user.email` wrote a fixture identity into `--local` config, which every
17
- // linked work tree of the repository shares, so all five checkouts on the machine started authoring
18
- // commits as `Lane Fixture <lane@example.test>`.
19
- //
20
- // **This is the second instance of one class in a day.** joshuafolkken/kit#1515 fixed the first, in
21
- // `propagate-git`'s probes, by clearing the location variables — and fixing the instance is all it
22
- // did. The class recurred within hours, in a file written after it, arriving the same way: silently,
23
- // with the only evidence a reflog someone had to go and read.
24
- //
25
- // **So the guard is built one dimension over from #1515's own network guard, on the same mechanism**
26
- // (`test-network-guard.ts` arms the `PATH` shim, holds the record and fails the run from
27
- // `globalSetup`'s teardown; this module contributes the `sh` that decides). Reusing it rather than
28
- // writing a second guard is `CLAUDE.md` → "No clones", and it is also the only way the two can share
29
- // one record: a run that both reached the network and wrote to the repository reports both.
30
- //
31
- // **What it refuses, and why that is narrower than "any command touching the repository".** Reads
32
- // are the suite's business — half of it reads the repository it is running in, and refusing that
33
- // would stop the suite rather than the defect. A read also cannot move a branch. So the arm is the
34
- // *writing* subcommands, and it refuses one on either of two findings:
35
- //
36
- // 1. **Any git location variable is set.** A fixture is targeted by clearing the environment, never
37
- // by out-arguing it, so a writing command reaching git with `GIT_DIR` or `GIT_INDEX_FILE` still
38
- // in its environment is refused whatever it was aimed at. This is the exact shape of both
39
- // instances, and it is decided without spawning anything.
40
- // 2. **The command resolves into the guarded repository.** The environment can be clean and the
41
- // write still land here, by way of an ambient `cwd` — a test that `process.chdir`s into the
42
- // checkout and commits. The resolution is git's own `rev-parse --absolute-git-dir`, **asked with
43
- // the caller's own global options in front of it**, so `-C <fixture>` is not refused for the
44
- // directory the shim happens to sit in and `--git-dir <checkout>` is not let through for it.
45
- //
46
- // The two lists below say which finding a subcommand faces: an unambiguous writer faces both, and
47
- // one whose reading spelling this suite uses faces the first only.
48
- //
49
- // **It fails closed on the second finding**: a resolution that fails means the command is not inside
50
- // any repository at all — that is an answer, not an unknown, and the first finding has already
51
- // refused every environment that could have redirected it elsewhere.
52
- //
53
- // **The one deliberate fail-open** is a `PROJECT_ROOT` that is not a git repository, where there is
54
- // no repository for a test to damage and the second finding is left out of the generated shim. The
55
- // first finding is generated unconditionally, so an inherited environment is refused either way.
56
- //
57
- // **What it names.** The shim cannot see which test called it — it is a `PATH` shim, and vitest puts
58
- // no test identity in the environment. It does not need to: it refuses **at the call**, with `exit 1`
59
- // and the command and the finding on stderr, so `execa` throws inside the test and vitest names the
60
- // file and the test itself. The record read at teardown is the backstop for a test that swallows the
61
- // error, and it carries the command and the finding for the same reason — so nobody has to
62
- // reconstruct it from a reflog the way joshuafolkken/kit#1530 was.
63
-
64
- // The `git` subcommands that can write to a repository, an index or a config. Absent from it are the
65
- // reads the suite lives on (`status`, `log`, `diff`, `rev-parse`, `merge-base`, `show`,
66
- // `ls-files`, `cat-file`, `describe`) and the network subcommands, which `test-network-guard.ts`
67
- // refuses outright before this arm is reached.
68
- //
69
- // **`symbolic-ref` is deliberately absent**: its writing spelling takes a second argument and its
70
- // reading spelling does not, and splitting that inside a shell `case` fails open when it gets it
71
- // wrong. Nothing in this suite writes a symbolic ref, and the branch-moving spellings that matter
72
- // (`update-ref`, `checkout`, `switch`, `reset`) are all here.
73
- const GIT_WRITING_SUBCOMMANDS: ReadonlyArray<string> = [
74
- 'add',
75
- 'am',
76
- 'checkout',
77
- 'cherry-pick',
78
- 'clean',
79
- 'commit',
80
- 'gc',
81
- 'init',
82
- 'merge',
83
- 'mv',
84
- 'prune',
85
- 'rebase',
86
- 'reset',
87
- 'restore',
88
- 'revert',
89
- 'rm',
90
- 'switch',
91
- 'update-index',
92
- 'update-ref',
93
- ]
94
-
95
- // The subcommands whose **reading** spelling this suite actually uses — `worktree list`,
96
- // `branch --show-current`, `config --get`, `stash list`, `tag -l`, `notes list`, `apply --check`.
97
- // They are refused on the inherited-environment finding only.
98
- //
99
- // **Which is not a hole, but the line between the two findings drawn where it belongs.** An
100
- // inherited environment is wrong for a read as surely as for a write — it is the other half of what
101
- // joshuafolkken/kit#1530 did, where `git_command`'s readings answered about the repository being
102
- // pushed — so finding 1 covers these whole, `config user.email <value>` and `worktree add` included.
103
- // Finding 2 is the one that cannot tell them apart: an ambient `cwd` inside the checkout is exactly
104
- // how a legitimate read of the real repository is spelled, and refusing that would stop the suite
105
- // rather than the defect (`test-network-guard.ts` names the worktree machinery as a read that has to
106
- // keep working). Splitting them on their flags instead was rejected for the reason the network list
107
- // refuses `remote` whole: getting a flag `case` wrong in `sh` fails **open**.
108
- const GIT_AMBIGUOUS_SUBCOMMANDS: ReadonlyArray<string> = [
109
- 'apply',
110
- 'branch',
111
- 'config',
112
- 'notes',
113
- 'stash',
114
- 'tag',
115
- 'worktree',
116
- ]
117
-
118
- // The word every recorded write violation starts with, so one shared record still says which guard
119
- // caught which call and `describe_violations` can report each kind under its own heading.
120
- const WRITE_MARKER = 'repository write:'
121
- // **The headline states the rule and the detail line states the finding**, because the two findings
122
- // have different fixes: clearing the environment is the answer to the first and gives the second a
123
- // misleading instruction, which is what the same sentence said for both until review round 1.
124
- const BLOCKED_WRITE_MESSAGE =
125
- 'josh: a unit test must not run a writing git command against the repository the suite is running in'
126
- // Finding-neutral, like the stderr message and for the same reason: each recorded line carries its
127
- // own `— via …`, and a run failed on the working-directory finding must not be told to clear an
128
- // environment that was already clean (review round 2).
129
- const WRITE_VIOLATION_HEADING =
130
- 'The unit suite ran a writing git command against the repository it is running in. Each line names what took it there:'
131
-
132
- // The record and the stderr detail both read as prose and both expand in `sh`: `$*` is the arguments
133
- // the shim stood in for, `$guard_via` the finding that refused them.
134
- const REFUSED_COMMAND = 'git $* — via $guard_via'
135
- const RECORDED_WRITE = `${WRITE_MARKER} ${REFUSED_COMMAND}`
136
-
137
- const GIT_DIRECTORY_ARGUMENTS = ['rev-parse', '--path-format=absolute', '--git-common-dir']
138
-
139
- // **The one failure that is an answer rather than an unknown.** There is no repository at
140
- // `PROJECT_ROOT`, so finding 2 has nothing to protect and is left out of the generated shim while
141
- // finding 1 stays armed. Every other failure — an unreadable directory, a git that cannot run — is a
142
- // guard that does not know what it is guarding, and a guard that cannot answer must not report the
143
- // run clean (review round 1 of joshuafolkken/kit#1530).
144
- const NOT_A_REPOSITORY = 'not a git repository'
145
- const RESOLUTION_HEADING =
146
- 'The unit-suite repository guard could not work out which repository it protects, so the run proves nothing:'
147
-
148
- function text_of(value: unknown): string {
149
- return typeof value === 'string' ? value : ''
150
- }
151
-
152
- // What git said, plus what node said when git never ran — the two places the reason can be, joined
153
- // so one `includes` decides between them.
154
- function stderr_of(error: unknown): string {
155
- if (typeof error !== 'object' || error === null) return ''
156
-
157
- const stderr = 'stderr' in error ? text_of(error.stderr) : ''
158
- const message = 'message' in error ? text_of(error.message) : ''
159
-
160
- return `${stderr} ${message}`.trim()
161
- }
162
-
163
- function is_absent_repository(error: unknown): boolean {
164
- return stderr_of(error).includes(NOT_A_REPOSITORY)
165
- }
166
-
167
- // The repository this guard protects, as git itself resolves it — the **common** directory rather
168
- // than the per-work-tree one, so a lane's shim refuses a write into any of the repository's linked
169
- // work trees and not only into its own.
170
- function guarded_git_directory(git_binary: string | undefined, from: string): string | undefined {
171
- if (git_binary === undefined) return undefined
172
-
173
- try {
174
- return execFileSync(git_binary, GIT_DIRECTORY_ARGUMENTS, {
175
- cwd: from,
176
- encoding: 'utf8',
177
- // **Cleared here too, and not belt-and-braces.** The guard is armed from `globalSetup`, and
178
- // under the pre-push hook that process has itself inherited `GIT_DIR` — so asking without
179
- // clearing it answers about whatever the hook was pointed at, and the shim would be built
180
- // to protect a repository this suite has nothing to do with.
181
- env: { ...process.env, ...git_location_environment.location_free_environment() },
182
- stdio: ['ignore', 'pipe', 'pipe'],
183
- }).trim()
184
- } catch (error) {
185
- if (!is_absent_repository(error)) {
186
- throw new Error(`${RESOLUTION_HEADING} ${stderr_of(error)}`, { cause: error })
187
- }
188
-
189
- return undefined
190
- }
191
- }
192
-
193
- // Finding 1, one line per variable. `${NAME-}` rather than `$NAME` so an unset name is the empty
194
- // string under `set -u` as readily as without it.
195
- function environment_lines(): Array<string> {
196
- return GIT_LOCATION_VARIABLES.map(
197
- (name) => `[ -z "\${${name}-}" ] || guard_env="$guard_env ${name}"`,
198
- )
199
- }
200
-
201
- // The `sh` function finding 2 resolves through, emitted once at the top of the shim.
202
- //
203
- // **It keeps the caller's own global options** — the words the scan counted before the subcommand —
204
- // and asks git where *that* command would act. `cwd` alone is not that answer, in both directions:
205
- // `git -C <fixture>` moves it away from the directory the shim was spawned in, and
206
- // `git --git-dir <checkout>` moves it towards the guarded one. A resolution taken from `cwd` refused
207
- // the first and let the second straight through, which is the bypass review round 1 demonstrated.
208
- //
209
- // The rotation is how a POSIX shell function keeps only its first `n` arguments: each of the first
210
- // `n` is moved to the end, and everything left in front of them is then dropped. A function has its
211
- // own positional parameters, so the shim's `$@` is untouched and still `exec`s intact.
212
- function resolver_lines(git_binary: string): Array<string> {
213
- return [
214
- 'guard_dir_of() {',
215
- '\tguard_kept=$1',
216
- '\tshift',
217
- '\tguard_i=0',
218
- '\twhile [ "$guard_i" -lt "$guard_kept" ]; do',
219
- '\t\tguard_head=$1',
220
- '\t\tshift',
221
- '\t\tset -- "$@" "$guard_head"',
222
- '\t\tguard_i=$((guard_i + 1))',
223
- '\tdone',
224
- '\twhile [ "$#" -gt "$guard_kept" ]; do shift; done',
225
- `\t${shim_shell.quoted(git_binary)} "$@" rev-parse --absolute-git-dir 2>/dev/null`,
226
- '}',
227
- ]
228
- }
229
-
230
- // Finding 2, asked only when finding 1 found nothing — a contaminated environment is already a
231
- // refusal, and asking git where it would write costs a process.
232
- function target_lines(guarded_directory: string): Array<string> {
233
- const quoted_directory = shim_shell.quoted(guarded_directory)
234
-
235
- return [
236
- 'if [ -z "$guard_via" ]; then',
237
- '\tguard_dir=$(guard_dir_of "$guard_seen" "$@")',
238
- '\tcase "$guard_dir" in',
239
- `\t\t${quoted_directory}|${quoted_directory}/*) guard_via="a working directory inside $guard_dir" ;;`,
240
- '\tesac',
241
- 'fi',
242
- ]
243
- }
244
-
245
- function refusal_lines(log_file: string): Array<string> {
246
- return [
247
- 'if [ -n "$guard_via" ]; then',
248
- `\t${shim_shell.record_line(log_file, RECORDED_WRITE)}`,
249
- `\techo ${shim_shell.quoted(BLOCKED_WRITE_MESSAGE)} >&2`,
250
- `\techo " refused: ${REFUSED_COMMAND}" >&2`,
251
- '\texit 1',
252
- 'fi',
253
- ]
254
- }
255
-
256
- interface WriteGuard {
257
- log_file: string
258
- guarded_directory: string | undefined
259
- }
260
-
261
- // Finding 1, with the two variables it answers through declared empty first.
262
- function environment_findings(): Array<string> {
263
- return [
264
- "guard_env=''",
265
- ...environment_lines(),
266
- "guard_via=''",
267
- '[ -z "$guard_env" ] || guard_via="an inherited git environment:$guard_env"',
268
- ]
269
- }
270
-
271
- // Finding 1 on its own, which is also the whole of the ambiguous arm.
272
- function environment_guard_lines(log_file: string): Array<string> {
273
- return [...environment_findings(), ...refusal_lines(log_file)]
274
- }
275
-
276
- // The whole `sh` body of the shim's writing-subcommand arm, unindented — the caller indents it into
277
- // the `case` the way it does the blocking lines it already had. Finding 1 then finding 2, with the
278
- // refusal after both, so the cheap one answers first and the process is spawned only when it must be.
279
- function write_guard_lines(guard: WriteGuard): Array<string> {
280
- const { guarded_directory } = guard
281
-
282
- if (guarded_directory === undefined) return environment_guard_lines(guard.log_file)
283
-
284
- return [
285
- ...environment_findings(),
286
- ...target_lines(guarded_directory),
287
- ...refusal_lines(guard.log_file),
288
- ]
289
- }
290
-
291
- // The shim's prelude: the resolver, where there is a repository to resolve against.
292
- function prelude_lines(git_binary: string, guarded_directory: string | undefined): Array<string> {
293
- return guarded_directory === undefined ? [] : resolver_lines(git_binary)
294
- }
295
-
296
- // Which heading a recorded line belongs under. The record is shared with the network guard, so the
297
- // marker is what tells the two apart at teardown.
298
- function is_write_violation(call: string): boolean {
299
- return call.startsWith(WRITE_MARKER)
300
- }
301
-
302
- const test_repository_guard = {
303
- BLOCKED_WRITE_MESSAGE,
304
- GIT_AMBIGUOUS_SUBCOMMANDS,
305
- GIT_WRITING_SUBCOMMANDS,
306
- RESOLUTION_HEADING,
307
- WRITE_MARKER,
308
- WRITE_VIOLATION_HEADING,
309
- environment_guard_lines,
310
- guarded_git_directory,
311
- is_write_violation,
312
- prelude_lines,
313
- write_guard_lines,
314
- }
315
-
316
- export { test_repository_guard }
317
- export type { WriteGuard }
@@ -1,85 +0,0 @@
1
- import { execSync } from 'node:child_process'
2
-
3
- // Environment variables that git reads from the environment. A test that sets these affects every
4
- // subsequent git command in the same worker when isolation is disabled.
5
- const GIT_ENV_KEYS: ReadonlyArray<string> = [
6
- 'GIT_DIR',
7
- 'GIT_INDEX_FILE',
8
- 'GIT_OBJECT_DIRECTORY',
9
- 'GIT_WORK_TREE',
10
- 'GIT_COMMON_DIR',
11
- ]
12
-
13
- interface StateSnapshot {
14
- branch: string
15
- status: string
16
- git_env: Record<string, string | undefined>
17
- }
18
-
19
- function run_git(args: string): string {
20
- try {
21
- return execSync(`git ${args}`, { encoding: 'utf8', stdio: 'pipe' }).trim()
22
- } catch {
23
- return '(error)'
24
- }
25
- }
26
-
27
- function capture_git_environment(): Record<string, string | undefined> {
28
- return Object.fromEntries(GIT_ENV_KEYS.map((key) => [key, process.env[key]]))
29
- }
30
-
31
- function capture_state(): StateSnapshot {
32
- return {
33
- branch: run_git('rev-parse --abbrev-ref HEAD'),
34
- status: run_git('status --porcelain'),
35
- git_env: capture_git_environment(),
36
- }
37
- }
38
-
39
- function environment_diff(
40
- before: Record<string, string | undefined>,
41
- after: Record<string, string | undefined>,
42
- ): Array<string> {
43
- return GIT_ENV_KEYS.filter((key) => before[key] !== after[key]).map(
44
- (key) => ` ${key}: ${String(before[key])} → ${String(after[key])}`,
45
- )
46
- }
47
-
48
- function format_violations(before: StateSnapshot, after: StateSnapshot): string | undefined {
49
- const lines: Array<string> = []
50
-
51
- if (before.branch !== after.branch) {
52
- lines.push(` branch: ${before.branch} → ${after.branch}`)
53
- }
54
-
55
- if (before.status !== after.status) {
56
- lines.push(' working tree changed (run "git status" to inspect)')
57
- }
58
-
59
- lines.push(...environment_diff(before.git_env, after.git_env))
60
-
61
- if (lines.length === 0) return undefined
62
-
63
- return `test-state-guard: tests mutated repository state:\n${lines.join('\n')}`
64
- }
65
-
66
- function setup(): () => void {
67
- const before = capture_state()
68
-
69
- return function disarm(): void {
70
- const after = capture_state()
71
- const violation = format_violations(before, after)
72
-
73
- if (violation !== undefined) throw new Error(violation)
74
- }
75
- }
76
-
77
- const test_state_guard = {
78
- GIT_ENV_KEYS,
79
- capture_state,
80
- env_diff: environment_diff,
81
- format_violations,
82
- }
83
-
84
- export type { StateSnapshot }
85
- export { setup, test_state_guard }
@@ -1,54 +0,0 @@
1
- import { beforeEach } from 'vitest'
2
-
3
- // **Fixture writes to the real streams no longer reach the gate's output** (joshuafolkken/kit#2296).
4
- // A unit test that drives a CLI `main`/`run` — `test:declared --match`, `run:review` with a bad
5
- // argument — writes its `match:` / `Usage: josh …` lines straight to `process.stdout` and
6
- // `process.stderr`, which Vitest forwards from the worker verbatim. Those lines then sat at the tail
7
- // of `pnpm josh test:unit`, and a run reading the gate with `tail` took them for the suite's own
8
- // output. Silencing the two streams for the body of every test drops them; console.* is untouched, so
9
- // a deliberate debug line still shows, and the restore is a per-test teardown so nothing leaks between
10
- // tests or into Vitest's own reporter.
11
-
12
- type WriteFunction = typeof process.stdout.write
13
-
14
- interface SavedWrites {
15
- stdout: WriteFunction
16
- stderr: WriteFunction
17
- }
18
-
19
- function swallow(): true {
20
- return true
21
- }
22
-
23
- // Swaps both streams' `write` for a no-op and returns what was there, so the caller restores exactly
24
- // what it replaced rather than a stream reference captured somewhere else — Vitest wraps these in the
25
- // worker, and that wrapper is what must come back.
26
- function silence_streams(): SavedWrites {
27
- const saved: SavedWrites = { stdout: process.stdout.write, stderr: process.stderr.write }
28
-
29
- process.stdout.write = swallow
30
- process.stderr.write = swallow
31
-
32
- return saved
33
- }
34
-
35
- function restore_streams(saved: SavedWrites): void {
36
- process.stdout.write = saved.stdout
37
- process.stderr.write = saved.stderr
38
- }
39
-
40
- // The teardown is returned from `beforeEach` rather than kept in a module binding, so two tests never
41
- // share one saved pair and nothing here writes to a top-level variable from inside a hook. The hook
42
- // registration is the setup file's whole purpose, so its top-level effect is deliberate.
43
- // eslint-disable-next-line unicorn/no-top-level-side-effects
44
- beforeEach(() => {
45
- const saved = silence_streams()
46
-
47
- return () => {
48
- restore_streams(saved)
49
- }
50
- })
51
-
52
- const test_stdout_guard = { restore_streams, silence_streams }
53
-
54
- export { test_stdout_guard }
@@ -1,90 +0,0 @@
1
- import { appendFileSync } from 'node:fs'
2
- import { expect } from 'vitest'
3
- import { GUARD_LOG_KEY } from './unit-guard-environment'
4
-
5
- // **A unit test that forgets to mock a notification must fail rather than notify**
6
- // (joshuafolkken/kit#2494). A lane child's new test drove `run_carry_cli.run(['--end', '--stopped',
7
- // …])` without replacing `./run-stop-notify`, and two runs of it sent two real "backlogrun stopped"
8
- // confirmations — while the run was still going — and the test passed. `telegram-notify.ts` posts
9
- // with Node's own `fetch`, which the `gh` / `git` `PATH` shims in `test-network-guard.ts` never see.
10
- //
11
- // **This wraps `fetch` in every worker rather than mocking the notifier module**, for the reason the
12
- // network guard shims a binary rather than mocking a module: any caller that reaches Telegram is
13
- // caught, not only the import graph a mock was written against. Anything not addressed to Telegram
14
- // is handed to the real `fetch` untouched, and a test that stubs `fetch` itself replaces this
15
- // wrapper for its own duration, so the correctly mocked suites (`telegram-notify.test.ts`) see
16
- // nothing of it.
17
- //
18
- // **The attempt is recorded in the network guard's own log, not a second one.** `disarm` already
19
- // reads that record at teardown and throws with every line in it, so a Telegram send fails the run
20
- // and is reported exactly as a live `gh api` is — naming the test that made it, since a `fetch` has
21
- // no command line to quote. The call itself is refused either way, so even an unarmed run sends
22
- // nothing.
23
- //
24
- // **Only this process is covered.** A CLI a test spawns as a subprocess loads no setup file; the
25
- // unit suite's Telegram sends so far have all been in-process, and the subprocess case is left to
26
- // the notifier's own mocks.
27
-
28
- const TELEGRAM_HOST = 'api.telegram.org'
29
- const RECORD_NAME = 'telegram'
30
- const BLOCKED_MESSAGE =
31
- 'josh: the unit suite must not send a Telegram notification — mock it instead'
32
- const UNKNOWN_TEST = '(outside a test)'
33
- // Marks a `fetch` that is already this wrapper. `isolate: false` re-runs a setup file for every test
34
- // file in one worker, and wrapping the wrapper each time would stack one closure per file.
35
- const GUARDED_MARK = Symbol.for('josh.unit-telegram-guard')
36
-
37
- type Fetch = typeof globalThis.fetch
38
- type GuardedFetch = Fetch & { [GUARDED_MARK]?: true }
39
-
40
- function request_url(input: Parameters<Fetch>[0]): string {
41
- if (typeof input === 'string') return input
42
-
43
- return input instanceof URL ? input.href : input.url
44
- }
45
-
46
- // The host, compared exactly. The URL carries the bot token in its path, so nothing of it is kept.
47
- function is_telegram(input: Parameters<Fetch>[0]): boolean {
48
- return URL.parse(request_url(input))?.hostname === TELEGRAM_HOST
49
- }
50
-
51
- function current_test(): string {
52
- const state = expect.getState()
53
-
54
- return [state.testPath, state.currentTestName ?? UNKNOWN_TEST].filter(Boolean).join(' > ')
55
- }
56
-
57
- // `>>`-style append, for `shim_shell.record_line`'s reason: every worker writes the one record.
58
- function record(log_file: string | undefined): void {
59
- if (log_file !== undefined) appendFileSync(log_file, `${RECORD_NAME} ${current_test()}\n`)
60
- }
61
-
62
- function guarded_fetch(real_fetch: Fetch): GuardedFetch {
63
- const guarded: GuardedFetch = async function (input, init) {
64
- if (!is_telegram(input)) return await real_fetch(input, init)
65
-
66
- record(process.env[GUARD_LOG_KEY])
67
-
68
- throw new Error(BLOCKED_MESSAGE)
69
- }
70
-
71
- guarded[GUARDED_MARK] = true
72
-
73
- return guarded
74
- }
75
-
76
- // A plain assignment rather than `vi.stubGlobal`: a test's own `vi.unstubAllGlobals` restores what
77
- // was there before the *first* stub, which would be the unguarded `fetch` had this been one.
78
- function install(): void {
79
- const current: GuardedFetch = fetch
80
-
81
- if (current[GUARDED_MARK] !== true) Reflect.set(globalThis, 'fetch', guarded_fetch(current))
82
- }
83
-
84
- // The wrapping is the setup file's whole purpose, so its top-level effect is deliberate.
85
- // eslint-disable-next-line unicorn/no-top-level-side-effects
86
- install()
87
-
88
- const test_telegram_guard = { BLOCKED_MESSAGE, RECORD_NAME, guarded_fetch, is_telegram }
89
-
90
- export { test_telegram_guard }
@@ -1,12 +0,0 @@
1
- // The environment variable the network guard's record is handed to the test workers under
2
- // (joshuafolkken/kit#2494). The `gh` and `git` shims carry the record's path baked into their own
3
- // script, but an in-process guard — the Telegram one wraps `fetch` inside each worker — has nothing
4
- // baked in, so `arm` exports the path and every worker inherits it with `PATH`.
5
- //
6
- // A module of its own because both sides must name it and neither may load the other: the network
7
- // guard runs in Vitest's main process and must not import `vitest`, and the Telegram guard runs in
8
- // every worker, where loading the network guard would resolve the guarded repository with a `git`
9
- // spawn once per test file.
10
- const GUARD_LOG_KEY = 'JOSH_UNIT_GUARD_LOG'
11
-
12
- export { GUARD_LOG_KEY }