opencode-agent-skill 14.4.0 → 15.0.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 (44) hide show
  1. package/README.md +33 -15
  2. package/docs/PI-COMPAT.md +151 -2
  3. package/docs/V15-MANAGED-RUNTIME.md +128 -0
  4. package/global-config/agents/architect.md +63 -1
  5. package/global-config/agents/executor.md +7 -0
  6. package/global-config/agents/plan-checker.md +8 -1
  7. package/global-config/agents/verifier.md +3 -3
  8. package/global-config/skills/database-engineering/references/workflow.md +15 -0
  9. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +21 -11
  10. package/lib/activity-deadline.mjs +70 -0
  11. package/lib/adaptive-context-budget.mjs +25 -5
  12. package/lib/affected-tests.mjs +1 -1
  13. package/lib/capability-fabric.mjs +17 -0
  14. package/lib/completion-auditor.mjs +25 -5
  15. package/lib/evidence-store.mjs +92 -8
  16. package/lib/execution-contract.mjs +579 -0
  17. package/lib/leaf-runtime-optimizer.mjs +76 -0
  18. package/lib/pi-child-invocation.mjs +65 -0
  19. package/lib/pi-rpc-pool.mjs +61 -9
  20. package/lib/plan-salvage.mjs +124 -0
  21. package/lib/planning-speed-policy.mjs +131 -0
  22. package/lib/repo-graph.mjs +1 -1
  23. package/lib/repo-inspect.mjs +1 -0
  24. package/lib/runtime-artifacts.mjs +42 -0
  25. package/lib/runtime-events.mjs +33 -1
  26. package/lib/safety.mjs +1 -0
  27. package/lib/semantic-index.mjs +1 -1
  28. package/lib/service-manager.mjs +526 -0
  29. package/lib/session-display.mjs +49 -0
  30. package/lib/task-graph.mjs +87 -0
  31. package/lib/task-policy.mjs +150 -4
  32. package/lib/trajectory.mjs +111 -1
  33. package/lib/turbo-fast-path.mjs +44 -0
  34. package/lib/workspace-fingerprint.mjs +2 -11
  35. package/lib/workspace-root.mjs +49 -0
  36. package/lib/worktree-sandbox.mjs +234 -11
  37. package/package.json +8 -5
  38. package/pi/extensions/ues-child-runtime.ts +145 -2
  39. package/pi/extensions/ues.ts +1760 -160
  40. package/scripts/check-release-consistency.mjs +25 -2
  41. package/scripts/check-source-integrity.mjs +471 -4
  42. package/scripts/eval-pi.mjs +76 -36
  43. package/scripts/smoke-pi-extension.mjs +1 -1
  44. package/pi/prompts/ues-run.md +0 -17
@@ -0,0 +1,65 @@
1
+ import { existsSync } from "node:fs"
2
+ import path from "node:path"
3
+ import { resolveManagedPiCommand, resolveWindowsCommand } from "./windows-shim.mjs"
4
+
5
+ export function looksLikePiCliEntrypoint(file) {
6
+ const value = String(file || "").replaceAll("\\", "/").toLowerCase()
7
+ if (!value) return false
8
+
9
+ // Managed/npm Pi installs execute the package CLI script. ACP adapters may
10
+ // also be Node scripts, so do not trust an arbitrary existing argv[1].
11
+ if (value.includes("/@earendil-works/pi-coding-agent/")) return true
12
+
13
+ const base = path.posix.basename(value)
14
+ return /^(?:pi)(?:\.[cm]?js)?$/.test(base)
15
+ }
16
+
17
+ function fromResolvedPi(resolved, args, source) {
18
+ if (!resolved?.executable) return null
19
+ return {
20
+ command: resolved.executable,
21
+ args: [...(resolved.argsPrefix || []), ...args],
22
+ source,
23
+ }
24
+ }
25
+
26
+ export function resolvePiChildInvocation(args = [], options = {}) {
27
+ const currentScript = options.currentScript ?? process.argv[1]
28
+ const execPath = options.execPath ?? process.execPath
29
+ const platform = options.platform ?? process.platform
30
+ const exists = options.existsSync ?? existsSync
31
+
32
+ if (
33
+ currentScript &&
34
+ !String(currentScript).startsWith("/$bunfs/root/") &&
35
+ exists(currentScript) &&
36
+ looksLikePiCliEntrypoint(currentScript)
37
+ ) {
38
+ return {
39
+ command: execPath,
40
+ args: [currentScript, ...args],
41
+ source: "host-pi-cli",
42
+ }
43
+ }
44
+
45
+ const execBase = path.basename(String(execPath || "")).toLowerCase()
46
+ if (/^pi(?:\.exe)?$/.test(execBase)) {
47
+ return { command: execPath, args, source: "host-pi-executable" }
48
+ }
49
+
50
+ if (platform === "win32") {
51
+ const managed = options.managedPi === undefined
52
+ ? resolveManagedPiCommand()
53
+ : options.managedPi
54
+ const managedInvocation = fromResolvedPi(managed, args, "managed-pi")
55
+ if (managedInvocation) return managedInvocation
56
+
57
+ const pathPi = options.pathPi === undefined
58
+ ? resolveWindowsCommand("pi")
59
+ : options.pathPi
60
+ const pathInvocation = fromResolvedPi(pathPi, args, "path-pi")
61
+ if (pathInvocation) return pathInvocation
62
+ }
63
+
64
+ return { command: "pi", args, source: "path-pi-fallback" }
65
+ }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process"
2
2
  import { terminateProcessTree } from "./process-supervisor.mjs"
3
+ import { createAdaptiveDeadline } from "./activity-deadline.mjs"
3
4
 
4
5
  function clamp(value, fallback, min, max) {
5
6
  const parsed = Number(value)
@@ -226,11 +227,37 @@ class RpcWorker {
226
227
  this.stderr = ""
227
228
 
228
229
  const hardTimeoutMs = clamp(options.hardTimeoutMs, 30 * 60_000, 30_000, 2 * 60 * 60_000)
230
+ const absoluteHardTimeoutMs = clamp(
231
+ options.absoluteHardTimeoutMs,
232
+ hardTimeoutMs,
233
+ hardTimeoutMs,
234
+ 2 * 60 * 60_000,
235
+ )
236
+ const activityExtensionMs = clamp(
237
+ options.activityExtensionMs,
238
+ 0,
239
+ 0,
240
+ Math.max(hardTimeoutMs, absoluteHardTimeoutMs),
241
+ )
242
+ const activityWindowMs = clamp(
243
+ options.activityWindowMs,
244
+ Math.max(5_000, Math.min(activityExtensionMs || hardTimeoutMs, 30_000)),
245
+ 1_000,
246
+ Math.max(hardTimeoutMs, absoluteHardTimeoutMs),
247
+ )
229
248
  const idleTimeoutMs = clamp(options.idleTimeoutMs, 5 * 60_000, 10_000, 30 * 60_000)
230
- let lastActivityAt = Date.now()
249
+ const runStartedAt = Date.now()
250
+ let lastActivityAt = runStartedAt
251
+ const adaptiveDeadline = createAdaptiveDeadline({
252
+ hardTimeoutMs,
253
+ absoluteHardTimeoutMs,
254
+ activityExtensionMs,
255
+ activityWindowMs,
256
+ }, runStartedAt)
231
257
  this.lastActivityAt = lastActivityAt
232
258
  this.active = true
233
259
  let finalMessage = null
260
+ let partialAssistantMessage = null
234
261
  let toolCalls = 0
235
262
  let lastToolErrorAt = 0
236
263
  const toolNames = new Set()
@@ -240,19 +267,37 @@ class RpcWorker {
240
267
  settledResolve = resolve
241
268
  settledReject = reject
242
269
  })
270
+ // The active run can be aborted while the prompt transport is still
271
+ // completing, before execution reaches "await settled". Attach a handler
272
+ // immediately so Node never reports a transient unhandled rejection;
273
+ // awaiting the original promise below still preserves the rejection.
274
+ settled.catch(() => {})
243
275
 
244
276
  let aborting = false
245
277
  const abortFor = async (reason) => {
246
278
  if (aborting) return
247
279
  aborting = true
280
+ const error = new Error("UES RPC " + reason)
281
+ error.partialMessage = partialAssistantMessage || finalMessage
282
+ error.stderr = this.stderr
283
+ error.toolCalls = toolCalls
284
+ error.toolNames = [...toolNames]
285
+ error.deadlineExtensions = adaptiveDeadline.extensions
286
+ error.deadlineAt = adaptiveDeadline.deadlineAt
287
+ error.absoluteDeadlineAt = adaptiveDeadline.absoluteAt
288
+ // Settle with the rich timeout error before transport teardown can emit a
289
+ // generic worker-exit error and win the race.
290
+ settledReject(error)
248
291
  try { await this.abortTransport() } catch {}
249
- settledReject(new Error("UES RPC " + reason))
250
292
  }
251
293
  this.activeAbort = abortFor
252
294
 
253
295
  const unsubscribe = this.onEvent((event) => {
254
296
  lastActivityAt = Date.now()
255
297
  this.lastActivityAt = lastActivityAt
298
+ if (event?.message?.role === "assistant") {
299
+ partialAssistantMessage = event.message
300
+ }
256
301
  if (event.type === "tool_execution_start") {
257
302
  toolCalls += 1
258
303
  lastToolErrorAt = 0
@@ -272,22 +317,27 @@ class RpcWorker {
272
317
  if (event.type === "ues_rpc_worker_error") settledReject(new Error(event.error || "RPC worker failed"))
273
318
  })
274
319
 
275
- const hardTimer = setTimeout(() => void abortFor("hard-timeout"), hardTimeoutMs)
276
- hardTimer.unref?.()
277
320
  const postToolErrorIdleTimeoutMs = clamp(
278
321
  options.postToolErrorIdleTimeoutMs,
279
322
  60_000,
280
323
  5_000,
281
324
  idleTimeoutMs,
282
325
  )
283
- const idleTimer = setInterval(() => {
326
+ const watchdogTimer = setInterval(() => {
284
327
  const now = Date.now()
285
- const threshold = lastToolErrorAt > 0 ? Math.min(idleTimeoutMs, postToolErrorIdleTimeoutMs) : idleTimeoutMs
328
+ const threshold = lastToolErrorAt > 0
329
+ ? Math.min(idleTimeoutMs, postToolErrorIdleTimeoutMs)
330
+ : idleTimeoutMs
286
331
  if (now - lastActivityAt >= threshold) {
287
332
  void abortFor(lastToolErrorAt > 0 ? "post-tool-error-stall" : "idle-timeout")
333
+ return
334
+ }
335
+ const deadline = adaptiveDeadline.shouldAbort(now, lastActivityAt)
336
+ if (deadline.abort) {
337
+ void abortFor(deadline.reason || "hard-timeout")
288
338
  }
289
339
  }, 1000)
290
- idleTimer.unref?.()
340
+ watchdogTimer.unref?.()
291
341
 
292
342
  const signal = options.signal
293
343
  const onAbort = () => void abortFor("aborted")
@@ -304,6 +354,9 @@ class RpcWorker {
304
354
  stderr: this.stderr,
305
355
  toolCalls,
306
356
  toolNames: [...toolNames],
357
+ deadlineExtensions: adaptiveDeadline.extensions,
358
+ deadlineAt: adaptiveDeadline.deadlineAt,
359
+ absoluteDeadlineAt: adaptiveDeadline.absoluteAt,
307
360
  }
308
361
  } catch (error) {
309
362
  if (error && typeof error === "object") error.uesRpcPhase = "runtime"
@@ -311,8 +364,7 @@ class RpcWorker {
311
364
  } finally {
312
365
  this.active = false
313
366
  this.activeAbort = null
314
- clearTimeout(hardTimer)
315
- clearInterval(idleTimer)
367
+ clearInterval(watchdogTimer)
316
368
  unsubscribe()
317
369
  if (signal) signal.removeEventListener("abort", onAbort)
318
370
  }
@@ -0,0 +1,124 @@
1
+ import {
2
+ normalizePlanForValidation,
3
+ validatePlan,
4
+ } from "./task-graph.mjs"
5
+
6
+ function balancedObject(source, start) {
7
+ let depth = 0
8
+ let inString = false
9
+ let escaped = false
10
+ for (let index = start; index < source.length; index++) {
11
+ const char = source[index]
12
+ if (inString) {
13
+ if (escaped) escaped = false
14
+ else if (char === "\\") escaped = true
15
+ else if (char === "\"") inString = false
16
+ continue
17
+ }
18
+ if (char === "\"") {
19
+ inString = true
20
+ continue
21
+ }
22
+ if (char === "{") depth += 1
23
+ else if (char === "}") {
24
+ depth -= 1
25
+ if (depth === 0) {
26
+ const raw = source.slice(start, index + 1)
27
+ try {
28
+ return { value: JSON.parse(raw), raw, end: index + 1 }
29
+ } catch {
30
+ return null
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return null
36
+ }
37
+
38
+ function markedCandidate(source, marker) {
39
+ const markerIndex = source.lastIndexOf(marker)
40
+ if (markerIndex < 0) return null
41
+ const start = source.indexOf("{", markerIndex + marker.length)
42
+ if (start < 0) return null
43
+ const parsed = balancedObject(source, start)
44
+ return parsed ? { ...parsed, source: "marked" } : null
45
+ }
46
+
47
+ function fencedCandidates(source) {
48
+ const results = []
49
+ const fence = String.fromCharCode(96, 96, 96)
50
+ let offset = 0
51
+ while (offset < source.length) {
52
+ const open = source.indexOf(fence, offset)
53
+ if (open < 0) break
54
+ const bodyStart = source.indexOf("\n", open + fence.length)
55
+ if (bodyStart < 0) break
56
+ const close = source.indexOf(fence, bodyStart + 1)
57
+ if (close < 0) break
58
+ const body = source.slice(bodyStart + 1, close).trim()
59
+ if (body.startsWith("{")) {
60
+ try { results.push({ value: JSON.parse(body), raw: body, source: "fenced" }) } catch {}
61
+ }
62
+ offset = close + fence.length
63
+ }
64
+ return results
65
+ }
66
+
67
+ function balancedCandidates(source, limit = 24) {
68
+ const starts = []
69
+ for (let i = source.length - 1; i >= 0 && starts.length < limit; i--) {
70
+ if (source[i] === "{") starts.push(i)
71
+ }
72
+ const results = []
73
+ for (const start of starts) {
74
+ const parsed = balancedObject(source, start)
75
+ if (parsed) results.push({ ...parsed, source: "unmarked-balanced" })
76
+ }
77
+ return results
78
+ }
79
+
80
+ function planLike(value) {
81
+ return Boolean(
82
+ value &&
83
+ typeof value === "object" &&
84
+ Array.isArray(value.tasks) &&
85
+ (typeof value.goal === "string" || value.schemaVersion === 1)
86
+ )
87
+ }
88
+
89
+ export function extractValidatedPlan(output, options = {}) {
90
+ const source = String(output || "")
91
+ const marker = String(options.marker || "UES_PLAN_JSON:")
92
+ const candidates = []
93
+ const marked = markedCandidate(source, marker)
94
+ if (marked) candidates.push(marked)
95
+ candidates.push(...fencedCandidates(source))
96
+ candidates.push(...balancedCandidates(source))
97
+
98
+ const seen = new Set()
99
+ let bestInvalid = null
100
+ for (const candidate of candidates) {
101
+ if (!planLike(candidate.value)) continue
102
+ let normalized
103
+ try { normalized = normalizePlanForValidation(candidate.value) } catch { continue }
104
+ const key = JSON.stringify(normalized)
105
+ if (seen.has(key)) continue
106
+ seen.add(key)
107
+ const validation = validatePlan(normalized)
108
+ const result = {
109
+ plan: normalized,
110
+ validation,
111
+ source: candidate.source,
112
+ salvaged: candidate.source !== "marked",
113
+ }
114
+ if (validation && validation.valid === true) return result
115
+ if (!bestInvalid) bestInvalid = result
116
+ }
117
+
118
+ return bestInvalid || {
119
+ plan: null,
120
+ validation: null,
121
+ source: null,
122
+ salvaged: false,
123
+ }
124
+ }
@@ -0,0 +1,131 @@
1
+ function bounded(value, fallback, min, max) {
2
+ const parsed = Number(value)
3
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback
4
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
5
+ }
6
+
7
+ export function planningRuntimeBudget(role, attempt = 1, options = {}) {
8
+ const normalizedRole = String(role || "").replace(/^ues-/, "")
9
+ const normalizedAttempt = Math.max(1, Math.trunc(Number(attempt) || 1))
10
+
11
+ if (normalizedRole === "architect") {
12
+ const recovery = normalizedAttempt > 1
13
+ const deep = String(options.executionProfile || "") === "deep"
14
+ const largePrompt = Number(options.taskChars || 0) >= 2500
15
+ const initialHard = recovery ? 50_000 : 60_000
16
+ const absoluteHard = recovery
17
+ ? 95_000
18
+ : deep || largePrompt
19
+ ? 150_000
20
+ : 120_000
21
+ return {
22
+ hardTimeoutMs: bounded(
23
+ recovery ? options.architectRecoveryHardTimeoutMs : options.architectHardTimeoutMs,
24
+ initialHard,
25
+ 30_000,
26
+ 5 * 60_000,
27
+ ),
28
+ absoluteHardTimeoutMs: bounded(
29
+ recovery ? options.architectRecoveryAbsoluteHardTimeoutMs : options.architectAbsoluteHardTimeoutMs,
30
+ absoluteHard,
31
+ initialHard,
32
+ 6 * 60_000,
33
+ ),
34
+ activityExtensionMs: bounded(
35
+ recovery ? options.architectRecoveryActivityExtensionMs : options.architectActivityExtensionMs,
36
+ recovery ? 25_000 : 35_000,
37
+ 10_000,
38
+ 90_000,
39
+ ),
40
+ activityWindowMs: bounded(
41
+ options.architectActivityWindowMs,
42
+ 20_000,
43
+ 5_000,
44
+ 60_000,
45
+ ),
46
+ idleTimeoutMs: bounded(
47
+ recovery ? options.architectRecoveryIdleTimeoutMs : options.architectIdleTimeoutMs,
48
+ recovery ? 28_000 : 40_000,
49
+ 10_000,
50
+ 2 * 60_000,
51
+ ),
52
+ softSteerMs: bounded(
53
+ options.architectSoftSteerMs,
54
+ 30_000,
55
+ 15_000,
56
+ 2 * 60_000,
57
+ ),
58
+ maxExplorationTools: bounded(options.architectMaxExplorationTools, 16, 8, 80),
59
+ }
60
+ }
61
+
62
+ if (normalizedRole === "plan-checker") {
63
+ const recovery = normalizedAttempt > 1
64
+ const initialHard = recovery ? 45_000 : 70_000
65
+ return {
66
+ hardTimeoutMs: bounded(
67
+ recovery ? options.planCheckerRecoveryHardTimeoutMs : options.planCheckerHardTimeoutMs,
68
+ initialHard,
69
+ 30_000,
70
+ 5 * 60_000,
71
+ ),
72
+ absoluteHardTimeoutMs: bounded(
73
+ recovery ? options.planCheckerRecoveryAbsoluteHardTimeoutMs : options.planCheckerAbsoluteHardTimeoutMs,
74
+ recovery ? 80_000 : 130_000,
75
+ initialHard,
76
+ 5 * 60_000,
77
+ ),
78
+ activityExtensionMs: bounded(
79
+ recovery ? options.planCheckerRecoveryActivityExtensionMs : options.planCheckerActivityExtensionMs,
80
+ recovery ? 20_000 : 30_000,
81
+ 10_000,
82
+ 60_000,
83
+ ),
84
+ activityWindowMs: bounded(
85
+ options.planCheckerActivityWindowMs,
86
+ 20_000,
87
+ 5_000,
88
+ 60_000,
89
+ ),
90
+ idleTimeoutMs: bounded(
91
+ recovery ? options.planCheckerRecoveryIdleTimeoutMs : options.planCheckerIdleTimeoutMs,
92
+ recovery ? 24_000 : 38_000,
93
+ 10_000,
94
+ 2 * 60_000,
95
+ ),
96
+ softSteerMs: bounded(
97
+ recovery ? options.planCheckerRecoverySoftSteerMs : options.planCheckerSoftSteerMs,
98
+ recovery ? 18_000 : 35_000,
99
+ 10_000,
100
+ 2 * 60_000,
101
+ ),
102
+ maxExplorationTools: bounded(
103
+ recovery ? options.planCheckerRecoveryMaxExplorationTools : options.planCheckerMaxExplorationTools,
104
+ recovery ? 6 : 10,
105
+ 4,
106
+ 40,
107
+ ),
108
+ }
109
+ }
110
+
111
+ return null
112
+ }
113
+
114
+ export function shouldSoftSteerArchitect(progress = {}, budget = null) {
115
+ if (!budget) return false
116
+ const elapsedMs = Number(progress.elapsedMs || 0)
117
+ const toolCalls = Number(progress.toolCalls || 0)
118
+ const idleMs = Number(progress.idleMs || 0)
119
+
120
+ if (budget.softSteerMs && elapsedMs >= budget.softSteerMs) return true
121
+ if (budget.maxExplorationTools && toolCalls >= budget.maxExplorationTools) return true
122
+
123
+ // If the model has gone quiet after doing real exploration, ask it to emit
124
+ // the plan before the hard idle watchdog has to kill the worker.
125
+ return toolCalls > 0 && idleMs >= Math.max(10_000, Math.floor((budget.idleTimeoutMs || 30_000) * 0.65))
126
+ }
127
+
128
+
129
+ export function shouldSoftSteerPlanningRole(progress = {}, budget = null) {
130
+ return shouldSoftSteerArchitect(progress, budget)
131
+ }
@@ -6,7 +6,7 @@ const SKIP = new Set([
6
6
  ".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
7
7
  "Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "obj",
8
8
  ".ues-work", ".ues-cache", ".ues-traces", ".ues-learning", ".ues-dashboard",
9
- ".ues-sandboxes", ".ues-memory", ".ues-evals",
9
+ ".ues-sandboxes", ".ues-memory", ".ues-evals", ".ues-services",
10
10
  ])
11
11
 
12
12
  const SOURCE_EXTENSIONS = new Set([
@@ -6,6 +6,7 @@ import { spawnSync } from "node:child_process"
6
6
  const SKIP_DIRS = new Set([
7
7
  ".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
8
8
  "Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "bin", "obj",
9
+ ".ues-services",
9
10
  ])
10
11
 
11
12
  const TEXT_EXTENSIONS = new Set([
@@ -0,0 +1,42 @@
1
+ export const UES_RUNTIME_DIRS = Object.freeze([
2
+ ".ues-cache",
3
+ ".ues-traces",
4
+ ".ues-work",
5
+ ".ues-learning",
6
+ ".ues-dashboard",
7
+ ".ues-sandboxes",
8
+ ".ues-memory",
9
+ ".ues-evals",
10
+ ".ues-services",
11
+ ])
12
+
13
+ export function normalizeRuntimePath(value) {
14
+ return String(value || "")
15
+ .replaceAll("\\", "/")
16
+ .replace(/^\.\//, "")
17
+ .replace(/^\/+/, "")
18
+ }
19
+
20
+ export function isUesRuntimeArtifactPath(value) {
21
+ const normalized = normalizeRuntimePath(value)
22
+ if (!normalized) return false
23
+ return UES_RUNTIME_DIRS.some(
24
+ (dir) => normalized === dir || normalized.startsWith(dir + "/"),
25
+ )
26
+ }
27
+
28
+ export function sourceFacingPaths(values = []) {
29
+ return [...new Set(
30
+ (values || [])
31
+ .map(normalizeRuntimePath)
32
+ .filter(Boolean)
33
+ .filter((value) => !isUesRuntimeArtifactPath(value)),
34
+ )]
35
+ }
36
+
37
+ export function sourceGitPathspecs() {
38
+ return [
39
+ ".",
40
+ ...UES_RUNTIME_DIRS.map((dir) => ":(exclude)" + dir + "/**"),
41
+ ]
42
+ }
@@ -1,7 +1,39 @@
1
1
  import { randomUUID } from "node:crypto"
2
- import { appendFile, readFile } from "node:fs/promises"
2
+ import { appendFile, readFile, stat, writeFile } from "node:fs/promises"
3
+
4
+ const DEFAULT_MAX_EVENT_FILE_BYTES = 4 * 1024 * 1024
5
+ const DEFAULT_RETAIN_EVENT_BYTES = 2 * 1024 * 1024
6
+
7
+ async function compactRuntimeEventFile(file, options = {}) {
8
+ const maxBytes = Math.max(
9
+ 256 * 1024,
10
+ Math.min(64 * 1024 * 1024, Number(options.maxBytes || process.env.UES_EVENT_LOG_MAX_BYTES || DEFAULT_MAX_EVENT_FILE_BYTES)),
11
+ )
12
+ const retainBytes = Math.max(
13
+ 128 * 1024,
14
+ Math.min(maxBytes, Number(options.retainBytes || DEFAULT_RETAIN_EVENT_BYTES)),
15
+ )
16
+ const info = await stat(file).catch(() => null)
17
+ if (!info?.isFile() || info.size < maxBytes) return false
18
+
19
+ const raw = await readFile(file)
20
+ let kept = raw.subarray(Math.max(0, raw.length - retainBytes))
21
+ const newline = kept.indexOf(0x0a)
22
+ if (newline >= 0 && raw.length > retainBytes) kept = kept.subarray(newline + 1)
23
+ const marker = Buffer.from(JSON.stringify({
24
+ schemaVersion: 1,
25
+ id: randomUUID(),
26
+ type: "runtime-events.compacted",
27
+ at: new Date().toISOString(),
28
+ previousBytes: raw.length,
29
+ retainedBytes: kept.length,
30
+ }) + "\n")
31
+ await writeFile(file, Buffer.concat([marker, kept]))
32
+ return true
33
+ }
3
34
 
4
35
  export async function appendRuntimeEvent(file, type, data = {}) {
36
+ await compactRuntimeEventFile(file).catch(() => false)
5
37
  const event = {
6
38
  schemaVersion: 1,
7
39
  id: randomUUID(),
package/lib/safety.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  const RULES = [
2
2
  { id: "git-force", pattern: /\bgit\s+(?:push\b[^\n]*(?:--force(?:-with-lease)?|(?:^|\s)-f(?:\s|$))|reset\s+--hard|clean\s+-[^\n]*f)/i },
3
+ { id: "git-discard-work", pattern: /\bgit\s+(?:restore\b|checkout\s+--(?:\s|$)|stash(?:\s|$))/i },
3
4
  { id: "history-rewrite", pattern: /\bgit\s+(?:rebase\b|filter-branch\b|filter-repo\b)/i },
4
5
  { id: "publish", pattern: /\b(?:npm\s+publish|pnpm\s+publish|yarn\s+npm\s+publish)\b/i },
5
6
  { id: "destructive-files", pattern: /(?:^|[;&|]\s*)(?:rm\s+-[^\n]*r[^\n]*f|rmdir\s+\/s|del\s+\/s|remove-item\b[^\n]*-recurse[^\n]*-force)/i },
@@ -12,7 +12,7 @@ const SOURCE_EXTENSIONS = new Set([
12
12
  const SKIP_DIRS = new Set([
13
13
  ".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
14
14
  "Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "obj",
15
- ".ues-work", ".ues-learning", ".ues-memory", ".ues-dashboard", ".ues-sandboxes", ".ues-cache", ".ues-traces",
15
+ ".ues-work", ".ues-learning", ".ues-memory", ".ues-dashboard", ".ues-sandboxes", ".ues-cache", ".ues-traces", ".ues-services",
16
16
  ])
17
17
  const STOP_IDENTIFIERS = new Set([
18
18
  "const","let","var","function","class","interface","type","export","import","from","return",