instar 0.28.74 → 0.28.76

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 (323) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/discovery.d.ts.map +1 -1
  4. package/dist/commands/discovery.js +1 -0
  5. package/dist/commands/discovery.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +2 -0
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/job.d.ts.map +1 -1
  10. package/dist/commands/job.js +1 -0
  11. package/dist/commands/job.js.map +1 -1
  12. package/dist/commands/ledgerCleanup.d.ts.map +1 -1
  13. package/dist/commands/ledgerCleanup.js +1 -0
  14. package/dist/commands/ledgerCleanup.js.map +1 -1
  15. package/dist/commands/listener.d.ts.map +1 -1
  16. package/dist/commands/listener.js +6 -0
  17. package/dist/commands/listener.js.map +1 -1
  18. package/dist/commands/nuke.d.ts.map +1 -1
  19. package/dist/commands/nuke.js +6 -0
  20. package/dist/commands/nuke.js.map +1 -1
  21. package/dist/commands/server.d.ts.map +1 -1
  22. package/dist/commands/server.js +2 -0
  23. package/dist/commands/server.js.map +1 -1
  24. package/dist/commands/setup.d.ts.map +1 -1
  25. package/dist/commands/setup.js +6 -0
  26. package/dist/commands/setup.js.map +1 -1
  27. package/dist/commands/slack-cli.d.ts.map +1 -1
  28. package/dist/commands/slack-cli.js +4 -0
  29. package/dist/commands/slack-cli.js.map +1 -1
  30. package/dist/commands/whatsapp.d.ts.map +1 -1
  31. package/dist/commands/whatsapp.js +1 -0
  32. package/dist/commands/whatsapp.js.map +1 -1
  33. package/dist/commands/worktree.d.ts.map +1 -1
  34. package/dist/commands/worktree.js +1 -0
  35. package/dist/commands/worktree.js.map +1 -1
  36. package/dist/core/AgentConnector.d.ts.map +1 -1
  37. package/dist/core/AgentConnector.js +3 -0
  38. package/dist/core/AgentConnector.js.map +1 -1
  39. package/dist/core/AgentRegistry.d.ts.map +1 -1
  40. package/dist/core/AgentRegistry.js +2 -0
  41. package/dist/core/AgentRegistry.js.map +1 -1
  42. package/dist/core/AutoDispatcher.d.ts.map +1 -1
  43. package/dist/core/AutoDispatcher.js +1 -0
  44. package/dist/core/AutoDispatcher.js.map +1 -1
  45. package/dist/core/AutoUpdater.d.ts.map +1 -1
  46. package/dist/core/AutoUpdater.js +1 -0
  47. package/dist/core/AutoUpdater.js.map +1 -1
  48. package/dist/core/AutonomousEvolution.d.ts.map +1 -1
  49. package/dist/core/AutonomousEvolution.js +1 -0
  50. package/dist/core/AutonomousEvolution.js.map +1 -1
  51. package/dist/core/BackupManager.d.ts.map +1 -1
  52. package/dist/core/BackupManager.js +1 -0
  53. package/dist/core/BackupManager.js.map +1 -1
  54. package/dist/core/BranchManager.d.ts.map +1 -1
  55. package/dist/core/BranchManager.js +3 -0
  56. package/dist/core/BranchManager.js.map +1 -1
  57. package/dist/core/CaffeinateManager.d.ts.map +1 -1
  58. package/dist/core/CaffeinateManager.js +1 -0
  59. package/dist/core/CaffeinateManager.js.map +1 -1
  60. package/dist/core/DeferredDispatchTracker.d.ts.map +1 -1
  61. package/dist/core/DeferredDispatchTracker.js +1 -0
  62. package/dist/core/DeferredDispatchTracker.js.map +1 -1
  63. package/dist/core/DispatchManager.d.ts.map +1 -1
  64. package/dist/core/DispatchManager.js +2 -0
  65. package/dist/core/DispatchManager.js.map +1 -1
  66. package/dist/core/EvolutionManager.d.ts.map +1 -1
  67. package/dist/core/EvolutionManager.js +1 -0
  68. package/dist/core/EvolutionManager.js.map +1 -1
  69. package/dist/core/ExecutionJournal.d.ts.map +1 -1
  70. package/dist/core/ExecutionJournal.js +1 -0
  71. package/dist/core/ExecutionJournal.js.map +1 -1
  72. package/dist/core/FeedbackManager.d.ts.map +1 -1
  73. package/dist/core/FeedbackManager.js +1 -0
  74. package/dist/core/FeedbackManager.js.map +1 -1
  75. package/dist/core/FileClassifier.d.ts.map +1 -1
  76. package/dist/core/FileClassifier.js +4 -0
  77. package/dist/core/FileClassifier.js.map +1 -1
  78. package/dist/core/ForegroundRestartWatcher.d.ts.map +1 -1
  79. package/dist/core/ForegroundRestartWatcher.js +2 -0
  80. package/dist/core/ForegroundRestartWatcher.js.map +1 -1
  81. package/dist/core/GitStateManager.d.ts.map +1 -1
  82. package/dist/core/GitStateManager.js +1 -0
  83. package/dist/core/GitStateManager.js.map +1 -1
  84. package/dist/core/GitSync.d.ts.map +1 -1
  85. package/dist/core/GitSync.js +5 -0
  86. package/dist/core/GitSync.js.map +1 -1
  87. package/dist/core/GlobalInstallCleanup.d.ts.map +1 -1
  88. package/dist/core/GlobalInstallCleanup.js +2 -0
  89. package/dist/core/GlobalInstallCleanup.js.map +1 -1
  90. package/dist/core/GlobalSecretStore.d.ts.map +1 -1
  91. package/dist/core/GlobalSecretStore.js +2 -0
  92. package/dist/core/GlobalSecretStore.js.map +1 -1
  93. package/dist/core/HandoffManager.d.ts.map +1 -1
  94. package/dist/core/HandoffManager.js +4 -0
  95. package/dist/core/HandoffManager.js.map +1 -1
  96. package/dist/core/LedgerSessionRegistry.d.ts.map +1 -1
  97. package/dist/core/LedgerSessionRegistry.js +1 -0
  98. package/dist/core/LedgerSessionRegistry.js.map +1 -1
  99. package/dist/core/MachineIdentity.d.ts.map +1 -1
  100. package/dist/core/MachineIdentity.js +1 -0
  101. package/dist/core/MachineIdentity.js.map +1 -1
  102. package/dist/core/ParallelDevWiring.d.ts.map +1 -1
  103. package/dist/core/ParallelDevWiring.js +1 -0
  104. package/dist/core/ParallelDevWiring.js.map +1 -1
  105. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  106. package/dist/core/PostUpdateMigrator.js +2 -0
  107. package/dist/core/PostUpdateMigrator.js.map +1 -1
  108. package/dist/core/ProjectMapper.d.ts.map +1 -1
  109. package/dist/core/ProjectMapper.js +2 -0
  110. package/dist/core/ProjectMapper.js.map +1 -1
  111. package/dist/core/RelationshipManager.d.ts.map +1 -1
  112. package/dist/core/RelationshipManager.js +2 -0
  113. package/dist/core/RelationshipManager.js.map +1 -1
  114. package/dist/core/SafeFsExecutor.d.ts +41 -0
  115. package/dist/core/SafeFsExecutor.d.ts.map +1 -0
  116. package/dist/core/SafeFsExecutor.js +146 -0
  117. package/dist/core/SafeFsExecutor.js.map +1 -0
  118. package/dist/core/SafeGitExecutor.d.ts +139 -0
  119. package/dist/core/SafeGitExecutor.d.ts.map +1 -0
  120. package/dist/core/SafeGitExecutor.js +631 -0
  121. package/dist/core/SafeGitExecutor.js.map +1 -0
  122. package/dist/core/ScopeVerifier.d.ts.map +1 -1
  123. package/dist/core/ScopeVerifier.js +1 -0
  124. package/dist/core/ScopeVerifier.js.map +1 -1
  125. package/dist/core/SecretStore.d.ts.map +1 -1
  126. package/dist/core/SecretStore.js +1 -0
  127. package/dist/core/SecretStore.js.map +1 -1
  128. package/dist/core/SharedStateLedger.d.ts.map +1 -1
  129. package/dist/core/SharedStateLedger.js +1 -0
  130. package/dist/core/SharedStateLedger.js.map +1 -1
  131. package/dist/core/SoulManager.d.ts.map +1 -1
  132. package/dist/core/SoulManager.js +2 -0
  133. package/dist/core/SoulManager.js.map +1 -1
  134. package/dist/core/SourceTreeGuard.d.ts +69 -0
  135. package/dist/core/SourceTreeGuard.d.ts.map +1 -0
  136. package/dist/core/SourceTreeGuard.js +378 -0
  137. package/dist/core/SourceTreeGuard.js.map +1 -0
  138. package/dist/core/StateManager.d.ts.map +1 -1
  139. package/dist/core/StateManager.js +49 -9
  140. package/dist/core/StateManager.js.map +1 -1
  141. package/dist/core/SyncOrchestrator.d.ts.map +1 -1
  142. package/dist/core/SyncOrchestrator.js +3 -0
  143. package/dist/core/SyncOrchestrator.js.map +1 -1
  144. package/dist/core/UpdateChecker.d.ts.map +1 -1
  145. package/dist/core/UpdateChecker.js +2 -0
  146. package/dist/core/UpdateChecker.js.map +1 -1
  147. package/dist/core/UpgradeGuideProcessor.d.ts.map +1 -1
  148. package/dist/core/UpgradeGuideProcessor.js +2 -0
  149. package/dist/core/UpgradeGuideProcessor.js.map +1 -1
  150. package/dist/core/WorktreeManager.d.ts.map +1 -1
  151. package/dist/core/WorktreeManager.js +7 -0
  152. package/dist/core/WorktreeManager.js.map +1 -1
  153. package/dist/knowledge/KnowledgeManager.d.ts.map +1 -1
  154. package/dist/knowledge/KnowledgeManager.js +1 -0
  155. package/dist/knowledge/KnowledgeManager.js.map +1 -1
  156. package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
  157. package/dist/lifeline/ServerSupervisor.js +14 -0
  158. package/dist/lifeline/ServerSupervisor.js.map +1 -1
  159. package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
  160. package/dist/lifeline/TelegramLifeline.js +1 -0
  161. package/dist/lifeline/TelegramLifeline.js.map +1 -1
  162. package/dist/lifeline/droppedMessages.d.ts.map +1 -1
  163. package/dist/lifeline/droppedMessages.js +1 -0
  164. package/dist/lifeline/droppedMessages.js.map +1 -1
  165. package/dist/memory/EpisodicMemory.d.ts.map +1 -1
  166. package/dist/memory/EpisodicMemory.js +1 -0
  167. package/dist/memory/EpisodicMemory.js.map +1 -1
  168. package/dist/memory/TopicMemory.d.ts.map +1 -1
  169. package/dist/memory/TopicMemory.js +4 -0
  170. package/dist/memory/TopicMemory.js.map +1 -1
  171. package/dist/messaging/AgentTokenManager.d.ts.map +1 -1
  172. package/dist/messaging/AgentTokenManager.js +1 -0
  173. package/dist/messaging/AgentTokenManager.js.map +1 -1
  174. package/dist/messaging/DropPickup.js +1 -0
  175. package/dist/messaging/DropPickup.js.map +1 -1
  176. package/dist/messaging/GitSyncTransport.d.ts.map +1 -1
  177. package/dist/messaging/GitSyncTransport.js +3 -0
  178. package/dist/messaging/GitSyncTransport.js.map +1 -1
  179. package/dist/messaging/MessageStore.d.ts.map +1 -1
  180. package/dist/messaging/MessageStore.js +2 -0
  181. package/dist/messaging/MessageStore.js.map +1 -1
  182. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  183. package/dist/messaging/TelegramAdapter.js +4 -0
  184. package/dist/messaging/TelegramAdapter.js.map +1 -1
  185. package/dist/messaging/backends/BaileysBackend.d.ts.map +1 -1
  186. package/dist/messaging/backends/BaileysBackend.js +2 -0
  187. package/dist/messaging/backends/BaileysBackend.js.map +1 -1
  188. package/dist/messaging/shared/EncryptedAuthStore.d.ts.map +1 -1
  189. package/dist/messaging/shared/EncryptedAuthStore.js +2 -0
  190. package/dist/messaging/shared/EncryptedAuthStore.js.map +1 -1
  191. package/dist/messaging/shared/MessageLogger.d.ts.map +1 -1
  192. package/dist/messaging/shared/MessageLogger.js +1 -0
  193. package/dist/messaging/shared/MessageLogger.js.map +1 -1
  194. package/dist/messaging/shared/PrivacyConsent.d.ts.map +1 -1
  195. package/dist/messaging/shared/PrivacyConsent.js +1 -0
  196. package/dist/messaging/shared/PrivacyConsent.js.map +1 -1
  197. package/dist/messaging/shared/SessionChannelRegistry.d.ts.map +1 -1
  198. package/dist/messaging/shared/SessionChannelRegistry.js +1 -0
  199. package/dist/messaging/shared/SessionChannelRegistry.js.map +1 -1
  200. package/dist/moltbridge/ProfileCompiler.d.ts.map +1 -1
  201. package/dist/moltbridge/ProfileCompiler.js +3 -0
  202. package/dist/moltbridge/ProfileCompiler.js.map +1 -1
  203. package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
  204. package/dist/monitoring/CommitmentTracker.js +1 -0
  205. package/dist/monitoring/CommitmentTracker.js.map +1 -1
  206. package/dist/monitoring/CredentialProvider.d.ts.map +1 -1
  207. package/dist/monitoring/CredentialProvider.js +1 -0
  208. package/dist/monitoring/CredentialProvider.js.map +1 -1
  209. package/dist/monitoring/HealthChecker.d.ts.map +1 -1
  210. package/dist/monitoring/HealthChecker.js +1 -0
  211. package/dist/monitoring/HealthChecker.js.map +1 -1
  212. package/dist/monitoring/HookEventReceiver.d.ts.map +1 -1
  213. package/dist/monitoring/HookEventReceiver.js +1 -0
  214. package/dist/monitoring/HookEventReceiver.js.map +1 -1
  215. package/dist/monitoring/InstructionsVerifier.d.ts.map +1 -1
  216. package/dist/monitoring/InstructionsVerifier.js +1 -0
  217. package/dist/monitoring/InstructionsVerifier.js.map +1 -1
  218. package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
  219. package/dist/monitoring/PresenceProxy.js +4 -0
  220. package/dist/monitoring/PresenceProxy.js.map +1 -1
  221. package/dist/monitoring/QuotaTracker.d.ts.map +1 -1
  222. package/dist/monitoring/QuotaTracker.js +1 -0
  223. package/dist/monitoring/QuotaTracker.js.map +1 -1
  224. package/dist/monitoring/SessionMigrator.d.ts.map +1 -1
  225. package/dist/monitoring/SessionMigrator.js +1 -0
  226. package/dist/monitoring/SessionMigrator.js.map +1 -1
  227. package/dist/monitoring/SessionRecovery.d.ts.map +1 -1
  228. package/dist/monitoring/SessionRecovery.js +1 -0
  229. package/dist/monitoring/SessionRecovery.js.map +1 -1
  230. package/dist/monitoring/TelemetryAuth.d.ts.map +1 -1
  231. package/dist/monitoring/TelemetryAuth.js +2 -0
  232. package/dist/monitoring/TelemetryAuth.js.map +1 -1
  233. package/dist/monitoring/TriageOrchestrator.d.ts.map +1 -1
  234. package/dist/monitoring/TriageOrchestrator.js +2 -0
  235. package/dist/monitoring/TriageOrchestrator.js.map +1 -1
  236. package/dist/monitoring/WorktreeReaper.d.ts.map +1 -1
  237. package/dist/monitoring/WorktreeReaper.js +3 -0
  238. package/dist/monitoring/WorktreeReaper.js.map +1 -1
  239. package/dist/monitoring/probes/PlatformProbe.d.ts.map +1 -1
  240. package/dist/monitoring/probes/PlatformProbe.js +2 -0
  241. package/dist/monitoring/probes/PlatformProbe.js.map +1 -1
  242. package/dist/paste/PasteManager.d.ts.map +1 -1
  243. package/dist/paste/PasteManager.js +4 -0
  244. package/dist/paste/PasteManager.js.map +1 -1
  245. package/dist/publishing/PrivateViewer.d.ts.map +1 -1
  246. package/dist/publishing/PrivateViewer.js +1 -0
  247. package/dist/publishing/PrivateViewer.js.map +1 -1
  248. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  249. package/dist/scheduler/JobScheduler.js +1 -0
  250. package/dist/scheduler/JobScheduler.js.map +1 -1
  251. package/dist/server/routes.d.ts.map +1 -1
  252. package/dist/server/routes.js +21 -9
  253. package/dist/server/routes.js.map +1 -1
  254. package/dist/threadline/AgentDiscovery.d.ts.map +1 -1
  255. package/dist/threadline/AgentDiscovery.js +1 -0
  256. package/dist/threadline/AgentDiscovery.js.map +1 -1
  257. package/dist/threadline/AgentTrustManager.d.ts.map +1 -1
  258. package/dist/threadline/AgentTrustManager.js +1 -0
  259. package/dist/threadline/AgentTrustManager.js.map +1 -1
  260. package/dist/threadline/CircuitBreaker.d.ts.map +1 -1
  261. package/dist/threadline/CircuitBreaker.js +1 -0
  262. package/dist/threadline/CircuitBreaker.js.map +1 -1
  263. package/dist/threadline/ComputeMeter.d.ts.map +1 -1
  264. package/dist/threadline/ComputeMeter.js +1 -0
  265. package/dist/threadline/ComputeMeter.js.map +1 -1
  266. package/dist/threadline/ContextThreadMap.d.ts.map +1 -1
  267. package/dist/threadline/ContextThreadMap.js +1 -0
  268. package/dist/threadline/ContextThreadMap.js.map +1 -1
  269. package/dist/threadline/InvitationManager.d.ts.map +1 -1
  270. package/dist/threadline/InvitationManager.js +1 -0
  271. package/dist/threadline/InvitationManager.js.map +1 -1
  272. package/dist/threadline/MCPAuth.d.ts.map +1 -1
  273. package/dist/threadline/MCPAuth.js +1 -0
  274. package/dist/threadline/MCPAuth.js.map +1 -1
  275. package/dist/threadline/PipeSessionSpawner.d.ts.map +1 -1
  276. package/dist/threadline/PipeSessionSpawner.js +2 -0
  277. package/dist/threadline/PipeSessionSpawner.js.map +1 -1
  278. package/dist/threadline/RateLimiter.d.ts.map +1 -1
  279. package/dist/threadline/RateLimiter.js +1 -0
  280. package/dist/threadline/RateLimiter.js.map +1 -1
  281. package/dist/threadline/SessionLifecycle.d.ts.map +1 -1
  282. package/dist/threadline/SessionLifecycle.js +1 -0
  283. package/dist/threadline/SessionLifecycle.js.map +1 -1
  284. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  285. package/dist/threadline/ThreadlineBootstrap.js +1 -0
  286. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  287. package/dist/threadline/WakeSocketServer.d.ts.map +1 -1
  288. package/dist/threadline/WakeSocketServer.js +2 -0
  289. package/dist/threadline/WakeSocketServer.js.map +1 -1
  290. package/dist/threadline/listener-daemon.d.ts.map +1 -1
  291. package/dist/threadline/listener-daemon.js +2 -0
  292. package/dist/threadline/listener-daemon.js.map +1 -1
  293. package/dist/users/UserManager.d.ts.map +1 -1
  294. package/dist/users/UserManager.js +1 -0
  295. package/dist/users/UserManager.js.map +1 -1
  296. package/dist/users/UserOnboarding.d.ts.map +1 -1
  297. package/dist/users/UserOnboarding.js +1 -0
  298. package/dist/users/UserOnboarding.js.map +1 -1
  299. package/dist/utils/jsonl-rotation.d.ts.map +1 -1
  300. package/dist/utils/jsonl-rotation.js +1 -0
  301. package/dist/utils/jsonl-rotation.js.map +1 -1
  302. package/package.json +4 -2
  303. package/scripts/add-migration-marker.js +121 -0
  304. package/scripts/analyze-release.js +6 -0
  305. package/scripts/check-contract-evidence.js +2 -0
  306. package/scripts/destructive-command-shim.js +1 -0
  307. package/scripts/fix-better-sqlite3.cjs +2 -0
  308. package/scripts/generate-builtin-manifest.cjs +1 -0
  309. package/scripts/instar-dev-precommit.js +2 -0
  310. package/scripts/lint-no-direct-destructive.js +597 -0
  311. package/scripts/migrate-incident-2026-04-17.mjs +1 -0
  312. package/scripts/pre-push-gate.js +24 -0
  313. package/scripts/test-bootstrap-relay.mjs +1 -0
  314. package/scripts/worktree-commit-msg-hook.js +4 -0
  315. package/scripts/worktree-precommit-gate.js +1 -0
  316. package/src/data/builtin-manifest.json +98 -98
  317. package/src/templates/scripts/git-sync-gate.sh +4 -0
  318. package/upgrades/0.28.75.md +29 -0
  319. package/upgrades/0.28.76.md +67 -0
  320. package/upgrades/side-effects/0.28.75.md +53 -0
  321. package/upgrades/side-effects/comprehensive-destructive-tool-containment-foundation.md +74 -0
  322. package/upgrades/side-effects/source-tree-guard.md +340 -0
  323. package/upgrades/side-effects/telegram-lifeline-version-missing-info.md +76 -0
@@ -0,0 +1,597 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-no-direct-destructive.js — refuses direct destructive git/fs callsites.
4
+ *
5
+ * Implements AC-3 / AC-5 / AC-6 / AC-7 from
6
+ * docs/specs/COMPREHENSIVE-DESTRUCTIVE-TOOL-CONTAINMENT-SPEC.md.
7
+ *
8
+ * The funnels (`src/core/SafeGitExecutor.ts`, `src/core/SafeFsExecutor.ts`)
9
+ * are the only modules in the codebase allowed to call:
10
+ * - `child_process.execFileSync('git', ...)` / `execSync('git ...')` /
11
+ * `spawn('git', ...)` / `spawnSync('git', ...)` / `exec('git ...')`
12
+ * - `fs.rm` / `fs.rmSync` / `fs.unlink` / `fs.unlinkSync` / `fs.rmdir` /
13
+ * `fs.rmdirSync` (and their `fs/promises` counterparts)
14
+ * - `simpleGit(...)` from the `simple-git` package
15
+ *
16
+ * This script AST-walks every `.ts`/`.tsx`/`.js`/`.mjs`/`.cjs` file in
17
+ * `src/`, `tests/`, `scripts/` (configurable via CLI args) and flags
18
+ * violations.
19
+ *
20
+ * It also greps `.sh` files and the `scripts` section of `package.json`
21
+ * for direct destructive `git <verb>` invocations (closed verb list).
22
+ *
23
+ * Exit codes:
24
+ * 0 — no violations.
25
+ * 1 — at least one violation.
26
+ *
27
+ * Usage:
28
+ * node scripts/lint-no-direct-destructive.js # full repo
29
+ * node scripts/lint-no-direct-destructive.js --staged # staged files only
30
+ * node scripts/lint-no-direct-destructive.js path1 path2 # specific files
31
+ */
32
+
33
+ import fs from 'node:fs';
34
+ import path from 'node:path';
35
+ import { execSync } from 'node:child_process';
36
+ import { fileURLToPath } from 'node:url';
37
+
38
+ // Avoid importing typescript at module top — it's a heavy dep. We require it
39
+ // only when a TS file actually needs parsing.
40
+ let _ts = null;
41
+ function ts() {
42
+ if (_ts) return _ts;
43
+ _ts = require('typescript');
44
+ return _ts;
45
+ }
46
+ import { createRequire } from 'node:module';
47
+ const require = createRequire(import.meta.url);
48
+
49
+ const __filename = fileURLToPath(import.meta.url);
50
+ const __dirname = path.dirname(__filename);
51
+ const ROOT = path.resolve(__dirname, '..');
52
+
53
+ // ── Allowlist (closed) ─────────────────────────────────────────────
54
+
55
+ /**
56
+ * Files that may legitimately call destructive git/fs primitives directly.
57
+ * Adding entries requires a spec change.
58
+ */
59
+ const ALLOWLIST = new Set([
60
+ 'src/core/SafeGitExecutor.ts',
61
+ 'src/core/SafeFsExecutor.ts',
62
+ 'tests/unit/SafeGitExecutor.test.ts',
63
+ 'tests/unit/SafeFsExecutor.test.ts',
64
+ // Transitional: tracked under commitment://incremental-migration (due
65
+ // 2026-05-03). PR #2 migrates these fs.unlinkSync calls through
66
+ // SafeFsExecutor and removes them from this list.
67
+ 'src/messaging/imessage/IMessageAdapter.ts',
68
+ 'src/messaging/imessage/NativeBackend.ts',
69
+ // The shim runs `git <verb> --dry-run` first to count files, then re-invokes
70
+ // for real. Both invocations route through SafeGitExecutor, but the shim's
71
+ // own implementation file must be allowed to import the executor.
72
+ // The shim itself uses SafeGitExecutor — no direct execFileSync needed. If
73
+ // the shim ever needs direct access it gets added here.
74
+ ]);
75
+
76
+ /**
77
+ * Allow `// safe-git-allow: <reason>` as a per-file escape on the FIRST
78
+ * non-empty line of the file. Used by SafeGitExecutor.ts itself and the
79
+ * test files. Other callers must be on the closed allowlist.
80
+ */
81
+ function hasAllowComment(text) {
82
+ const lines = text.split('\n').slice(0, 5);
83
+ for (const line of lines) {
84
+ if (/^\s*\/\/\s*safe-git-allow:/.test(line)) return true;
85
+ if (/^\s*\/\*[\s\S]*?safe-git-allow:/m.test(line)) return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ // ── Violation reporting ────────────────────────────────────────────
91
+
92
+ const violations = [];
93
+
94
+ /**
95
+ * Per-callsite marker: `// safe-git-allow: incremental-migration` placed on
96
+ * the line immediately above (or at end of) a flagged callsite suppresses
97
+ * that single violation. Used during the transitional period before
98
+ * commitment://incremental-migration lands (PR #2). Expires 2026-05-03.
99
+ *
100
+ * NEW callsites cannot use this marker — only pre-existing callsites that
101
+ * were stamped in PR #1 by scripts/add-migration-marker.js. After PR #2,
102
+ * zero markers should remain.
103
+ */
104
+ const MIGRATION_MARKER_EXPIRY = new Date('2026-05-03T00:00:00Z');
105
+ const MIGRATION_MARKER_RE = /\/\/\s*safe-git-allow:\s*incremental-migration\b/;
106
+
107
+ function hasLineMarker(text, line) {
108
+ // line is 1-based. Check the line itself (trailing comment) and the
109
+ // line immediately above it.
110
+ const lines = text.split('\n');
111
+ const idx = line - 1;
112
+ if (idx < 0 || idx >= lines.length) return false;
113
+ if (MIGRATION_MARKER_RE.test(lines[idx])) return true;
114
+ if (idx - 1 >= 0 && MIGRATION_MARKER_RE.test(lines[idx - 1])) return true;
115
+ // Also allow the marker two lines above to tolerate blank-line spacing.
116
+ if (idx - 2 >= 0 && /^\s*$/.test(lines[idx - 1]) && MIGRATION_MARKER_RE.test(lines[idx - 2])) return true;
117
+ return false;
118
+ }
119
+
120
+ function migrationMarkerExpired() {
121
+ return new Date() >= MIGRATION_MARKER_EXPIRY;
122
+ }
123
+
124
+ function migrationMarkerDisabled() {
125
+ // Internal flag used by scripts/add-migration-marker.js to collect ALL
126
+ // pre-existing violations (including ones that already carry the marker)
127
+ // so it can re-stamp idempotently.
128
+ return process.env.INSTAR_DISABLE_MIGRATION_MARKER === '1';
129
+ }
130
+
131
+ function report(file, line, col, msg, ctx) {
132
+ // Honor per-callsite incremental-migration marker (transitional period).
133
+ if (
134
+ ctx &&
135
+ ctx.text &&
136
+ !migrationMarkerDisabled() &&
137
+ !migrationMarkerExpired() &&
138
+ hasLineMarker(ctx.text, line)
139
+ ) {
140
+ return;
141
+ }
142
+ violations.push({ file, line, col, msg });
143
+ }
144
+
145
+ // ── AST scan: TS / JS files ────────────────────────────────────────
146
+
147
+ const DESTRUCTIVE_FS_NAMES = new Set([
148
+ 'rm',
149
+ 'rmSync',
150
+ 'unlink',
151
+ 'unlinkSync',
152
+ 'rmdir',
153
+ 'rmdirSync',
154
+ ]);
155
+
156
+ const CHILD_PROCESS_FNS = new Set([
157
+ 'execFileSync',
158
+ 'execSync',
159
+ 'spawn',
160
+ 'spawnSync',
161
+ 'exec',
162
+ 'execFile',
163
+ ]);
164
+
165
+ const CHILD_PROCESS_MODULE_NAMES = new Set([
166
+ 'child_process',
167
+ 'node:child_process',
168
+ ]);
169
+
170
+ const FS_MODULE_NAMES = new Set([
171
+ 'fs',
172
+ 'node:fs',
173
+ 'fs/promises',
174
+ 'node:fs/promises',
175
+ ]);
176
+
177
+ /**
178
+ * Walk a TypeScript SourceFile AST, collecting violations.
179
+ *
180
+ * Rules:
181
+ * 1. Direct call to one of CHILD_PROCESS_FNS where the first arg is the
182
+ * string literal 'git' OR a string literal starting with 'git ' — flag.
183
+ * 2. Member access on an identifier known to alias the child_process or
184
+ * fs module — same rule applies.
185
+ * 3. Named import of `simpleGit` from `simple-git` — flag.
186
+ * 4. Named import of one of DESTRUCTIVE_FS_NAMES from fs / fs/promises — flag.
187
+ * 5. Namespace import of fs / fs/promises followed by member-access
188
+ * to a destructive name — flag.
189
+ * 6. require('child_process').execFileSync('git', ...) and equivalents — flag.
190
+ * 7. Dynamic property access used to evade the rule (`fs['rm' + 'Sync']`) — flag.
191
+ */
192
+ function lintTsFile(file, text) {
193
+ const T = ts();
194
+ const sf = T.createSourceFile(file, text, T.ScriptTarget.Latest, true);
195
+ const ctx = { text };
196
+ const r = (f, l, c, m) => report(f, l, c, m, ctx);
197
+
198
+ /** module name → local identifier (default + namespace + named) */
199
+ const childProcessIdentifiers = new Set();
200
+ const childProcessNamedImports = new Map(); // localName -> originalName
201
+ const fsNamespaceIdentifiers = new Set();
202
+ const fsNamedDestructiveImports = new Map(); // localName -> originalName
203
+ const simpleGitImports = []; // {localName}
204
+ const requireBindings = new Map(); // localName -> moduleName (if literal)
205
+
206
+ function lineCol(node) {
207
+ const lc = sf.getLineAndCharacterOfPosition(node.getStart(sf));
208
+ return { line: lc.line + 1, col: lc.character + 1 };
209
+ }
210
+
211
+ function visit(node) {
212
+ // ── Imports ────────────────────────────────────────────────
213
+ if (T.isImportDeclaration(node) && node.moduleSpecifier && T.isStringLiteral(node.moduleSpecifier)) {
214
+ const mod = node.moduleSpecifier.text;
215
+ const ic = node.importClause;
216
+ if (!ic) return;
217
+ if (ic.namedBindings) {
218
+ if (T.isNamespaceImport(ic.namedBindings)) {
219
+ const localName = ic.namedBindings.name.text;
220
+ if (CHILD_PROCESS_MODULE_NAMES.has(mod)) {
221
+ childProcessIdentifiers.add(localName);
222
+ } else if (FS_MODULE_NAMES.has(mod)) {
223
+ fsNamespaceIdentifiers.add(localName);
224
+ }
225
+ } else if (T.isNamedImports(ic.namedBindings)) {
226
+ for (const el of ic.namedBindings.elements) {
227
+ const importedName = el.propertyName ? el.propertyName.text : el.name.text;
228
+ const localName = el.name.text;
229
+ if (CHILD_PROCESS_MODULE_NAMES.has(mod) && CHILD_PROCESS_FNS.has(importedName)) {
230
+ childProcessNamedImports.set(localName, importedName);
231
+ } else if (FS_MODULE_NAMES.has(mod) && DESTRUCTIVE_FS_NAMES.has(importedName)) {
232
+ fsNamedDestructiveImports.set(localName, importedName);
233
+ } else if (mod === 'simple-git' && importedName === 'simpleGit') {
234
+ simpleGitImports.push({ localName });
235
+ }
236
+ }
237
+ }
238
+ }
239
+ // Default import — `import fs from 'node:fs'` style.
240
+ if (ic.name) {
241
+ const localName = ic.name.text;
242
+ if (CHILD_PROCESS_MODULE_NAMES.has(mod)) {
243
+ childProcessIdentifiers.add(localName);
244
+ } else if (FS_MODULE_NAMES.has(mod)) {
245
+ fsNamespaceIdentifiers.add(localName);
246
+ } else if (mod === 'simple-git') {
247
+ simpleGitImports.push({ localName });
248
+ }
249
+ }
250
+ }
251
+
252
+ // ── require('child_process') / require('fs') ───────────────
253
+ if (T.isVariableDeclaration(node) && node.initializer && T.isCallExpression(node.initializer)) {
254
+ const callee = node.initializer.expression;
255
+ if (T.isIdentifier(callee) && callee.text === 'require' && node.initializer.arguments.length === 1) {
256
+ const arg = node.initializer.arguments[0];
257
+ if (T.isStringLiteral(arg)) {
258
+ const mod = arg.text;
259
+ if (T.isIdentifier(node.name)) {
260
+ const localName = node.name.text;
261
+ if (CHILD_PROCESS_MODULE_NAMES.has(mod)) {
262
+ childProcessIdentifiers.add(localName);
263
+ requireBindings.set(localName, mod);
264
+ } else if (FS_MODULE_NAMES.has(mod)) {
265
+ fsNamespaceIdentifiers.add(localName);
266
+ requireBindings.set(localName, mod);
267
+ } else if (mod === 'simple-git') {
268
+ simpleGitImports.push({ localName });
269
+ }
270
+ }
271
+ // Destructured: const { execFileSync } = require('child_process')
272
+ if (T.isObjectBindingPattern(node.name)) {
273
+ for (const el of node.name.elements) {
274
+ const importedName = el.propertyName && T.isIdentifier(el.propertyName)
275
+ ? el.propertyName.text
276
+ : (T.isIdentifier(el.name) ? el.name.text : null);
277
+ const localName = T.isIdentifier(el.name) ? el.name.text : null;
278
+ if (!importedName || !localName) continue;
279
+ if (CHILD_PROCESS_MODULE_NAMES.has(mod) && CHILD_PROCESS_FNS.has(importedName)) {
280
+ childProcessNamedImports.set(localName, importedName);
281
+ } else if (FS_MODULE_NAMES.has(mod) && DESTRUCTIVE_FS_NAMES.has(importedName)) {
282
+ fsNamedDestructiveImports.set(localName, importedName);
283
+ } else if (mod === 'simple-git' && importedName === 'simpleGit') {
284
+ simpleGitImports.push({ localName });
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ // ── Inline require('child_process').execFileSync(...) ──────
293
+ if (T.isCallExpression(node) && T.isPropertyAccessExpression(node.expression)) {
294
+ const obj = node.expression.expression;
295
+ const name = node.expression.name.text;
296
+ if (T.isCallExpression(obj) && T.isIdentifier(obj.expression) && obj.expression.text === 'require'
297
+ && obj.arguments.length === 1 && T.isStringLiteral(obj.arguments[0])) {
298
+ const mod = obj.arguments[0].text;
299
+ if (CHILD_PROCESS_MODULE_NAMES.has(mod) && CHILD_PROCESS_FNS.has(name)) {
300
+ if (firstArgIsGit(node)) {
301
+ const lc = lineCol(node);
302
+ r(file, lc.line, lc.col, `Direct require('${mod}').${name}('git', ...) — use SafeGitExecutor.`);
303
+ }
304
+ } else if (FS_MODULE_NAMES.has(mod) && DESTRUCTIVE_FS_NAMES.has(name)) {
305
+ const lc = lineCol(node);
306
+ r(file, lc.line, lc.col, `Direct require('${mod}').${name}(...) — use SafeFsExecutor.`);
307
+ }
308
+ }
309
+ }
310
+
311
+ // ── Plain CallExpression of an identifier ──────────────────
312
+ if (T.isCallExpression(node) && T.isIdentifier(node.expression)) {
313
+ const fnName = node.expression.text;
314
+ // Named import alias for execFileSync/etc.
315
+ if (childProcessNamedImports.has(fnName)) {
316
+ if (firstArgIsGit(node)) {
317
+ const lc = lineCol(node);
318
+ r(file, lc.line, lc.col, `Direct ${childProcessNamedImports.get(fnName)}('git', ...) — use SafeGitExecutor.`);
319
+ }
320
+ }
321
+ // Named import alias for rm/etc.
322
+ if (fsNamedDestructiveImports.has(fnName)) {
323
+ const lc = lineCol(node);
324
+ r(file, lc.line, lc.col, `Direct ${fsNamedDestructiveImports.get(fnName)}(...) — use SafeFsExecutor.`);
325
+ }
326
+ // simpleGit() call.
327
+ if (simpleGitImports.some((s) => s.localName === fnName)) {
328
+ const lc = lineCol(node);
329
+ r(file, lc.line, lc.col, `Direct simpleGit(...) — use SafeGitExecutor.`);
330
+ }
331
+ }
332
+
333
+ // ── Member call on namespace identifier ────────────────────
334
+ if (T.isCallExpression(node) && T.isPropertyAccessExpression(node.expression)) {
335
+ const obj = node.expression.expression;
336
+ const name = node.expression.name.text;
337
+ if (T.isIdentifier(obj)) {
338
+ const objName = obj.text;
339
+ if (childProcessIdentifiers.has(objName) && CHILD_PROCESS_FNS.has(name)) {
340
+ if (firstArgIsGit(node)) {
341
+ const lc = lineCol(node);
342
+ r(file, lc.line, lc.col, `Direct ${objName}.${name}('git', ...) — use SafeGitExecutor.`);
343
+ }
344
+ }
345
+ if (fsNamespaceIdentifiers.has(objName) && DESTRUCTIVE_FS_NAMES.has(name)) {
346
+ const lc = lineCol(node);
347
+ r(file, lc.line, lc.col, `Direct ${objName}.${name}(...) — use SafeFsExecutor.`);
348
+ }
349
+ // Defense-in-depth: catch fs.promises.rm via fs identifier.
350
+ if (fsNamespaceIdentifiers.has(objName) && name === 'promises') {
351
+ // The next member access in a chained call.
352
+ // Detected via the parent CallExpression where expression is
353
+ // `fs.promises.rm(...)` — the whole chain is this PropertyAccessExpression.
354
+ // We already capture this via the ElementAccessExpression branch below
355
+ // and via deep walking, but most simply: a CallExpression whose
356
+ // callee is `<fs>.promises.<destructive>` deserves a flag.
357
+ }
358
+ }
359
+ // fs.promises.rm(...) — the callee is PropertyAccess(PropertyAccess(fs, promises), rm)
360
+ if (T.isPropertyAccessExpression(obj) && T.isIdentifier(obj.expression)
361
+ && fsNamespaceIdentifiers.has(obj.expression.text)
362
+ && obj.name.text === 'promises'
363
+ && DESTRUCTIVE_FS_NAMES.has(name)) {
364
+ const lc = lineCol(node);
365
+ r(file, lc.line, lc.col, `Direct ${obj.expression.text}.promises.${name}(...) — use SafeFsExecutor.`);
366
+ }
367
+ }
368
+
369
+ // ── Dynamic / computed access: fs['rm' + 'Sync'](...) ──────
370
+ if (T.isCallExpression(node) && T.isElementAccessExpression(node.expression)) {
371
+ const obj = node.expression.expression;
372
+ const arg = node.expression.argumentExpression;
373
+ if (T.isIdentifier(obj) && fsNamespaceIdentifiers.has(obj.text)) {
374
+ // Any computed member access on the fs namespace is suspicious;
375
+ // refuse with a clear message.
376
+ const lc = lineCol(node);
377
+ r(file, lc.line, lc.col, `Computed member access on fs (${obj.text}[...]) — refuse, use SafeFsExecutor.`);
378
+ }
379
+ if (T.isIdentifier(obj) && childProcessIdentifiers.has(obj.text)) {
380
+ const lc = lineCol(node);
381
+ r(file, lc.line, lc.col, `Computed member access on child_process (${obj.text}[...]) — refuse, use SafeGitExecutor.`);
382
+ }
383
+ }
384
+
385
+ T.forEachChild(node, visit);
386
+ }
387
+
388
+ function firstArgIsGit(call) {
389
+ const args = call.arguments;
390
+ if (args.length === 0) return false;
391
+ const first = args[0];
392
+ const T = ts();
393
+ if (T.isStringLiteral(first)) {
394
+ if (first.text === 'git') return true;
395
+ if (first.text.startsWith('git ')) return true;
396
+ }
397
+ if (T.isTemplateExpression(first) || T.isNoSubstitutionTemplateLiteral(first)) {
398
+ // Best-effort: check the head.
399
+ const text = first.getText(sf);
400
+ if (/^[`'"]git[\s'"]/.test(text)) return true;
401
+ }
402
+ return false;
403
+ }
404
+
405
+ visit(sf);
406
+ }
407
+
408
+ // ── Shell + package.json grep ──────────────────────────────────────
409
+
410
+ const DESTRUCTIVE_GIT_VERBS = [
411
+ 'add', 'am', 'apply', 'branch', 'checkout', 'cherry-pick',
412
+ 'clean', 'clone', 'commit', 'fetch', 'gc', 'init', 'merge',
413
+ 'mv', 'pull', 'push', 'rebase', 'reset', 'restore', 'revert',
414
+ 'rm', 'stash', 'submodule', 'switch', 'tag', 'update-ref',
415
+ 'worktree', 'prune', 'notes', 'replace', 'filter-branch',
416
+ ];
417
+ const SHELL_GIT_REGEX = new RegExp(
418
+ String.raw`(?:^|[\s;&|()])git\s+(?:-C\s+\S+\s+|-c\s+\S+\s+)*(${DESTRUCTIVE_GIT_VERBS.join('|')})\b`,
419
+ 'g',
420
+ );
421
+
422
+ const SHELL_ALLOWLIST = new Set([
423
+ 'scripts/setup-imessage-hardlink.sh',
424
+ // Transitional: pre-existing template script with destructive git verbs.
425
+ // Tracked under commitment://incremental-migration (due 2026-05-03). PR #2
426
+ // ports this to a Node script that uses SafeGitExecutor and removes this
427
+ // entry.
428
+ 'src/templates/scripts/git-sync-gate.sh',
429
+ ]);
430
+
431
+ function lintShellFile(file, text) {
432
+ const rel = path.relative(ROOT, path.resolve(file));
433
+ if (SHELL_ALLOWLIST.has(rel)) return;
434
+ const lines = text.split('\n');
435
+ for (let i = 0; i < lines.length; i++) {
436
+ const line = lines[i];
437
+ // Skip comments
438
+ const stripped = line.replace(/^\s*#.*$/, '');
439
+ if (!stripped) continue;
440
+ SHELL_GIT_REGEX.lastIndex = 0;
441
+ const m = SHELL_GIT_REGEX.exec(stripped);
442
+ if (m) {
443
+ report(file, i + 1, (m.index || 0) + 1, `Shell script invokes destructive 'git ${m[1]}' — port to a Node script using SafeGitExecutor.`);
444
+ }
445
+ }
446
+ }
447
+
448
+ function lintPackageJsonScripts(file, text) {
449
+ let pkg;
450
+ try {
451
+ pkg = JSON.parse(text);
452
+ } catch {
453
+ return;
454
+ }
455
+ const scripts = pkg.scripts || {};
456
+ for (const [name, cmd] of Object.entries(scripts)) {
457
+ if (typeof cmd !== 'string') continue;
458
+ SHELL_GIT_REGEX.lastIndex = 0;
459
+ const m = SHELL_GIT_REGEX.exec(cmd);
460
+ if (m) {
461
+ report(file, 1, 1, `npm script "${name}" runs destructive 'git ${m[1]}' — refuse.`);
462
+ }
463
+ }
464
+ }
465
+
466
+ // ── File enumeration ───────────────────────────────────────────────
467
+
468
+ const TARGET_DIRS = ['src', 'tests', 'scripts'];
469
+ const TARGET_EXTS = ['.ts', '.tsx', '.js', '.mjs', '.cjs'];
470
+
471
+ function walkDir(dir, out) {
472
+ let entries;
473
+ try {
474
+ entries = fs.readdirSync(dir, { withFileTypes: true });
475
+ } catch {
476
+ return;
477
+ }
478
+ for (const e of entries) {
479
+ if (e.name === 'node_modules' || e.name === '.git' || e.name === 'dist') continue;
480
+ const full = path.join(dir, e.name);
481
+ if (e.isDirectory()) {
482
+ walkDir(full, out);
483
+ } else if (e.isFile()) {
484
+ const ext = path.extname(e.name);
485
+ if (TARGET_EXTS.includes(ext)) out.push(full);
486
+ else if (e.name.endsWith('.sh')) out.push(full);
487
+ else if (e.name === 'package.json') out.push(full);
488
+ }
489
+ }
490
+ }
491
+
492
+ function gatherFilesFromArgs(args) {
493
+ const out = [];
494
+ for (const a of args) {
495
+ const full = path.resolve(a);
496
+ if (!fs.existsSync(full)) continue;
497
+ const st = fs.statSync(full);
498
+ if (st.isDirectory()) walkDir(full, out);
499
+ else out.push(full);
500
+ }
501
+ return out;
502
+ }
503
+
504
+ function gatherStagedFiles() {
505
+ // safe-git-allow: incremental-migration
506
+ const stdout = execSync('git diff --cached --name-only --diff-filter=ACMR', {
507
+ cwd: ROOT,
508
+ encoding: 'utf8',
509
+ });
510
+ return stdout
511
+ .split('\n')
512
+ .map((s) => s.trim())
513
+ .filter(Boolean)
514
+ .map((rel) => path.resolve(ROOT, rel))
515
+ .filter((full) => {
516
+ try {
517
+ return fs.statSync(full).isFile();
518
+ } catch {
519
+ return false;
520
+ }
521
+ });
522
+ }
523
+
524
+ function gatherAll() {
525
+ const out = [];
526
+ for (const d of TARGET_DIRS) {
527
+ walkDir(path.join(ROOT, d), out);
528
+ }
529
+ // Also include package.json at root.
530
+ out.push(path.join(ROOT, 'package.json'));
531
+ return out;
532
+ }
533
+
534
+ // ── Main ───────────────────────────────────────────────────────────
535
+
536
+ function main() {
537
+ const argv = process.argv.slice(2);
538
+ let files;
539
+ if (argv.includes('--staged')) {
540
+ files = gatherStagedFiles();
541
+ } else if (argv.length > 0) {
542
+ files = gatherFilesFromArgs(argv);
543
+ } else {
544
+ files = gatherAll();
545
+ }
546
+
547
+ for (const file of files) {
548
+ let text;
549
+ try {
550
+ text = fs.readFileSync(file, 'utf8');
551
+ } catch {
552
+ continue;
553
+ }
554
+
555
+ const rel = path.relative(ROOT, file);
556
+ // package.json scripts grep
557
+ if (path.basename(file) === 'package.json' && rel === 'package.json') {
558
+ lintPackageJsonScripts(file, text);
559
+ continue;
560
+ }
561
+ // Shell grep
562
+ if (file.endsWith('.sh')) {
563
+ lintShellFile(file, text);
564
+ continue;
565
+ }
566
+ // Allowlist check (closed)
567
+ if (ALLOWLIST.has(rel)) continue;
568
+ if (hasAllowComment(text)) continue;
569
+
570
+ // AST lint for ts/js/mjs/cjs
571
+ try {
572
+ lintTsFile(file, text);
573
+ } catch (err) {
574
+ // Parse failure → emit a soft warning, not a violation.
575
+ process.stderr.write(`[lint-no-direct-destructive] failed to parse ${rel}: ${err.message}\n`);
576
+ }
577
+ }
578
+
579
+ if (violations.length === 0) {
580
+ return 0;
581
+ }
582
+ process.stderr.write('\n');
583
+ process.stderr.write('╔════════════════════════════════════════════════════════════════════╗\n');
584
+ process.stderr.write('║ lint-no-direct-destructive — VIOLATIONS ║\n');
585
+ process.stderr.write('╚════════════════════════════════════════════════════════════════════╝\n');
586
+ process.stderr.write('\n');
587
+ for (const v of violations) {
588
+ const rel = path.relative(ROOT, v.file);
589
+ process.stderr.write(` ${rel}:${v.line}:${v.col}\n ${v.msg}\n\n`);
590
+ }
591
+ process.stderr.write(`Total: ${violations.length} violation(s).\n`);
592
+ process.stderr.write('See docs/specs/COMPREHENSIVE-DESTRUCTIVE-TOOL-CONTAINMENT-SPEC.md for guidance.\n');
593
+ return 1;
594
+ }
595
+
596
+ const code = main();
597
+ process.exit(code);
@@ -54,6 +54,7 @@ function generateKeypair() {
54
54
  function verifyStash() {
55
55
  let stashList;
56
56
  try {
57
+ // safe-git-allow: incremental-migration
57
58
  stashList = execSync('git stash list', { encoding: 'utf-8' });
58
59
  } catch (err) {
59
60
  logStep(`No stash present (or not a git repo). Skipping stash verification.`);
@@ -271,6 +271,30 @@ if (!process.env.CI) {
271
271
  }
272
272
  }
273
273
 
274
+ // ── Destructive-tool containment lint (full repo) ─────────────────────
275
+ //
276
+ // Runs the lint-no-direct-destructive AST scanner across the whole repo on
277
+ // every push. Pre-commit only runs it over staged files; pre-push catches
278
+ // commits that landed before the rule existed (or before the marker scheme
279
+ // expired). Fails the push on any violation.
280
+ //
281
+ // Wired here rather than in .husky/pre-push because the husky hook files
282
+ // are managed by a sandboxed flow that this gate can extend.
283
+
284
+ try {
285
+ const { spawnSync } = await import('node:child_process');
286
+ const result = spawnSync(
287
+ process.execPath,
288
+ [path.join(ROOT, 'scripts/lint-no-direct-destructive.js')],
289
+ { cwd: ROOT, stdio: ['ignore', 'inherit', 'inherit'] },
290
+ );
291
+ if (result.status !== 0) {
292
+ errors.push('lint-no-direct-destructive: violations detected (see output above)');
293
+ }
294
+ } catch (err) {
295
+ warnings.push(`lint-no-direct-destructive failed to run: ${err.message}`);
296
+ }
297
+
274
298
  // ── Report ────────────────────────────────────────────────────────────
275
299
 
276
300
  if (errors.length > 0 || warnings.length > 0) {
@@ -85,6 +85,7 @@ try {
85
85
  } finally {
86
86
  // Cleanup
87
87
  try {
88
+ // safe-git-allow: incremental-migration
88
89
  fs.rmSync(tmpDir, { recursive: true, force: true });
89
90
  } catch {}
90
91
  }
@@ -38,11 +38,13 @@ function findSessionContext(startCwd) {
38
38
 
39
39
  function gitParents(cwd) {
40
40
  // Determine if this is a merge: presence of $GIT_REFLOG_ACTION=merge OR MERGE_HEAD file
41
+ // safe-git-allow: incremental-migration
41
42
  const gitDir = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], { encoding: 'utf-8' }).trim();
42
43
  const mergeHeadPath = path.join(cwd, gitDir, 'MERGE_HEAD');
43
44
  let parents = [];
44
45
  // Primary parent = current HEAD (or 0...0 for initial commit)
45
46
  try {
47
+ // safe-git-allow: incremental-migration
46
48
  const head = execFileSync('git', ['-C', cwd, 'rev-parse', 'HEAD'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
47
49
  parents.push(head);
48
50
  } catch {
@@ -58,6 +60,7 @@ function gitParents(cwd) {
58
60
  function gitWriteTree(cwd) {
59
61
  // K-fix: honor $GIT_INDEX_FILE (set by `git commit -a` and `git commit <file>`)
60
62
  const env = { ...process.env };
63
+ // safe-git-allow: incremental-migration
61
64
  return execFileSync('git', ['-C', cwd, 'write-tree', '--missing-ok'], {
62
65
  encoding: 'utf-8', env,
63
66
  }).trim();
@@ -151,6 +154,7 @@ async function main() {
151
154
  trailerArgs.push('--trailer', t);
152
155
  }
153
156
  try {
157
+ // safe-git-allow: incremental-migration
154
158
  execFileSync('git', ['interpret-trailers', '--in-place', ...trailerArgs, commitMsgFile], {
155
159
  stdio: 'inherit',
156
160
  });
@@ -94,6 +94,7 @@ async function main() {
94
94
  // Collect staged files (from `git diff --cached --name-only`)
95
95
  let stagedFiles = [];
96
96
  try {
97
+ // safe-git-allow: incremental-migration
97
98
  stagedFiles = execSync('git diff --cached --name-only -z', { encoding: 'utf-8', cwd })
98
99
  .split('\0').filter(Boolean);
99
100
  } catch { /* @silent-fallback-ok */ }