@wrongstack/wrongtrace 0.313.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/LICENSE +21 -0
- package/dist/adapters/ipc.d.ts +49 -0
- package/dist/adapters/ipc.js +127 -0
- package/dist/adapters/mcp.d.ts +28 -0
- package/dist/adapters/mcp.js +42 -0
- package/dist/agent-helpers.d.ts +109 -0
- package/dist/agent-helpers.js +226 -0
- package/dist/client.d.ts +45 -0
- package/dist/client.js +304 -0
- package/dist/discovery.d.ts +37 -0
- package/dist/discovery.js +65 -0
- package/dist/gate-counters.d.ts +53 -0
- package/dist/gate-counters.js +161 -0
- package/dist/gate.d.ts +64 -0
- package/dist/gate.js +94 -0
- package/dist/hooks.d.ts +120 -0
- package/dist/hooks.js +228 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +28 -0
- package/dist/types.d.ts +237 -0
- package/dist/types.js +12 -0
- package/package.json +49 -0
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared WrongTrace pre-flight gate for every host process.
|
|
3
|
+
*
|
|
4
|
+
* Originally lived in `@wrongstack/cli/wiring`; moved here so the CLI
|
|
5
|
+
* leader, fleet subagents, AND the standalone WebUI server can all pass
|
|
6
|
+
* their mutating tool calls through the exact same lock gate without any
|
|
7
|
+
* of them importing `@wrongstack/cli` (dep direction cli -> webui-server
|
|
8
|
+
* forbids the reverse edge).
|
|
9
|
+
*
|
|
10
|
+
* Wraps the @wrongstack/wrongtrace client into tiny entry points a host
|
|
11
|
+
* can call without caring whether the daemon is running:
|
|
12
|
+
*
|
|
13
|
+
* - `preflightFileEdit(path)` -> health/lock check BEFORE a heavy edit.
|
|
14
|
+
* Locked files hard-block; fragile files soften the edit strategy.
|
|
15
|
+
* - `withFileLock(path, fn)` -> claim the daemon's lock around a heavy
|
|
16
|
+
* edit so peer agents don't thrash the same file; unlocks in `finally`.
|
|
17
|
+
*
|
|
18
|
+
* Design rules (kept deliberately):
|
|
19
|
+
* - The daemon is OPTIONAL. Every path degrades to "allow" - an offline
|
|
20
|
+
* WrongTrace must never block an edit (mirrors proxy-probe's soft
|
|
21
|
+
* -signal philosophy).
|
|
22
|
+
* - Discovery runs ONCE, lazily, fire-and-forget per process. Each host
|
|
23
|
+
* process gets its own singleton here; the CLI additionally warms it
|
|
24
|
+
* at boot (`void getWrongTrace()`).
|
|
25
|
+
* - Stale locks (TTL already elapsed) do not block; the risk fusion in
|
|
26
|
+
* agent-helpers already ignores them.
|
|
27
|
+
*/
|
|
28
|
+
import { type WrongTraceClientInternal } from "./client.js";
|
|
29
|
+
import { type CrossAgentRisk } from "./agent-helpers.js";
|
|
30
|
+
/** Outcome the caller dispatches on. `allow` is always the safe default. */
|
|
31
|
+
export type PreflightVerdict = {
|
|
32
|
+
kind: "allow";
|
|
33
|
+
risk: CrossAgentRisk | null;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "blocked";
|
|
36
|
+
risk: CrossAgentRisk;
|
|
37
|
+
};
|
|
38
|
+
/** Lazily created singleton — resolves to isAvailable:false when offline. */
|
|
39
|
+
export declare function getWrongTrace(): Promise<WrongTraceClientInternal>;
|
|
40
|
+
/** Test seam: reset the singleton between suites. */
|
|
41
|
+
export declare function resetWrongTraceGate(): void;
|
|
42
|
+
export interface PreflightOptions {
|
|
43
|
+
/** Caller identity stamped on locks (e.g. session id / agent name). */
|
|
44
|
+
owner?: string;
|
|
45
|
+
ownerRunId?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Pre-flight check before a heavy edit. Fast (single file-health fetch
|
|
49
|
+
* behind the adapter's risk fusion). Locked by ANOTHER owner → blocked;
|
|
50
|
+
* everything else, including daemon-offline → allow. A lock the caller
|
|
51
|
+
* itself holds (`selfOwner` matching the daemon's `lock_owner`) is exempted.
|
|
52
|
+
*
|
|
53
|
+
* Verified live (2026-08-24): `/api/file/health` returns `is_locked`,
|
|
54
|
+
* `lock_owner`, `lock_reason`, `lock_expires_at` while a lock is held, so
|
|
55
|
+
* this CAN return `blocked` on the current daemon schema.
|
|
56
|
+
*/
|
|
57
|
+
export declare function preflightFileEdit(path: string, selfOwner?: string): Promise<PreflightVerdict>;
|
|
58
|
+
/**
|
|
59
|
+
* Run `fn` under the daemon's file lock. Best-effort: if the daemon is
|
|
60
|
+
* offline, or lock acquisition fails for any transport reason, `fn` still
|
|
61
|
+
* runs — coordination is an optimization, not a hard dependency.
|
|
62
|
+
*/
|
|
63
|
+
export declare function withFileLock<T>(path: string, reason: string, fn: () => Promise<T>, opts?: PreflightOptions): Promise<T>;
|
|
64
|
+
//# sourceMappingURL=gate.d.ts.map
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared WrongTrace pre-flight gate for every host process.
|
|
3
|
+
*
|
|
4
|
+
* Originally lived in `@wrongstack/cli/wiring`; moved here so the CLI
|
|
5
|
+
* leader, fleet subagents, AND the standalone WebUI server can all pass
|
|
6
|
+
* their mutating tool calls through the exact same lock gate without any
|
|
7
|
+
* of them importing `@wrongstack/cli` (dep direction cli -> webui-server
|
|
8
|
+
* forbids the reverse edge).
|
|
9
|
+
*
|
|
10
|
+
* Wraps the @wrongstack/wrongtrace client into tiny entry points a host
|
|
11
|
+
* can call without caring whether the daemon is running:
|
|
12
|
+
*
|
|
13
|
+
* - `preflightFileEdit(path)` -> health/lock check BEFORE a heavy edit.
|
|
14
|
+
* Locked files hard-block; fragile files soften the edit strategy.
|
|
15
|
+
* - `withFileLock(path, fn)` -> claim the daemon's lock around a heavy
|
|
16
|
+
* edit so peer agents don't thrash the same file; unlocks in `finally`.
|
|
17
|
+
*
|
|
18
|
+
* Design rules (kept deliberately):
|
|
19
|
+
* - The daemon is OPTIONAL. Every path degrades to "allow" - an offline
|
|
20
|
+
* WrongTrace must never block an edit (mirrors proxy-probe's soft
|
|
21
|
+
* -signal philosophy).
|
|
22
|
+
* - Discovery runs ONCE, lazily, fire-and-forget per process. Each host
|
|
23
|
+
* process gets its own singleton here; the CLI additionally warms it
|
|
24
|
+
* at boot (`void getWrongTrace()`).
|
|
25
|
+
* - Stale locks (TTL already elapsed) do not block; the risk fusion in
|
|
26
|
+
* agent-helpers already ignores them.
|
|
27
|
+
*/
|
|
28
|
+
import { createWrongTraceClient } from "./client.js";
|
|
29
|
+
import { getCrossAgentRisk } from "./agent-helpers.js";
|
|
30
|
+
let clientPromise;
|
|
31
|
+
/** Lazily created singleton — resolves to isAvailable:false when offline. */
|
|
32
|
+
export function getWrongTrace() {
|
|
33
|
+
if (clientPromise === undefined) {
|
|
34
|
+
clientPromise = createWrongTraceClient().catch(() => {
|
|
35
|
+
// Never let a discovery rejection poison the singleton.
|
|
36
|
+
return { isAvailable: false };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return clientPromise;
|
|
40
|
+
}
|
|
41
|
+
/** Test seam: reset the singleton between suites. */
|
|
42
|
+
export function resetWrongTraceGate() {
|
|
43
|
+
clientPromise = undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pre-flight check before a heavy edit. Fast (single file-health fetch
|
|
47
|
+
* behind the adapter's risk fusion). Locked by ANOTHER owner → blocked;
|
|
48
|
+
* everything else, including daemon-offline → allow. A lock the caller
|
|
49
|
+
* itself holds (`selfOwner` matching the daemon's `lock_owner`) is exempted.
|
|
50
|
+
*
|
|
51
|
+
* Verified live (2026-08-24): `/api/file/health` returns `is_locked`,
|
|
52
|
+
* `lock_owner`, `lock_reason`, `lock_expires_at` while a lock is held, so
|
|
53
|
+
* this CAN return `blocked` on the current daemon schema.
|
|
54
|
+
*/
|
|
55
|
+
export async function preflightFileEdit(path, selfOwner) {
|
|
56
|
+
const wt = await getWrongTrace();
|
|
57
|
+
if (!wt.isAvailable)
|
|
58
|
+
return { kind: "allow", risk: null };
|
|
59
|
+
const risk = await getCrossAgentRisk(wt, path, 50, selfOwner);
|
|
60
|
+
if (risk.band === "locked")
|
|
61
|
+
return { kind: "blocked", risk };
|
|
62
|
+
return { kind: "allow", risk };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Run `fn` under the daemon's file lock. Best-effort: if the daemon is
|
|
66
|
+
* offline, or lock acquisition fails for any transport reason, `fn` still
|
|
67
|
+
* runs — coordination is an optimization, not a hard dependency.
|
|
68
|
+
*/
|
|
69
|
+
export async function withFileLock(path, reason, fn, opts = {}) {
|
|
70
|
+
const wt = await getWrongTrace();
|
|
71
|
+
if (!wt.isAvailable)
|
|
72
|
+
return fn();
|
|
73
|
+
const lockOpts = {
|
|
74
|
+
// Generous TTL: heavy edits can run long; the lock self-reaps if we
|
|
75
|
+
// crash mid-edit so a dead session can never block the file forever.
|
|
76
|
+
ttlSeconds: 900,
|
|
77
|
+
};
|
|
78
|
+
if (opts.owner !== undefined)
|
|
79
|
+
lockOpts.owner = opts.owner;
|
|
80
|
+
if (opts.ownerRunId !== undefined)
|
|
81
|
+
lockOpts.ownerRunId = opts.ownerRunId;
|
|
82
|
+
const res = await wt.lockFile(path, reason, lockOpts);
|
|
83
|
+
// Conflict body carries ok:false + owner/expires_at — do NOT steal the
|
|
84
|
+
// file; run unlocked rather than forcing a peer's lock away.
|
|
85
|
+
const acquired = res?.ok === true;
|
|
86
|
+
try {
|
|
87
|
+
return await fn();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (acquired)
|
|
91
|
+
await wt.unlockFile(path);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=gate.js.map
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WrongTrace guardrail hooks — shared by every host that executes tools.
|
|
3
|
+
*
|
|
4
|
+
* Originally lived in `@wrongstack/cli/wiring`; moved here so the CLI
|
|
5
|
+
* leader, fleet subagents, the standalone WebUI server, and runtime-package
|
|
6
|
+
* light subagents register the identical gate without cross-importing each
|
|
7
|
+
* other. These are STRUCTURAL functions: they deliberately avoid an
|
|
8
|
+
* `@wrongstack/core` dependency (the adapter package stays decoupled from
|
|
9
|
+
* every runtime inside WrongStack), so hosts register them on their own
|
|
10
|
+
* `HookRegistry` and the structural parameter types accept core's richer
|
|
11
|
+
* `HookInput`.
|
|
12
|
+
*
|
|
13
|
+
* Behaviour:
|
|
14
|
+
* preToolUse: resolve the target path from toolInput; run the
|
|
15
|
+
* WrongTrace pre-flight (health + lock state). A file
|
|
16
|
+
* locked by ANOTHER owner DENIES the call with the
|
|
17
|
+
* owner/expiry in the reason — the model sees it and can
|
|
18
|
+
* pick another file. A lock owned by THIS session is
|
|
19
|
+
* treated as available (self-owner exemption) so the
|
|
20
|
+
* acquire-in-pre/release-in-post pairing stays usable —
|
|
21
|
+
* a leaked own lock (executor denied/threw before postToolUse)
|
|
22
|
+
* never blocks the session's own retry. Healthy/fragile/
|
|
23
|
+
* offline daemons ALLOW; fragile files additionally get a
|
|
24
|
+
* one-line "prefer surgical edits" nudge via additionalContext.
|
|
25
|
+
* On allow we also ACQUIRE the daemon lock for the edit
|
|
26
|
+
* (owner = session id) so peers see the claim.
|
|
27
|
+
* postToolUse: release the lock acquired in preToolUse (path-keyed).
|
|
28
|
+
* The daemon TTL is the leak backstop if execution never
|
|
29
|
+
* completes.
|
|
30
|
+
*
|
|
31
|
+
* Concurrency: lock bookkeeping is scoped PER HOOK PAIR (per runner), not
|
|
32
|
+
* process-global, and reference-counted. A shared module-level map would let
|
|
33
|
+
* one executor's postToolUse release another executor's active lock when the
|
|
34
|
+
* same path is claimed by two in-process runners. `createWrongTraceHookPair`
|
|
35
|
+
* allocates its own counter map; it is also safe to share ONE pair across
|
|
36
|
+
* concurrent executors (standalone WebUI parent + SDD-wizard workers, or the
|
|
37
|
+
* fleet runner across subagents) — overlapping claims increment the count and
|
|
38
|
+
* the daemon lock is only released when the LAST finisher's postToolUse
|
|
39
|
+
* decrements it to zero.
|
|
40
|
+
*
|
|
41
|
+
* Failure philosophy: COORDINATION optimization, never a hard dependency.
|
|
42
|
+
* Daemon offline → everything allows. Any throw inside the hooks must be
|
|
43
|
+
* swallowed by the caller-side catch below so a slow daemon (timeout) can
|
|
44
|
+
* never add latency surprises to the edit path.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Structural subset of the host's `HookInput` this hook actually reads.
|
|
48
|
+
* Core's `HookInput` (and any host equivalent carrying `toolName` +
|
|
49
|
+
* `toolInput`) is structurally assignable to this, which is what lets the
|
|
50
|
+
* same factory serve every host without a framework dependency here.
|
|
51
|
+
*/
|
|
52
|
+
export interface WrongTraceHookInput {
|
|
53
|
+
toolName?: string | undefined;
|
|
54
|
+
toolInput?: unknown;
|
|
55
|
+
}
|
|
56
|
+
/** Mutually-exclusive pre-flight verdict, mirroring core's contract. */
|
|
57
|
+
export type WrongTracePreToolUseOutcome = {
|
|
58
|
+
action: "allow";
|
|
59
|
+
additionalContext?: string | undefined;
|
|
60
|
+
} | {
|
|
61
|
+
action: "deny";
|
|
62
|
+
reason: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Typed gate-decision events emitted by the hooks when a host supplies an
|
|
66
|
+
* `emit` callback. One event per decision point — denied edit, fragile
|
|
67
|
+
* nudge, lock acquired / race-lost / released. Hosts map these onto their
|
|
68
|
+
* EventBus (e.g. `events.emit('wrongtrace.gate.decision', e)`); the
|
|
69
|
+
* adapter itself stays transport-agnostic.
|
|
70
|
+
*/
|
|
71
|
+
export type WrongTraceGateDecisionEvent = {
|
|
72
|
+
kind: "deny";
|
|
73
|
+
path: string;
|
|
74
|
+
reason: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: "allow-fragile";
|
|
77
|
+
path: string;
|
|
78
|
+
reasons: readonly string[];
|
|
79
|
+
} | {
|
|
80
|
+
kind: "lock-acquired";
|
|
81
|
+
path: string;
|
|
82
|
+
owner: string;
|
|
83
|
+
} | {
|
|
84
|
+
kind: "lock-conflict-race";
|
|
85
|
+
path: string;
|
|
86
|
+
} | {
|
|
87
|
+
kind: "lock-released";
|
|
88
|
+
path: string;
|
|
89
|
+
};
|
|
90
|
+
export interface WrongTraceHookPair {
|
|
91
|
+
/** Pre-flight + claim. Denies when another owner holds the file. */
|
|
92
|
+
preToolUse: (input: WrongTraceHookInput, runtime?: unknown) => Promise<WrongTracePreToolUseOutcome | undefined>;
|
|
93
|
+
/** Release the lock claimed by `preToolUse` for the same path. */
|
|
94
|
+
postToolUse: (input: WrongTraceHookInput) => Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
export interface WrongTraceHookOptions {
|
|
97
|
+
/** Typed gate-decision event sink (host maps it onto its EventBus). */
|
|
98
|
+
emit?: (event: WrongTraceGateDecisionEvent) => void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Per-runner lock bookkeeping. Reference-counted: when one hook pair is
|
|
102
|
+
* shared across concurrent executors (standalone WebUI parent + SDD-wizard
|
|
103
|
+
* workers, or the fleet runner across subagents), overlapping claims on the
|
|
104
|
+
* same path increment the count and the daemon lock is only released when
|
|
105
|
+
* the LAST finisher's postToolUse decrements it to zero — a sibling finishing
|
|
106
|
+
* early can never unlock a path another in-flight edit still holds.
|
|
107
|
+
*/
|
|
108
|
+
export declare function newWrongTraceLockCounter(): Map<string, number>;
|
|
109
|
+
/**
|
|
110
|
+
* Create a pre/post hook PAIR sharing one per-runner reference-counted lock
|
|
111
|
+
* map. Every host should use this when wiring both phases: the pairing
|
|
112
|
+
* guarantees `postToolUse` only releases locks this exact pair's
|
|
113
|
+
* `preToolUse` claimed (see concurrency note at the top of this file), and
|
|
114
|
+
* the reference count keeps the daemon lock held while any sibling executor
|
|
115
|
+
* sharing the pair still has the path in flight.
|
|
116
|
+
*/
|
|
117
|
+
export declare function createWrongTraceHookPair(sessionId: () => string, opts?: WrongTraceHookOptions, counters?: Map<string, number>): WrongTraceHookPair;
|
|
118
|
+
export declare function createWrongTracePreToolUseHook(sessionId: () => string, opts?: WrongTraceHookOptions): (input: WrongTraceHookInput, runtime?: unknown) => Promise<WrongTracePreToolUseOutcome | undefined>;
|
|
119
|
+
export declare function createWrongTracePostToolUseHook(opts?: WrongTraceHookOptions): (input: WrongTraceHookInput) => Promise<void>;
|
|
120
|
+
//# sourceMappingURL=hooks.d.ts.map
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WrongTrace guardrail hooks — shared by every host that executes tools.
|
|
3
|
+
*
|
|
4
|
+
* Originally lived in `@wrongstack/cli/wiring`; moved here so the CLI
|
|
5
|
+
* leader, fleet subagents, the standalone WebUI server, and runtime-package
|
|
6
|
+
* light subagents register the identical gate without cross-importing each
|
|
7
|
+
* other. These are STRUCTURAL functions: they deliberately avoid an
|
|
8
|
+
* `@wrongstack/core` dependency (the adapter package stays decoupled from
|
|
9
|
+
* every runtime inside WrongStack), so hosts register them on their own
|
|
10
|
+
* `HookRegistry` and the structural parameter types accept core's richer
|
|
11
|
+
* `HookInput`.
|
|
12
|
+
*
|
|
13
|
+
* Behaviour:
|
|
14
|
+
* preToolUse: resolve the target path from toolInput; run the
|
|
15
|
+
* WrongTrace pre-flight (health + lock state). A file
|
|
16
|
+
* locked by ANOTHER owner DENIES the call with the
|
|
17
|
+
* owner/expiry in the reason — the model sees it and can
|
|
18
|
+
* pick another file. A lock owned by THIS session is
|
|
19
|
+
* treated as available (self-owner exemption) so the
|
|
20
|
+
* acquire-in-pre/release-in-post pairing stays usable —
|
|
21
|
+
* a leaked own lock (executor denied/threw before postToolUse)
|
|
22
|
+
* never blocks the session's own retry. Healthy/fragile/
|
|
23
|
+
* offline daemons ALLOW; fragile files additionally get a
|
|
24
|
+
* one-line "prefer surgical edits" nudge via additionalContext.
|
|
25
|
+
* On allow we also ACQUIRE the daemon lock for the edit
|
|
26
|
+
* (owner = session id) so peers see the claim.
|
|
27
|
+
* postToolUse: release the lock acquired in preToolUse (path-keyed).
|
|
28
|
+
* The daemon TTL is the leak backstop if execution never
|
|
29
|
+
* completes.
|
|
30
|
+
*
|
|
31
|
+
* Concurrency: lock bookkeeping is scoped PER HOOK PAIR (per runner), not
|
|
32
|
+
* process-global, and reference-counted. A shared module-level map would let
|
|
33
|
+
* one executor's postToolUse release another executor's active lock when the
|
|
34
|
+
* same path is claimed by two in-process runners. `createWrongTraceHookPair`
|
|
35
|
+
* allocates its own counter map; it is also safe to share ONE pair across
|
|
36
|
+
* concurrent executors (standalone WebUI parent + SDD-wizard workers, or the
|
|
37
|
+
* fleet runner across subagents) — overlapping claims increment the count and
|
|
38
|
+
* the daemon lock is only released when the LAST finisher's postToolUse
|
|
39
|
+
* decrements it to zero.
|
|
40
|
+
*
|
|
41
|
+
* Failure philosophy: COORDINATION optimization, never a hard dependency.
|
|
42
|
+
* Daemon offline → everything allows. Any throw inside the hooks must be
|
|
43
|
+
* swallowed by the caller-side catch below so a slow daemon (timeout) can
|
|
44
|
+
* never add latency surprises to the edit path.
|
|
45
|
+
*/
|
|
46
|
+
import { getWrongTrace, preflightFileEdit } from "./gate.js";
|
|
47
|
+
/** Tools that mutate a single target file and must pass the gate. */
|
|
48
|
+
const EDIT_TOOLS = new Set([
|
|
49
|
+
"edit",
|
|
50
|
+
"write",
|
|
51
|
+
"replace",
|
|
52
|
+
"patch",
|
|
53
|
+
"codebase-ast-replace",
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* Extract the target file path from a mutating tool's input, if present.
|
|
57
|
+
*
|
|
58
|
+
* Order matters: `file` (codebase-ast-replace) MUST come before any
|
|
59
|
+
* selector-ish field — `target` is the body/full AST selector, never a
|
|
60
|
+
* path, so it must not be treated as one. Multi-file inputs resolve to
|
|
61
|
+
* their first concrete file; `patch` has no single derivable target (the
|
|
62
|
+
* diff payload names its own files), so its optional `directory` is the
|
|
63
|
+
* coarsest honest claim and a bare `patch` input (no directory) yields
|
|
64
|
+
* `undefined` → gate allows without a claim.
|
|
65
|
+
*/
|
|
66
|
+
function targetPathOf(toolInput) {
|
|
67
|
+
if (!toolInput || typeof toolInput !== "object")
|
|
68
|
+
return undefined;
|
|
69
|
+
const t = toolInput;
|
|
70
|
+
// Single-file keys: edit/write → `path`, codebase-ast-replace → `file`.
|
|
71
|
+
for (const key of ["path", "file_path", "file"]) {
|
|
72
|
+
const v = t[key];
|
|
73
|
+
if (typeof v === "string" && v.length > 0)
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
// `replace` accepts files: string | string[].
|
|
77
|
+
const files = t["files"];
|
|
78
|
+
if (typeof files === "string" && files.length > 0)
|
|
79
|
+
return files;
|
|
80
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
81
|
+
const first = files[0];
|
|
82
|
+
if (typeof first === "string" && first.length > 0)
|
|
83
|
+
return first;
|
|
84
|
+
}
|
|
85
|
+
// `patch` accepts an optional directory.
|
|
86
|
+
const dir = t["directory"];
|
|
87
|
+
if (typeof dir === "string" && dir.length > 0)
|
|
88
|
+
return dir;
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
/** Emit a gate-decision event without ever letting a throw escape. */
|
|
92
|
+
function emitSafe(emit, event) {
|
|
93
|
+
try {
|
|
94
|
+
emit?.(event);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Observability must never break the edit path.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Per-runner lock bookkeeping. Reference-counted: when one hook pair is
|
|
102
|
+
* shared across concurrent executors (standalone WebUI parent + SDD-wizard
|
|
103
|
+
* workers, or the fleet runner across subagents), overlapping claims on the
|
|
104
|
+
* same path increment the count and the daemon lock is only released when
|
|
105
|
+
* the LAST finisher's postToolUse decrements it to zero — a sibling finishing
|
|
106
|
+
* early can never unlock a path another in-flight edit still holds.
|
|
107
|
+
*/
|
|
108
|
+
export function newWrongTraceLockCounter() {
|
|
109
|
+
return new Map();
|
|
110
|
+
}
|
|
111
|
+
function acquireLock(counters, path) {
|
|
112
|
+
counters.set(path, (counters.get(path) ?? 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
function releaseLock(counters, path, onZero) {
|
|
115
|
+
const next = (counters.get(path) ?? 1) - 1;
|
|
116
|
+
if (next > 0) {
|
|
117
|
+
counters.set(path, next);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
counters.delete(path);
|
|
121
|
+
onZero();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a pre/post hook PAIR sharing one per-runner reference-counted lock
|
|
125
|
+
* map. Every host should use this when wiring both phases: the pairing
|
|
126
|
+
* guarantees `postToolUse` only releases locks this exact pair's
|
|
127
|
+
* `preToolUse` claimed (see concurrency note at the top of this file), and
|
|
128
|
+
* the reference count keeps the daemon lock held while any sibling executor
|
|
129
|
+
* sharing the pair still has the path in flight.
|
|
130
|
+
*/
|
|
131
|
+
export function createWrongTraceHookPair(sessionId, opts = {}, counters = newWrongTraceLockCounter()) {
|
|
132
|
+
const emit = opts.emit;
|
|
133
|
+
return {
|
|
134
|
+
async preToolUse(input, _runtime) {
|
|
135
|
+
if (!EDIT_TOOLS.has(input.toolName ?? ""))
|
|
136
|
+
return undefined;
|
|
137
|
+
const path = targetPathOf(input.toolInput);
|
|
138
|
+
if (!path)
|
|
139
|
+
return undefined;
|
|
140
|
+
try {
|
|
141
|
+
// Self-owner exemption: OUR OWN held lock (leaked by an interrupted
|
|
142
|
+
// earlier edit) must not deny this session's retry. lock_owner from
|
|
143
|
+
// the daemon's file-health response is compared against this pair's
|
|
144
|
+
// owner identity by preflightFileEdit.
|
|
145
|
+
const verdict = await preflightFileEdit(path, `wrongstack:${sessionId()}`);
|
|
146
|
+
if (verdict.kind === "blocked") {
|
|
147
|
+
const owner = verdict.risk.reasons.join("; ");
|
|
148
|
+
emitSafe(emit, { kind: "deny", path, reason: `WrongTrace lock: ${owner}` });
|
|
149
|
+
return { action: "deny", reason: `WrongTrace lock: ${owner}` };
|
|
150
|
+
}
|
|
151
|
+
// Allow — and claim the lock so peers see this edit in flight.
|
|
152
|
+
const wt = await getWrongTrace();
|
|
153
|
+
if (wt.isAvailable) {
|
|
154
|
+
const owner = `wrongstack:${sessionId()}`;
|
|
155
|
+
const res = await wt.lockFile(path, "WrongStack edit in progress", {
|
|
156
|
+
owner,
|
|
157
|
+
ttlSeconds: 900,
|
|
158
|
+
});
|
|
159
|
+
if (res?.ok === true) {
|
|
160
|
+
acquireLock(counters, path);
|
|
161
|
+
emitSafe(emit, { kind: "lock-acquired", path, owner });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Peer grabbed it between the pre-flight and the claim (or our
|
|
165
|
+
// own earlier leak still holds it — the exemption let us through).
|
|
166
|
+
// Either way the file is being edited by someone: we proceed
|
|
167
|
+
// without re-claiming; coordination stays advisory.
|
|
168
|
+
emitSafe(emit, { kind: "lock-conflict-race", path });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (verdict.risk && verdict.risk.band === "fragile") {
|
|
172
|
+
emitSafe(emit, { kind: "allow-fragile", path, reasons: verdict.risk.reasons });
|
|
173
|
+
return {
|
|
174
|
+
action: "allow",
|
|
175
|
+
additionalContext: `WrongTrace: ${path} is fragile (${verdict.risk.reasons.join("; ")}). Prefer surgical AST diffs over rewrites.`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return { action: "allow" };
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Fail-open: coordination must never break the edit path.
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
async postToolUse(input) {
|
|
186
|
+
if (!EDIT_TOOLS.has(input.toolName ?? ""))
|
|
187
|
+
return;
|
|
188
|
+
const path = targetPathOf(input.toolInput);
|
|
189
|
+
// Only release a lock THIS pair claimed — a shared (module-level) map
|
|
190
|
+
// would let one executor free another's active lock. Reference counts:
|
|
191
|
+
// the daemon unlock happens only when the LAST sibling release lands.
|
|
192
|
+
if (!path || !counters.has(path))
|
|
193
|
+
return;
|
|
194
|
+
let shouldUnlock = false;
|
|
195
|
+
releaseLock(counters, path, () => {
|
|
196
|
+
shouldUnlock = true;
|
|
197
|
+
});
|
|
198
|
+
if (!shouldUnlock)
|
|
199
|
+
return; // a sibling still holds this path
|
|
200
|
+
emitSafe(emit, { kind: "lock-released", path });
|
|
201
|
+
try {
|
|
202
|
+
const wt = await getWrongTrace();
|
|
203
|
+
if (wt.isAvailable)
|
|
204
|
+
await wt.unlockFile(path);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// TTL backstop will reap it.
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// ── Legacy single-phase factories ─────────────────────────────────────────
|
|
213
|
+
// Old consumers register pre/post separately. They share the module-level
|
|
214
|
+
// reference-counted lock map, preserving the historical behaviour; new code
|
|
215
|
+
// should use `createWrongTraceHookPair` (per-runner scoping) instead.
|
|
216
|
+
//
|
|
217
|
+
// IMPORTANT: paired and legacy hooks use SEPARATE counters — a legacy
|
|
218
|
+
// postToolUse can never release a paired pair's claim (and vice versa). Do
|
|
219
|
+
// not mix the two styles for the same path in one process: the intended
|
|
220
|
+
// release would no-op and the daemon TTL would reap the lock.
|
|
221
|
+
const legacyLocks = newWrongTraceLockCounter();
|
|
222
|
+
export function createWrongTracePreToolUseHook(sessionId, opts) {
|
|
223
|
+
return createWrongTraceHookPair(sessionId, opts, legacyLocks).preToolUse;
|
|
224
|
+
}
|
|
225
|
+
export function createWrongTracePostToolUseHook(opts) {
|
|
226
|
+
return createWrongTraceHookPair(() => "", opts, legacyLocks).postToolUse;
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=hooks.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the @wrongstack/wrongtrace package.
|
|
3
|
+
*
|
|
4
|
+
* Callers do this and never worry about whether the daemon is running:
|
|
5
|
+
*
|
|
6
|
+
* import { getWrongTraceClient } from "@wrongstack/wrongtrace";
|
|
7
|
+
* const wt = await getWrongTraceClient();
|
|
8
|
+
* if (wt.isAvailable) { /* safe to use lock/lineage/telemetry APIs *\/ }
|
|
9
|
+
*/
|
|
10
|
+
export type { WrongTraceAtlasFile, WrongTraceAtlasQuery, WrongTraceAtlasSummary, WrongTraceClient, WrongTraceFileHealth, WrongTraceFrictionRow, WrongTraceHealth, WrongTraceLockInfo, WrongTraceLockOwnership, WrongTraceLockRequest, WrongTraceLockResult, WrongTraceRecentEvent, WrongTraceRecentEventsQuery, WrongTraceSymbolEvent, WrongTraceTelemetryReport, WrongTraceUnlockRequest, } from "./types.js";
|
|
11
|
+
export { discover, defaultSocketPath } from "./discovery.js";
|
|
12
|
+
export type { DiscoveryOptions, DiscoveryResult } from "./discovery.js";
|
|
13
|
+
export { createWrongTraceClient } from "./client.js";
|
|
14
|
+
export type { WrongTraceClientOptions, WrongTraceClientInternal } from "./client.js";
|
|
15
|
+
export { getCrossAgentRisk, summarizeFriction, getRecentActivity, digestAtlas, } from "./agent-helpers.js";
|
|
16
|
+
export type { CrossAgentRisk, FrictionSummary, AtlasDigest, RecentActivityEntry, } from "./agent-helpers.js";
|
|
17
|
+
export { createIpcTransport } from "./adapters/ipc.js";
|
|
18
|
+
export type { IpcTransport, IpcCallResult, IpcTimeouts } from "./adapters/ipc.js";
|
|
19
|
+
export { createMcpTransport } from "./adapters/mcp.js";
|
|
20
|
+
export type { McpTransport, McpToolBag, McpToolHandler, McpToolName } from "./adapters/mcp.js";
|
|
21
|
+
export { getWrongTrace, preflightFileEdit, resetWrongTraceGate, withFileLock, } from "./gate.js";
|
|
22
|
+
export type { PreflightOptions, PreflightVerdict } from "./gate.js";
|
|
23
|
+
export { createWrongTraceHookPair, createWrongTracePostToolUseHook, createWrongTracePreToolUseHook, } from "./hooks.js";
|
|
24
|
+
export type { WrongTraceGateDecisionEvent, WrongTraceHookInput, WrongTraceHookOptions, WrongTraceHookPair, WrongTracePreToolUseOutcome, } from "./hooks.js";
|
|
25
|
+
export { createWrongTraceGateCounter, countersFilePath, formatGateCounterReport, loadWrongTraceGateCounters, persistWrongTraceGateCounters, recordGateDecision, resetGateDecisions, snapshotGateDecisions, } from "./gate-counters.js";
|
|
26
|
+
export type { WrongTraceGateCounter, WrongTraceGateCounterSnapshot, } from "./gate-counters.js";
|
|
27
|
+
/**
|
|
28
|
+
* Drop-in replacement for the legacy `getWrongTraceClient()` from the
|
|
29
|
+
* reference TypeScript snippet. Kept as a one-liner alias for caller
|
|
30
|
+
* familiarity — implementation lives in `client.ts`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getWrongTraceClient(): Promise<import("./client.js").WrongTraceClientInternal>;
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the @wrongstack/wrongtrace package.
|
|
3
|
+
*
|
|
4
|
+
* Callers do this and never worry about whether the daemon is running:
|
|
5
|
+
*
|
|
6
|
+
* import { getWrongTraceClient } from "@wrongstack/wrongtrace";
|
|
7
|
+
* const wt = await getWrongTraceClient();
|
|
8
|
+
* if (wt.isAvailable) { /* safe to use lock/lineage/telemetry APIs *\/ }
|
|
9
|
+
*/
|
|
10
|
+
export { discover, defaultSocketPath } from "./discovery.js";
|
|
11
|
+
export { createWrongTraceClient } from "./client.js";
|
|
12
|
+
export { getCrossAgentRisk, summarizeFriction, getRecentActivity, digestAtlas, } from "./agent-helpers.js";
|
|
13
|
+
export { createIpcTransport } from "./adapters/ipc.js";
|
|
14
|
+
export { createMcpTransport } from "./adapters/mcp.js";
|
|
15
|
+
// ── Shared guardrail gate + hooks (CLI leader, fleet subagents, WebUI server) ──
|
|
16
|
+
export { getWrongTrace, preflightFileEdit, resetWrongTraceGate, withFileLock, } from "./gate.js";
|
|
17
|
+
export { createWrongTraceHookPair, createWrongTracePostToolUseHook, createWrongTracePreToolUseHook, } from "./hooks.js";
|
|
18
|
+
export { createWrongTraceGateCounter, countersFilePath, formatGateCounterReport, loadWrongTraceGateCounters, persistWrongTraceGateCounters, recordGateDecision, resetGateDecisions, snapshotGateDecisions, } from "./gate-counters.js";
|
|
19
|
+
/**
|
|
20
|
+
* Drop-in replacement for the legacy `getWrongTraceClient()` from the
|
|
21
|
+
* reference TypeScript snippet. Kept as a one-liner alias for caller
|
|
22
|
+
* familiarity — implementation lives in `client.ts`.
|
|
23
|
+
*/
|
|
24
|
+
export async function getWrongTraceClient() {
|
|
25
|
+
const { createWrongTraceClient } = await import("./client.js");
|
|
26
|
+
return createWrongTraceClient();
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=index.js.map
|