@astrale-os/cli 0.5.0-alpha.0 → 0.6.1-alpha.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.
- package/dist/astrale.js +12962 -487
- package/package.json +7 -3
- package/src/command.ts +2 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -2
- package/src/commands/__tests__/domain-list.test.ts +6 -2
- package/src/commands/__tests__/install-identity-override.test.ts +1 -1
- package/src/commands/__tests__/view.test.ts +100 -0
- package/src/commands/call.ts +1 -1
- package/src/commands/domain/install.ts +5 -5
- package/src/commands/domain/list.ts +2 -2
- package/src/commands/domain/publish.ts +3 -3
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +3 -3
- package/src/commands/instance/list.ts +8 -3
- package/src/commands/instance/status.ts +4 -3
- package/src/commands/instance/use.ts +2 -2
- package/src/commands/query.ts +27 -8
- package/src/commands/session/analyze.ts +50 -0
- package/src/commands/session/list.ts +36 -0
- package/src/commands/token.ts +1 -1
- package/src/commands/view-serve.ts +26 -0
- package/src/commands/view.ts +614 -0
- package/src/connect-core.test.ts +42 -0
- package/src/connect-core.ts +53 -0
- package/src/kernel/__tests__/auth.test.ts +1 -0
- package/src/kernel/client.ts +30 -24
- package/src/kernel/expand.ts +3 -2
- package/src/kernel/index.ts +7 -1
- package/src/kernel/options.ts +1 -1
- package/src/lib/__tests__/instance-target.test.ts +34 -0
- package/src/lib/__tests__/view-open-intent.test.ts +308 -0
- package/src/lib/__tests__/view-snapshot.test.ts +77 -0
- package/src/lib/admin-domain.ts +8 -4
- package/src/lib/admin-instance.ts +5 -1
- package/src/lib/config.ts +1 -0
- package/src/lib/instance-target.ts +5 -0
- package/src/lib/instance.ts +11 -0
- package/src/lib/log.ts +12 -2
- package/src/lib/login-flow.ts +41 -4
- package/src/lib/provision-instance.ts +2 -2
- package/src/lib/view/open-intent.ts +97 -0
- package/src/lib/view/resolve.ts +104 -0
- package/src/lib/view/server.ts +294 -0
- package/src/lib/view/session.ts +123 -0
- package/src/lib/view/snapshot.ts +101 -0
- package/src/program.ts +12 -1
- package/src/registry.ts +1 -1
- package/src/setup/steps/instance.ts +2 -2
- package/src/telemetry/__tests__/gate.test.ts +63 -0
- package/src/telemetry/__tests__/recorder.test.ts +110 -0
- package/src/telemetry/__tests__/redact.test.ts +79 -0
- package/src/telemetry/__tests__/session.test.ts +166 -0
- package/src/telemetry/__tests__/trigger.test.ts +49 -0
- package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
- package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
- package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
- package/src/telemetry/adapters/claude-code.ts +90 -0
- package/src/telemetry/adapters/codex.ts +160 -0
- package/src/telemetry/adapters/index.ts +45 -0
- package/src/telemetry/adapters/types.ts +21 -0
- package/src/telemetry/analyze.ts +227 -0
- package/src/telemetry/gate.ts +79 -0
- package/src/telemetry/recorder.ts +57 -0
- package/src/telemetry/redact.ts +53 -0
- package/src/telemetry/session.ts +88 -0
- package/src/telemetry/settings.ts +26 -0
- package/src/telemetry/store.ts +91 -0
- package/src/telemetry/trigger.ts +119 -0
- package/src/telemetry/types.ts +64 -0
- package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
- package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/server/agent/ask.ts +2 -1
- package/studio/server/agent/runner.ts +4 -1
- package/studio/server/agent/session-id.ts +13 -0
- package/studio/server/api.ts +43 -25
- package/studio/server/cache.ts +17 -6
- package/studio/server/client-package.test.ts +147 -0
- package/studio/server/client-package.ts +242 -0
- package/studio/server/index.ts +13 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -0
- package/studio/server/introspect/anatomy-extras.ts +48 -49
- package/studio/server/introspect/anatomy.ts +7 -7
- package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
- package/studio/server/introspect/overlay-tsmorph.ts +122 -22
- package/studio/server/state/views.test.ts +90 -0
- package/studio/server/state/views.ts +396 -99
- package/studio/server/state/visibility.ts +5 -5
- package/studio/server/view-dev-server.test.ts +111 -0
- package/studio/server/view-dev-server.ts +372 -0
- package/studio/shared/types.ts +57 -10
- package/viewer/dist/index.html +93 -0
- package/viewer/dist/main.js +71 -0
- package/studio/client/dist/assets/index-BcejyJpa.css +0 -1
- package/studio/client/dist/assets/index-Cqz3Oy_B.js +0 -179
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session identity: buckets CLI invocations by workspace (git root, else cwd).
|
|
3
|
+
* ASTRALE_SESSION pins an explicit id owned by a surface; otherwise an ambient
|
|
4
|
+
* session is reused within the idle window or minted per root. Never spawns a
|
|
5
|
+
* child process; every fs touch is swallowed so telemetry can't break the CLI.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import type { SessionMeta } from './types'
|
|
12
|
+
|
|
13
|
+
import { listSessions, metaPath, sessionDir } from './store'
|
|
14
|
+
|
|
15
|
+
export type ResolvedSession = { id: string; root: string; explicit: boolean }
|
|
16
|
+
|
|
17
|
+
const MAX_ID_LEN = 64
|
|
18
|
+
|
|
19
|
+
/** Nearest ancestor of `start` (inclusive) containing a `.git` entry, else null. */
|
|
20
|
+
function findGitRoot(start: string): string | null {
|
|
21
|
+
let dir = start
|
|
22
|
+
for (;;) {
|
|
23
|
+
if (existsSync(join(dir, '.git'))) return dir
|
|
24
|
+
const parent = dirname(dir)
|
|
25
|
+
if (parent === dir) return null
|
|
26
|
+
dir = parent
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Keep only id-safe chars, capped to 64. */
|
|
31
|
+
function sanitizeId(raw: string): string {
|
|
32
|
+
return raw.replace(/[^a-zA-Z0-9._-]/g, '').slice(0, MAX_ID_LEN)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Local-time yyyymmddHHmm stamp for a minted ambient id. */
|
|
36
|
+
function stamp(d = new Date()): string {
|
|
37
|
+
const p = (n: number, w = 2) => String(n).padStart(w, '0')
|
|
38
|
+
return `${p(d.getFullYear(), 4)}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** id of an open, ambient session already bucketed to `root`, else null.
|
|
42
|
+
* Analyzed sessions are never reused — a straggler event may "reopen" one
|
|
43
|
+
* after its report, and new work funneled there would never be analyzed. */
|
|
44
|
+
function findOpenAmbient(root: string): string | null {
|
|
45
|
+
for (const s of listSessions()) {
|
|
46
|
+
if (s.meta?.root === root && s.meta.explicit === false && !s.closed && s.analyzed === null) {
|
|
47
|
+
return s.id
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mintAmbientId(root: string): string {
|
|
54
|
+
const hash8 = createHash('sha256').update(root).digest('hex').slice(0, 8)
|
|
55
|
+
return `amb-${hash8}-${stamp()}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve the session identity for `cwd` without creating anything on disk. */
|
|
59
|
+
export function resolveSession(cwd: string): ResolvedSession {
|
|
60
|
+
const root = findGitRoot(cwd) ?? cwd
|
|
61
|
+
const pinned = process.env.ASTRALE_SESSION
|
|
62
|
+
if (pinned) {
|
|
63
|
+
const id = sanitizeId(pinned)
|
|
64
|
+
if (id.length > 0) return { id, root, explicit: true }
|
|
65
|
+
}
|
|
66
|
+
return { id: findOpenAmbient(root) ?? mintAmbientId(root), root, explicit: false }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve, then create the session dir + meta.json on first sight. */
|
|
70
|
+
export function ensureSession(cwd: string): ResolvedSession {
|
|
71
|
+
const resolved = resolveSession(cwd)
|
|
72
|
+
const dir = sessionDir(resolved.id)
|
|
73
|
+
if (!existsSync(dir)) {
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(dir, { recursive: true })
|
|
76
|
+
const meta: SessionMeta = {
|
|
77
|
+
id: resolved.id,
|
|
78
|
+
root: resolved.root,
|
|
79
|
+
explicit: resolved.explicit,
|
|
80
|
+
createdAt: new Date().toISOString(),
|
|
81
|
+
}
|
|
82
|
+
writeFileSync(metaPath(resolved.id), JSON.stringify(meta, null, 2) + '\n')
|
|
83
|
+
} catch {
|
|
84
|
+
/* telemetry never breaks the CLI */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return resolved
|
|
88
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry kill-switch, evaluated synchronously at process start/exit.
|
|
3
|
+
* Off when ASTRALE_TELEMETRY is 0/false/off, or when config telemetry.enabled
|
|
4
|
+
* is false; on by default. Every read silent-fails to enabled.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
import { createPaths } from '../lib/env'
|
|
9
|
+
|
|
10
|
+
const OFF_VALUES = new Set(['0', 'false', 'off'])
|
|
11
|
+
|
|
12
|
+
/** Whether telemetry recording is enabled for this process. */
|
|
13
|
+
export function telemetryEnabled(): boolean {
|
|
14
|
+
const env = process.env.ASTRALE_TELEMETRY
|
|
15
|
+
if (env !== undefined && OFF_VALUES.has(env.trim().toLowerCase())) return false
|
|
16
|
+
try {
|
|
17
|
+
// Call-time path resolution — see sessionsRoot() in store.ts for why.
|
|
18
|
+
const parsed = JSON.parse(readFileSync(createPaths().config, 'utf-8')) as {
|
|
19
|
+
telemetry?: { enabled?: boolean }
|
|
20
|
+
}
|
|
21
|
+
if (parsed.telemetry?.enabled === false) return false
|
|
22
|
+
} catch {
|
|
23
|
+
/* missing or broken config → default on */
|
|
24
|
+
}
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session store layout under `~/.astrale/sessions/<id>/`:
|
|
3
|
+
* events.jsonl append-only CLI events; its mtime IS the session's last-activity
|
|
4
|
+
* meta.json SessionMeta, written once at creation
|
|
5
|
+
* .analyzed AnalyzedMarker, written by the analyzer (any outcome)
|
|
6
|
+
* report.md analyzer output
|
|
7
|
+
* A session is CLOSED when events.jsonl's mtime is older than IDLE_WINDOW_MS.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import type { AnalyzedMarker, SessionMeta } from './types'
|
|
13
|
+
|
|
14
|
+
import { createPaths } from '../lib/env'
|
|
15
|
+
|
|
16
|
+
export const IDLE_WINDOW_MS = 30 * 60 * 1000
|
|
17
|
+
|
|
18
|
+
// Resolved at CALL time (not import time) so ASTRALE_HOME set late — notably
|
|
19
|
+
// by tests whose module-load order is out of their control — always applies.
|
|
20
|
+
// The env can't change mid-process in real CLI runs, so behavior is identical.
|
|
21
|
+
export function sessionsRoot(): string {
|
|
22
|
+
return join(createPaths().home, 'sessions')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sessionDir(id: string): string {
|
|
26
|
+
return join(sessionsRoot(), id)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function eventsPath(id: string): string {
|
|
30
|
+
return join(sessionDir(id), 'events.jsonl')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function metaPath(id: string): string {
|
|
34
|
+
return join(sessionDir(id), 'meta.json')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function markerPath(id: string): string {
|
|
38
|
+
return join(sessionDir(id), '.analyzed')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function reportPath(id: string): string {
|
|
42
|
+
return join(sessionDir(id), 'report.md')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type SessionInfo = {
|
|
46
|
+
id: string
|
|
47
|
+
meta: SessionMeta | null
|
|
48
|
+
lastEventAt: Date | null
|
|
49
|
+
closed: boolean
|
|
50
|
+
analyzed: AnalyzedMarker | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJsonSafe<T>(path: string): T | null {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(path, 'utf-8')) as T
|
|
56
|
+
} catch {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function inspectSession(id: string, now = Date.now()): SessionInfo | null {
|
|
62
|
+
const dir = sessionDir(id)
|
|
63
|
+
if (!existsSync(dir)) return null
|
|
64
|
+
let lastEventAt: Date | null = null
|
|
65
|
+
try {
|
|
66
|
+
lastEventAt = statSync(eventsPath(id)).mtime
|
|
67
|
+
} catch {
|
|
68
|
+
/* no events yet */
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
meta: readJsonSafe<SessionMeta>(metaPath(id)),
|
|
73
|
+
lastEventAt,
|
|
74
|
+
closed: lastEventAt !== null && now - lastEventAt.getTime() > IDLE_WINDOW_MS,
|
|
75
|
+
analyzed: readJsonSafe<AnalyzedMarker>(markerPath(id)),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** All sessions, newest activity first. Missing store dir → empty list. */
|
|
80
|
+
export function listSessions(now = Date.now()): SessionInfo[] {
|
|
81
|
+
let ids: string[]
|
|
82
|
+
try {
|
|
83
|
+
ids = readdirSync(sessionsRoot()).filter((n) => !n.startsWith('.'))
|
|
84
|
+
} catch {
|
|
85
|
+
return []
|
|
86
|
+
}
|
|
87
|
+
return ids
|
|
88
|
+
.map((id) => inspectSession(id, now))
|
|
89
|
+
.filter((s): s is SessionInfo => s !== null)
|
|
90
|
+
.sort((a, b) => (b.lastEventAt?.getTime() ?? 0) - (a.lastEventAt?.getTime() ?? 0))
|
|
91
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opportunistic analysis trigger — the `git gc --auto` model. Each CLI start
|
|
3
|
+
* cheaply scans for a closed, unanalyzed session and spawns ONE detached
|
|
4
|
+
* analyzer for it. No daemon, no cron; a lockfile keeps it single-flight.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import { telemetryEnabled } from './settings'
|
|
11
|
+
import { listSessions, sessionDir, sessionsRoot, type SessionInfo } from './store'
|
|
12
|
+
|
|
13
|
+
const LOCK_STALE_MS = 30 * 60 * 1000
|
|
14
|
+
const GC_AGE_MS = 30 * 24 * 60 * 60 * 1000
|
|
15
|
+
const GC_MAX_PER_RUN = 20
|
|
16
|
+
|
|
17
|
+
/** Opportunistic GC: analyzed sessions idle past the retention age are removed
|
|
18
|
+
* (capped per invocation) so the store — and the per-invocation scan — stays
|
|
19
|
+
* bounded. Evidence worth keeping has left via reports or filed issues. */
|
|
20
|
+
function gcOldSessions(sessions: SessionInfo[]): void {
|
|
21
|
+
try {
|
|
22
|
+
const cutoff = Date.now() - GC_AGE_MS
|
|
23
|
+
let removed = 0
|
|
24
|
+
for (const s of sessions) {
|
|
25
|
+
if (removed >= GC_MAX_PER_RUN) break
|
|
26
|
+
if (s.analyzed !== null && s.lastEventAt !== null && s.lastEventAt.getTime() < cutoff) {
|
|
27
|
+
rmSync(sessionDir(s.id), { recursive: true, force: true })
|
|
28
|
+
removed++
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
/* best effort */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function lockPath(): string {
|
|
37
|
+
return join(sessionsRoot(), '.analyzer.lock')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True when the lock is free (or stale/dead) and was claimed. */
|
|
41
|
+
function claimLock(): boolean {
|
|
42
|
+
const path = lockPath()
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(path)) {
|
|
45
|
+
const lock = JSON.parse(readFileSync(path, 'utf-8')) as { pid: number; at: number }
|
|
46
|
+
const stale = Date.now() - lock.at > LOCK_STALE_MS
|
|
47
|
+
let alive = false
|
|
48
|
+
try {
|
|
49
|
+
process.kill(lock.pid, 0)
|
|
50
|
+
alive = true
|
|
51
|
+
} catch {
|
|
52
|
+
/* dead */
|
|
53
|
+
}
|
|
54
|
+
if (alive && !stale) return false
|
|
55
|
+
unlinkSync(path)
|
|
56
|
+
}
|
|
57
|
+
mkdirSync(sessionsRoot(), { recursive: true })
|
|
58
|
+
writeFileSync(path, JSON.stringify({ pid: process.pid, at: Date.now() }))
|
|
59
|
+
return true
|
|
60
|
+
} catch {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Release only in the parent that claimed; the child rewrites its own. */
|
|
66
|
+
export function releaseLock(): void {
|
|
67
|
+
try {
|
|
68
|
+
unlinkSync(lockPath())
|
|
69
|
+
} catch {
|
|
70
|
+
/* already gone */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fire-and-forget: spawn a detached `astrale session analyze <id> --auto` for
|
|
76
|
+
* the most recently closed unanalyzed session, if any. Never throws, never
|
|
77
|
+
* blocks — worst case a few ms of directory stats.
|
|
78
|
+
*/
|
|
79
|
+
export function maybeTriggerAnalysis(argv: string[]): void {
|
|
80
|
+
try {
|
|
81
|
+
if (!telemetryEnabled()) return
|
|
82
|
+
if (process.env.ASTRALE_TELEMETRY_NO_TRIGGER === '1') return
|
|
83
|
+
// Never cascade off the session commands themselves.
|
|
84
|
+
if (argv[2] === 'session') return
|
|
85
|
+
|
|
86
|
+
const sessions = listSessions()
|
|
87
|
+
gcOldSessions(sessions)
|
|
88
|
+
const target = sessions.find((s) => s.closed && s.analyzed === null && s.lastEventAt !== null)
|
|
89
|
+
if (!target) return
|
|
90
|
+
if (!claimLock()) return
|
|
91
|
+
|
|
92
|
+
// Re-invoke self: execPath is bun/node (dev) or the compiled binary; when
|
|
93
|
+
// argv[1] is a real script path, keep it as the first argument. In a
|
|
94
|
+
// bun-compiled binary argv[1] is a virtual /$bunfs/ path that must NOT be
|
|
95
|
+
// passed — the binary re-runs itself from execPath alone.
|
|
96
|
+
const script = process.argv[1]
|
|
97
|
+
const args =
|
|
98
|
+
script && script !== process.execPath && !script.startsWith('/$bunfs') ? [script] : []
|
|
99
|
+
args.push('session', 'analyze', target.id, '--auto')
|
|
100
|
+
const child = spawn(process.execPath, args, {
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: 'ignore',
|
|
103
|
+
env: { ...process.env, ASTRALE_TELEMETRY_NO_TRIGGER: '1' },
|
|
104
|
+
})
|
|
105
|
+
child.unref()
|
|
106
|
+
// The lock hands over to the child: it re-stamps with its own pid on start.
|
|
107
|
+
} catch {
|
|
108
|
+
/* telemetry must never affect the CLI */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Called by `session analyze --auto` to take over the parent's claim. */
|
|
113
|
+
export function restampLock(): void {
|
|
114
|
+
try {
|
|
115
|
+
writeFileSync(lockPath(), JSON.stringify({ pid: process.pid, at: Date.now() }))
|
|
116
|
+
} catch {
|
|
117
|
+
/* best effort */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry contracts. One CLI invocation = one appended event; one session =
|
|
3
|
+
* one directory of evidence under `~/.astrale/sessions/`; harness adapters
|
|
4
|
+
* contribute transcript discovery + a reading guide, never normalization.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** One CLI invocation, one JSON line in events.jsonl. */
|
|
8
|
+
export type TelemetryEvent = {
|
|
9
|
+
/** Envelope version. */
|
|
10
|
+
v: 1
|
|
11
|
+
/** ISO timestamp at process start. */
|
|
12
|
+
ts: string
|
|
13
|
+
/** argv after the binary name, secrets redacted (see redact.ts). */
|
|
14
|
+
argv: string[]
|
|
15
|
+
exitCode: number
|
|
16
|
+
durationMs: number
|
|
17
|
+
/** Error class name when the command threw (AstraleError code preferred). */
|
|
18
|
+
errorName?: string
|
|
19
|
+
cwd: string
|
|
20
|
+
/** Workspace root the session is bucketed by (git root, else cwd). */
|
|
21
|
+
root: string
|
|
22
|
+
/** Which surface produced the event. */
|
|
23
|
+
surface: 'cli'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Written once when a session directory is created. */
|
|
27
|
+
export type SessionMeta = {
|
|
28
|
+
id: string
|
|
29
|
+
root: string
|
|
30
|
+
/** true when the id came from ASTRALE_SESSION (a surface owns the lifecycle). */
|
|
31
|
+
explicit: boolean
|
|
32
|
+
createdAt: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Written by the analyzer when it finishes with a session (any outcome). */
|
|
36
|
+
export type AnalyzedMarker = {
|
|
37
|
+
analyzedAt: string
|
|
38
|
+
outcome: 'skipped-quiet' | 'reported' | 'filed' | 'error'
|
|
39
|
+
note?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** An agent-harness session discovered on this machine (transcript = evidence). */
|
|
43
|
+
export type HarnessSession = {
|
|
44
|
+
harness: string
|
|
45
|
+
sessionId?: string
|
|
46
|
+
transcriptPath: string
|
|
47
|
+
cwd?: string
|
|
48
|
+
startedAt?: string
|
|
49
|
+
endedAt?: string
|
|
50
|
+
sizeBytes: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Deterministic signals extracted from events.jsonl — the free gate's input. */
|
|
54
|
+
export type SessionSignals = {
|
|
55
|
+
eventCount: number
|
|
56
|
+
/** Events with a non-zero exit, grouped by leading command tokens. */
|
|
57
|
+
failures: { command: string; count: number; errorNames: string[] }[]
|
|
58
|
+
/** Same leading command tokens re-run 3+ times (retry smell). */
|
|
59
|
+
retries: { command: string; count: number }[]
|
|
60
|
+
firstEventAt?: string
|
|
61
|
+
lastEventAt?: string
|
|
62
|
+
/** Harness transcripts overlapping this session's root + time window. */
|
|
63
|
+
harnessSessions: HarnessSession[]
|
|
64
|
+
}
|