@skillstate/core 2.0.7 → 2.2.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.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @non-paper Wave-5 shared adapter plumbing — the SINGLE SOURCE OF TRUTH
3
+ * for the state-path resolution, atomic persistence, and hooks-merge
4
+ * mechanics that were duplicated across the platform adapters (claude,
5
+ * codex). Adapters stay EVENT TABLES + brand constants + script/config
6
+ * generators; everything below is host-agnostic:
7
+ *
8
+ * - {@link resolveTarget}: the `string | StatePathRef` resolution every
9
+ * adapter save/generate methods shared (throws on `..` traversal);
10
+ * - {@link saveGenerated}: resolve + `atomicWriteFile` + return the dest —
11
+ * the body of every adapter `save` method;
12
+ * - {@link mergeHookGroups}: the hooks.json / settings.json merge —
13
+ * idempotent by skillstate commands, appends fresh groups, preserves
14
+ * every foreign group and top-level key. Format specifics (which groups
15
+ * to generate, which commands are "ours") are passed in by the adapter.
16
+ */
17
+ import type { StatePathRef } from './atomic-write.js';
18
+ /**
19
+ * Resolve a `string | StatePathRef` target via `resolveStatePath` — the
20
+ * exact logic every adapter's private `resolve` helper shared. Raw strings
21
+ * pass through; `{ root, name }` refs are confined to `root` (traversal
22
+ * throws).
23
+ */
24
+ export declare function resolveTarget(target: string | StatePathRef): string;
25
+ /**
26
+ * Persist adapter-generated content to `target` via `atomicWriteFile`
27
+ * (temp sibling + fsync + rename) and return the absolute destination
28
+ * path. The shared body of the adapter `save*` methods.
29
+ */
30
+ export declare function saveGenerated(target: string | StatePathRef, content: string): Promise<string>;
31
+ /** Parameters for {@link mergeHookGroups} (the adapter-provided specifics). */
32
+ export interface MergeHookGroupsParams {
33
+ /** The existing hooks document text (hooks.json / settings.json). */
34
+ existingJson: string;
35
+ /** The adapter-generated `{ Event: [group, …] }` groups to append. */
36
+ generatedGroups: Record<string, unknown[]>;
37
+ /**
38
+ * JSON-stringified skillstate commands (one per generated event script)
39
+ * — a handler whose stringified `command` is in this set marks the
40
+ * document as already wired.
41
+ */
42
+ commandsOf: ReadonlySet<string>;
43
+ }
44
+ /**
45
+ * Merge the skillstate hook groups into an existing hooks document — the
46
+ * shared mechanics of the claude and codex `mergeHooksConfig` methods:
47
+ *
48
+ * - malformed/empty/non-object input starts from a fresh document (the
49
+ * CLI guards a live config before calling);
50
+ * - idempotent: if ANY skillstate command is already wired, the ORIGINAL
51
+ * text is returned byte-unchanged (no re-serialization, no duplicate
52
+ * groups, no surprise reformatting of a hand-written file);
53
+ * - otherwise every generated group is APPENDED per event: existing
54
+ * (non-skillstate) groups and every other top-level key survive, and a
55
+ * non-array event value is replaced by the fresh group.
56
+ *
57
+ * Returns the merged document re-serialized (2-space indent, newline-
58
+ * terminated) — or the untouched input on the already-wired path.
59
+ */
60
+ export declare function mergeHookGroups({ existingJson, generatedGroups, commandsOf, }: MergeHookGroupsParams): string;
61
+ //# sourceMappingURL=adapter-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-shared.d.ts","sourceRoot":"","sources":["../src/adapter-shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,MAAM,CAInE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,GAAG,YAAY,EAC7B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3C;;;;OAIG;IACH,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,EAC9B,YAAY,EACZ,eAAe,EACf,UAAU,GACX,EAAE,qBAAqB,GAAG,MAAM,CAoChC"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @non-paper Wave-5 shared adapter plumbing — the SINGLE SOURCE OF TRUTH
3
+ * for the state-path resolution, atomic persistence, and hooks-merge
4
+ * mechanics that were duplicated across the platform adapters (claude,
5
+ * codex). Adapters stay EVENT TABLES + brand constants + script/config
6
+ * generators; everything below is host-agnostic:
7
+ *
8
+ * - {@link resolveTarget}: the `string | StatePathRef` resolution every
9
+ * adapter save/generate methods shared (throws on `..` traversal);
10
+ * - {@link saveGenerated}: resolve + `atomicWriteFile` + return the dest —
11
+ * the body of every adapter `save` method;
12
+ * - {@link mergeHookGroups}: the hooks.json / settings.json merge —
13
+ * idempotent by skillstate commands, appends fresh groups, preserves
14
+ * every foreign group and top-level key. Format specifics (which groups
15
+ * to generate, which commands are "ours") are passed in by the adapter.
16
+ */
17
+ import { atomicWriteFile, resolveStatePath } from './atomic-write.js';
18
+ import { isPlainObject } from './hook-runtime.js';
19
+ /**
20
+ * Resolve a `string | StatePathRef` target via `resolveStatePath` — the
21
+ * exact logic every adapter's private `resolve` helper shared. Raw strings
22
+ * pass through; `{ root, name }` refs are confined to `root` (traversal
23
+ * throws).
24
+ */
25
+ export function resolveTarget(target) {
26
+ return typeof target === 'string'
27
+ ? target
28
+ : resolveStatePath(target.root, target.name);
29
+ }
30
+ /**
31
+ * Persist adapter-generated content to `target` via `atomicWriteFile`
32
+ * (temp sibling + fsync + rename) and return the absolute destination
33
+ * path. The shared body of the adapter `save*` methods.
34
+ */
35
+ export async function saveGenerated(target, content) {
36
+ const dest = resolveTarget(target);
37
+ await atomicWriteFile(dest, content);
38
+ return dest;
39
+ }
40
+ /**
41
+ * Merge the skillstate hook groups into an existing hooks document — the
42
+ * shared mechanics of the claude and codex `mergeHooksConfig` methods:
43
+ *
44
+ * - malformed/empty/non-object input starts from a fresh document (the
45
+ * CLI guards a live config before calling);
46
+ * - idempotent: if ANY skillstate command is already wired, the ORIGINAL
47
+ * text is returned byte-unchanged (no re-serialization, no duplicate
48
+ * groups, no surprise reformatting of a hand-written file);
49
+ * - otherwise every generated group is APPENDED per event: existing
50
+ * (non-skillstate) groups and every other top-level key survive, and a
51
+ * non-array event value is replaced by the fresh group.
52
+ *
53
+ * Returns the merged document re-serialized (2-space indent, newline-
54
+ * terminated) — or the untouched input on the already-wired path.
55
+ */
56
+ export function mergeHookGroups({ existingJson, generatedGroups, commandsOf, }) {
57
+ let doc = {};
58
+ try {
59
+ doc = JSON.parse(existingJson);
60
+ }
61
+ catch {
62
+ // Missing or malformed input: start from a fresh document.
63
+ }
64
+ if (!isPlainObject(doc)) {
65
+ doc = {};
66
+ }
67
+ if (!isPlainObject(doc['hooks'])) {
68
+ doc['hooks'] = {};
69
+ }
70
+ const hooks = doc['hooks'];
71
+ let alreadyWired = false;
72
+ for (const groups of Object.values(hooks)) {
73
+ if (!Array.isArray(groups))
74
+ continue;
75
+ for (const group of groups) {
76
+ if (!isPlainObject(group) || !Array.isArray(group['hooks']))
77
+ continue;
78
+ for (const handler of group['hooks']) {
79
+ if (isPlainObject(handler) && commandsOf.has(JSON.stringify(handler['command']))) {
80
+ alreadyWired = true;
81
+ }
82
+ }
83
+ }
84
+ }
85
+ if (alreadyWired) {
86
+ return existingJson;
87
+ }
88
+ for (const [event, groups] of Object.entries(generatedGroups)) {
89
+ const existing = Array.isArray(hooks[event])
90
+ ? hooks[event]
91
+ : [];
92
+ hooks[event] = [...existing, ...groups];
93
+ }
94
+ return `${JSON.stringify(doc, null, 2)}\n`;
95
+ }
96
+ //# sourceMappingURL=adapter-shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-shared.js","sourceRoot":"","sources":["../src/adapter-shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAEtE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,MAA6B;IACzD,OAAO,OAAO,MAAM,KAAK,QAAQ;QAC/B,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,gBAAgB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAA6B,EAC7B,OAAe;IAEf,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,IAAI,CAAC;AACd,CAAC;AAgBD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,YAAY,EACZ,eAAe,EACf,UAAU,GACY;IACtB,IAAI,GAAG,GAA4B,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAe,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,2DAA2D;IAC7D,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,GAAG,GAAG,EAAE,CAAC;IACX,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;IACpB,CAAC;IACD,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAA4B,CAAC;IACtD,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,SAAS;QACrC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAAE,SAAS;YACtE,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;gBACrC,IAAI,aAAa,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;oBACjF,YAAY,GAAG,IAAI,CAAC;gBACtB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,CAAC;QAC9D,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1C,CAAC,CAAE,KAAK,CAAC,KAAK,CAAe;YAC7B,CAAC,CAAC,EAAE,CAAC;QACP,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,QAAQ,EAAE,GAAG,MAAM,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC"}
@@ -30,6 +30,23 @@ export interface LockHandle {
30
30
  * - Unreadable/missing locations (stat fails) → `null`, never throws.
31
31
  */
32
32
  export declare function acquireLock(lockPath: string, ttlMs?: number): Promise<LockHandle | null>;
33
+ /** Default retry count for {@link withStateLock} (~10s at 50ms per try). */
34
+ export declare const DEFAULT_LOCK_RETRIES = 200;
35
+ /** Delay between {@link withStateLock} acquisition retries (50ms). */
36
+ export declare const LOCK_RETRY_DELAY_MS = 50;
37
+ /**
38
+ * Run `fn` while holding an exclusive cross-process lock on
39
+ * `statePath + '.lock'` ({@link acquireLock}, default TTL). Waits for a
40
+ * live holder: up to `retries` (default {@link DEFAULT_LOCK_RETRIES})
41
+ * attempts {@link LOCK_RETRY_DELAY_MS} apart, so 2-3 concurrent agent
42
+ * processes serialize instead of failing. The lock is acquired before
43
+ * `fn` starts and ALWAYS released in the `finally` block; the `fn`
44
+ * result (or failure) propagates after the release. Throws when the lock
45
+ * cannot be acquired within the retry budget. This is the async
46
+ * hot-path serialization primitive for the MCP server writes
47
+ * (patch/rollback/checkpoint/merge) and the host adapters.
48
+ */
49
+ export declare function withStateLock<T>(statePath: string, fn: () => Promise<T> | T, ttlMs?: number, retries?: number): Promise<T>;
33
50
  /**
34
51
  * Resolve `name` inside `root` and return the absolute path. Throws when
35
52
  * the result escapes `root` (`..` traversal or an absolute outsider).
@@ -1 +1 @@
1
- {"version":3,"file":"atomic-write.d.ts","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"AAkBA,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,UAAU,GAC3B,OAAO,CAAC,IAAI,CAAC,CAYf;AAED,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAUD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAwB5B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAOnE"}
1
+ {"version":3,"file":"atomic-write.d.ts","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"AAkBA,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,UAAU,GAC3B,OAAO,CAAC,IAAI,CAAC,CAYf;AAED,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAUD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAwB5B;AAED,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACxB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,GAAE,MAA6B,GACrC,OAAO,CAAC,CAAC,CAAC,CAgBZ;AAeD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAOnE"}
@@ -80,6 +80,53 @@ export async function acquireLock(lockPath, ttlMs) {
80
80
  return makeHandle(lockPath);
81
81
  }
82
82
  }
83
+ /** Default retry count for {@link withStateLock} (~10s at 50ms per try). */
84
+ export const DEFAULT_LOCK_RETRIES = 200;
85
+ /** Delay between {@link withStateLock} acquisition retries (50ms). */
86
+ export const LOCK_RETRY_DELAY_MS = 50;
87
+ /**
88
+ * Run `fn` while holding an exclusive cross-process lock on
89
+ * `statePath + '.lock'` ({@link acquireLock}, default TTL). Waits for a
90
+ * live holder: up to `retries` (default {@link DEFAULT_LOCK_RETRIES})
91
+ * attempts {@link LOCK_RETRY_DELAY_MS} apart, so 2-3 concurrent agent
92
+ * processes serialize instead of failing. The lock is acquired before
93
+ * `fn` starts and ALWAYS released in the `finally` block; the `fn`
94
+ * result (or failure) propagates after the release. Throws when the lock
95
+ * cannot be acquired within the retry budget. This is the async
96
+ * hot-path serialization primitive for the MCP server writes
97
+ * (patch/rollback/checkpoint/merge) and the host adapters.
98
+ */
99
+ export async function withStateLock(statePath, fn, ttlMs, retries = DEFAULT_LOCK_RETRIES) {
100
+ const lockPath = `${statePath}.lock`;
101
+ await fs.promises.mkdir(path.dirname(lockPath), { recursive: true });
102
+ let handle = await acquireLockSafely(lockPath, ttlMs);
103
+ for (let attempt = 0; handle === null && attempt < retries; attempt++) {
104
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
105
+ handle = await acquireLockSafely(lockPath, ttlMs);
106
+ }
107
+ if (!handle) {
108
+ throw new Error(`skillstate: could not acquire the state lock: ${lockPath}`);
109
+ }
110
+ try {
111
+ return await fn();
112
+ }
113
+ finally {
114
+ handle.release();
115
+ }
116
+ }
117
+ /**
118
+ * {@link acquireLock} with the stale-takeover race collapsed to `null`:
119
+ * between `unlink` and the re-`writeFile`, another waiter may recreate
120
+ * the lockfile (`EEXIST`) — that loser must retry, never crash.
121
+ */
122
+ async function acquireLockSafely(lockPath, ttlMs) {
123
+ try {
124
+ return await acquireLock(lockPath, ttlMs);
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
83
130
  /**
84
131
  * Resolve `name` inside `root` and return the absolute path. Throws when
85
132
  * the result escapes `root` (`..` traversal or an absolute outsider).
@@ -1 +1 @@
1
- {"version":3,"file":"atomic-write.js","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAY1C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAgB,EAChB,OAA4B;IAE5B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,GAAG,QAAQ,QAAQ,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACpF,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAChD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IACD,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAOD,SAAS,UAAU,CAAC,QAAgB;IAClC,OAAO;QACL,OAAO,EAAE,GAAS,EAAE;YAClB,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAAgB,EAChB,KAAc;IAEd,MAAM,GAAG,GAAG,KAAK,IAAI,mBAAmB,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;YACzD,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;QACH,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC9C,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,IAAI,GAAG,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;YACzD,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;QACH,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,IAAY;IACzD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACxC,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"atomic-write.js","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @non-paper Wave-5 hook runtime — the SINGLE SOURCE OF TRUTH for every
3
+ * piece of logic embedded into self-contained generated hook scripts
4
+ * (Claude Code `.cjs` hooks, Codex `.cjs` hooks) and reused directly by
5
+ * the OpenCode plugin.
6
+ *
7
+ * CONSTRAINT: the generated scripts cannot import `@skillstate/*`, so the
8
+ * adapters inline these functions into the emitted `.cjs` via
9
+ * `fn.toString()` (see {@link hookRuntimeSnippet}). That means every
10
+ * function body below must be PLAIN JavaScript:
11
+ *
12
+ * - no imports and no `require` — dependencies are passed as parameters
13
+ * (`readFile`, `writeFile`, `home`), so the caller wires the real
14
+ * `node:fs`/`node:os`/`node:path` (or test mocks) at the call site;
15
+ * - type annotations live ONLY in the signatures — both `tsc` (dist) and
16
+ * the vitest/esbuild transform erase them, so `fn.toString()` yields a
17
+ * valid CJS snippet;
18
+ * - no references to module-scope helpers — each function is either fully
19
+ * self-contained or calls only its sibling functions from this module
20
+ * (which the snippet inlines together).
21
+ *
22
+ * The parity suite (`tests/core/hook-runtime-parity.test.ts`) evals the
23
+ * snippets extracted from the generated scripts and asserts byte-identical
24
+ * behavior against these originals, so the "embedded copy" can no longer
25
+ * drift from the source of truth.
26
+ */
27
+ /** True for plain (non-null, non-array) objects. */
28
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
29
+ /**
30
+ * Agent-id sanitization: keep `[A-Za-z0-9_-]` runs, collapse everything
31
+ * else into single `-`, trim edge dashes, cap at 64 chars. Garbage input
32
+ * sanitizes to `''` — callers treat that as "no agent" (the main state).
33
+ */
34
+ export declare function sanitizeAgentId(agentId: string): string;
35
+ /**
36
+ * Agent id from a host session id (Claude Code / Codex hook stdin carry
37
+ * `session_id`; opencode hooks carry `sessionID`): the short prefix — the
38
+ * first 8 characters — keeps agent directories bounded while remaining
39
+ * unique enough per session. Non-string/empty input yields `''`.
40
+ */
41
+ export declare function resolveAgentIdFromSession(sessionId: unknown): string;
42
+ /**
43
+ * Resolve the per-project state file for a working directory — the pure,
44
+ * dependency-free mirror of `resolveHostStateForCwd`
45
+ * (`<cwd>/.skillstate/skillstate.json`; the global bucket
46
+ * `<home>/.skillstate/global/skillstate.json` when cwd equals home).
47
+ *
48
+ * AGENT-SCOPED STATE: a non-empty `agentId` (sanitized via
49
+ * {@link sanitizeAgentId}) scopes the file under an isolated
50
+ * `agents/<agentId>/` copy — parallel sub-agents (hook sessions) never
51
+ * share the main state file.
52
+ *
53
+ * String arithmetic only (POSIX): absolute paths are normalized like
54
+ * `path.resolve` (empty/`.` segments dropped, `..` popped, trailing
55
+ * slashes trimmed); relative inputs stay relative because there is no
56
+ * `process` access. Callers that may see relative paths resolve them
57
+ * first (`path.resolve(cwd)`) — the generated hook scripts do exactly
58
+ * that. `home` must be provided to detect the global bucket; when it is
59
+ * omitted the project path is returned.
60
+ */
61
+ export declare function resolveStatePathForCwd(cwd: string, home?: string, agentId?: string): string;
62
+ /**
63
+ * Read the state file through the injected `readFile` (real `fs` in
64
+ * generated scripts, mocks in tests). The on-disk envelope is
65
+ * `{ version: 1, state }`; a bare object is tolerated and treated as the
66
+ * state itself; anything else (missing file, corrupt JSON, arrays,
67
+ * scalars) yields `{}` — best-effort, never throws.
68
+ */
69
+ export declare function readStateEnvelope(statePath: string, readFile: (p: string) => string): unknown;
70
+ /**
71
+ * Persist the state through the injected `writeFile` as the
72
+ * `{ version: 1, state }` envelope (pretty-printed, newline-terminated).
73
+ * Throws on failure — the caller decides whether to swallow it (OpenCode
74
+ * plugin) or surface a `systemMessage` (PostToolUse hooks).
75
+ */
76
+ export declare function saveStateEnvelope(statePath: string, state: object, writeFile: (p: string, data: string) => void): void;
77
+ /**
78
+ * Minimal `node:fs` surface {@link lockStateWrite} needs. Injections keep
79
+ * the hook-runtime pure: real `node:fs` in generated scripts and the
80
+ * plugin, mocks in tests (the established hook-runtime dependency style).
81
+ */
82
+ export interface StateLockFs {
83
+ openSync(path: string, flags: string): number;
84
+ closeSync(fd: number): void;
85
+ statSync(path: string): {
86
+ mtimeMs: number;
87
+ };
88
+ unlinkSync(path: string): void;
89
+ mkdirSync(path: string): unknown;
90
+ }
91
+ /**
92
+ * Block the current thread for `ms` milliseconds — the sync sleep for the
93
+ * {@link lockStateWrite} retry loop. Prefers `Atomics.wait` on a shared
94
+ * integer (a real sleep); falls back to a busy spin when the primitive is
95
+ * unavailable. Plain JS — survives `fn.toString()` inlining.
96
+ */
97
+ export declare function sleepSync(ms: number): void;
98
+ /**
99
+ * Cross-process state lock for the SELF-CONTAINED hook scripts (sync,
100
+ * no imports): exclusive `O_EXCL` lockfile at `statePath + '.lock'`,
101
+ * stale-TTL takeover (10s — crashed hook holders), and a retry loop
102
+ * (50ms × 40 ≈ 2s) while a live holder runs its critical section. The
103
+ * injected `fs` (real `node:fs` in generated scripts and the plugin,
104
+ * mocks in tests) performs the lockfile I/O and creates the parent
105
+ * directory. The injected `fn` runs inside the lock; the lockfile is
106
+ * ALWAYS removed in the `finally` block. `fn` failures propagate AFTER
107
+ * the release; lock exhaustion throws.
108
+ */
109
+ export declare function lockStateWrite(statePath: string, fs: StateLockFs, fn: () => unknown): unknown;
110
+ /**
111
+ * Paper ⊕ merge: `null` deletes a key, nested plain objects merge
112
+ * recursively, everything else replaces. Pure — neither `state` nor
113
+ * `patch` is mutated.
114
+ */
115
+ export declare function mergePatch(state: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
116
+ /**
117
+ * Coerce a tool response into text: strings pass through, plain objects
118
+ * expose their `content` then `text` string field, other objects are
119
+ * JSON-stringified, null/undefined becomes `""`, everything else is
120
+ * `String()`-ed.
121
+ */
122
+ export declare function readResponseText(response: unknown): string;
123
+ /** Outcome of the patch extractors: a patch, an invalid attempt, or nothing. */
124
+ export type PatchLookup = {
125
+ patch: Record<string, unknown>;
126
+ } | {
127
+ invalid: true;
128
+ } | {
129
+ absent: true;
130
+ };
131
+ /**
132
+ * Look for a fenced ```json block: `{ patch }` when it parses and carries
133
+ * an object-shaped `state_patch`, `{ invalid: true }` when a block exists
134
+ * but is malformed (an open fence that never closes — truncated output —
135
+ * is still a patch attempt and classifies as invalid), `{ absent: true }`
136
+ * when there is no block at all.
137
+ */
138
+ export declare function findFencedPatch(text: string): PatchLookup;
139
+ /**
140
+ * Fallback: a raw JSON object with `state_patch` anywhere in the text
141
+ * (first `{` … last `}`). Ordinary JSON output without a `state_patch`
142
+ * key is simply not a patch (`{ absent: true }`); a `state_patch` key
143
+ * holding a non-object is an invalid attempt.
144
+ */
145
+ export declare function findRawPatch(text: string): PatchLookup;
146
+ /**
147
+ * Read the session-meta sidecar's status (`<dir>/.session-meta.json`,
148
+ * sibling of the state file). Plain JS — inlined into the generated
149
+ * SessionStart hooks via `fn.toString()`. Returns the `status` string for
150
+ * a readable object-shaped sidecar, else `null` (missing/corrupt/other
151
+ * shape — no lifecycle marker). The `readFile` dependency is injected at
152
+ * the call site (real `node:fs` in hooks, mocks in tests).
153
+ */
154
+ export declare function readSessionMetaStatus(metaPath: string, readFile: (p: string) => string): string | null;
155
+ /**
156
+ * Assemble the CJS snippet embedded into every generated hook script:
157
+ * all sibling functions of this module, source-verbatim via
158
+ * `fn.toString()`. The adapters splice this block after their `require`
159
+ * preamble, which keeps the embedded logic byte-identical across hosts
160
+ * and impossible to drift from this module.
161
+ */
162
+ export declare function hookRuntimeSnippet(): string;
163
+ //# sourceMappingURL=hook-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-runtime.d.ts","sourceRoot":"","sources":["../src/hook-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE9E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKvD;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,CAGpE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA+B3F;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAC9B,OAAO,CAaT;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAC3C,IAAI,CAEN;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9C,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAW1C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,OAAO,GAAG,OAAO,CA8C7F;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAQ1D;AAED,gFAAgF;AAChF,MAAM,MAAM,WAAW,GACnB;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAClC;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,IAAI,CAAA;CAAE,CAAC;AAErB;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAczD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CA4BtD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAC9B,MAAM,GAAG,IAAI,CAUf;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAkB3C"}