@praxisflux/gates 0.21.0 → 0.22.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.
@@ -5,13 +5,16 @@
5
5
  // "run every applicable gate" happens naturally across plugins; within one plugin the runner
6
6
  // runs that plugin's gates additively over every root they resolve.
7
7
  //
8
- // A gate is: { name, resolveRoots(startDir) -> string[], check(root) -> string[] (problems),
9
- // warn?(root) -> string[] (non-blocking notices) }.
8
+ // A gate is: { name, resolveRoots(startDir, ctx) -> string[], check(root, ctx) -> string[]
9
+ // (problems), warn?(root, ctx) -> string[] (non-blocking notices) }.
10
10
  // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
11
  // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
12
  // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ // `ctx` is { sessionId, input }: the invoking session's identity (hook input `session_id`,
14
+ // falling back to $CLAUDE_CODE_SESSION_ID) plus the raw hook input — gates that scope state
15
+ // to its owning session (e.g. reorient run records) key off it; every existing gate ignores it.
13
16
  //
14
- // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, cwd, … };
17
+ // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, session_id, cwd, … };
15
18
  // exit 0 = allow the model to stop; exit 2 = block, and stderr becomes the message it sees.
16
19
 
17
20
  /** Read all of stdin as a string (empty string on a TTY, so it's safe to run by hand). */
@@ -32,17 +35,21 @@ export function readStdin() {
32
35
  export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
33
36
  if (input && input.stop_hook_active === true) return { block: false, message: "", warnings: "" };
34
37
  const start = process.env.CLAUDE_PROJECT_DIR || (input && input.cwd) || cwd;
38
+ const ctx = {
39
+ sessionId: (input && input.session_id) || process.env.CLAUDE_CODE_SESSION_ID || null,
40
+ input: input || {},
41
+ };
35
42
  const problems = [];
36
43
  const warnings = [];
37
44
  for (const gate of gates) {
38
45
  let roots = [];
39
- try { roots = gate.resolveRoots(start) || []; } catch { roots = []; }
46
+ try { roots = gate.resolveRoots(start, ctx) || []; } catch { roots = []; }
40
47
  for (const root of roots) {
41
- try { problems.push(...(gate.check(root) || [])); } catch (e) {
48
+ try { problems.push(...(gate.check(root, ctx) || [])); } catch (e) {
42
49
  problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);
43
50
  }
44
51
  if (typeof gate.warn === "function") {
45
- try { warnings.push(...(gate.warn(root) || [])); } catch { /* warnings are best-effort */ }
52
+ try { warnings.push(...(gate.warn(root, ctx) || [])); } catch { /* warnings are best-effort */ }
46
53
  }
47
54
  }
48
55
  }
@@ -52,12 +59,17 @@ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
52
59
  /**
53
60
  * Full harness: read stdin, evaluate the gates, exit 0 (allow) or 2 (block, message on stderr).
54
61
  * Blocking problems win; otherwise any non-blocking warnings are written to stderr and we still
55
- * allow the stop (exit 0).
62
+ * allow the stop (exit 0). Optional `before(input)` runs after parsing and before the gates —
63
+ * the hook point for a plugin's own writer to do session-owned upkeep (e.g. heartbeat a run
64
+ * record); it is best-effort and never blocks the stop.
56
65
  */
57
- export async function runStopHook({ gates, exit = process.exit } = {}) {
66
+ export async function runStopHook({ gates, before, exit = process.exit } = {}) {
58
67
  const raw = await readStdin();
59
68
  let input = {};
60
69
  try { input = JSON.parse(raw || "{}"); } catch { input = {}; }
70
+ if (typeof before === "function") {
71
+ try { await before(input); } catch { /* upkeep is best-effort */ }
72
+ }
61
73
  const { block, message, warnings } = evaluate(input, gates);
62
74
  if (block) {
63
75
  process.stderr.write([message, warnings].filter(Boolean).join("\n") + "\n");
@@ -5,13 +5,16 @@
5
5
  // "run every applicable gate" happens naturally across plugins; within one plugin the runner
6
6
  // runs that plugin's gates additively over every root they resolve.
7
7
  //
8
- // A gate is: { name, resolveRoots(startDir) -> string[], check(root) -> string[] (problems),
9
- // warn?(root) -> string[] (non-blocking notices) }.
8
+ // A gate is: { name, resolveRoots(startDir, ctx) -> string[], check(root, ctx) -> string[]
9
+ // (problems), warn?(root, ctx) -> string[] (non-blocking notices) }.
10
10
  // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
11
  // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
12
  // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ // `ctx` is { sessionId, input }: the invoking session's identity (hook input `session_id`,
14
+ // falling back to $CLAUDE_CODE_SESSION_ID) plus the raw hook input — gates that scope state
15
+ // to its owning session (e.g. reorient run records) key off it; every existing gate ignores it.
13
16
  //
14
- // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, cwd, … };
17
+ // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, session_id, cwd, … };
15
18
  // exit 0 = allow the model to stop; exit 2 = block, and stderr becomes the message it sees.
16
19
 
17
20
  /** Read all of stdin as a string (empty string on a TTY, so it's safe to run by hand). */
@@ -32,17 +35,21 @@ export function readStdin() {
32
35
  export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
33
36
  if (input && input.stop_hook_active === true) return { block: false, message: "", warnings: "" };
34
37
  const start = process.env.CLAUDE_PROJECT_DIR || (input && input.cwd) || cwd;
38
+ const ctx = {
39
+ sessionId: (input && input.session_id) || process.env.CLAUDE_CODE_SESSION_ID || null,
40
+ input: input || {},
41
+ };
35
42
  const problems = [];
36
43
  const warnings = [];
37
44
  for (const gate of gates) {
38
45
  let roots = [];
39
- try { roots = gate.resolveRoots(start) || []; } catch { roots = []; }
46
+ try { roots = gate.resolveRoots(start, ctx) || []; } catch { roots = []; }
40
47
  for (const root of roots) {
41
- try { problems.push(...(gate.check(root) || [])); } catch (e) {
48
+ try { problems.push(...(gate.check(root, ctx) || [])); } catch (e) {
42
49
  problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);
43
50
  }
44
51
  if (typeof gate.warn === "function") {
45
- try { warnings.push(...(gate.warn(root) || [])); } catch { /* warnings are best-effort */ }
52
+ try { warnings.push(...(gate.warn(root, ctx) || [])); } catch { /* warnings are best-effort */ }
46
53
  }
47
54
  }
48
55
  }
@@ -52,12 +59,17 @@ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
52
59
  /**
53
60
  * Full harness: read stdin, evaluate the gates, exit 0 (allow) or 2 (block, message on stderr).
54
61
  * Blocking problems win; otherwise any non-blocking warnings are written to stderr and we still
55
- * allow the stop (exit 0).
62
+ * allow the stop (exit 0). Optional `before(input)` runs after parsing and before the gates —
63
+ * the hook point for a plugin's own writer to do session-owned upkeep (e.g. heartbeat a run
64
+ * record); it is best-effort and never blocks the stop.
56
65
  */
57
- export async function runStopHook({ gates, exit = process.exit } = {}) {
66
+ export async function runStopHook({ gates, before, exit = process.exit } = {}) {
58
67
  const raw = await readStdin();
59
68
  let input = {};
60
69
  try { input = JSON.parse(raw || "{}"); } catch { input = {}; }
70
+ if (typeof before === "function") {
71
+ try { await before(input); } catch { /* upkeep is best-effort */ }
72
+ }
61
73
  const { block, message, warnings } = evaluate(input, gates);
62
74
  if (block) {
63
75
  process.stderr.write([message, warnings].filter(Boolean).join("\n") + "\n");
@@ -5,13 +5,16 @@
5
5
  // "run every applicable gate" happens naturally across plugins; within one plugin the runner
6
6
  // runs that plugin's gates additively over every root they resolve.
7
7
  //
8
- // A gate is: { name, resolveRoots(startDir) -> string[], check(root) -> string[] (problems),
9
- // warn?(root) -> string[] (non-blocking notices) }.
8
+ // A gate is: { name, resolveRoots(startDir, ctx) -> string[], check(root, ctx) -> string[]
9
+ // (problems), warn?(root, ctx) -> string[] (non-blocking notices) }.
10
10
  // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
11
  // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
12
  // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ // `ctx` is { sessionId, input }: the invoking session's identity (hook input `session_id`,
14
+ // falling back to $CLAUDE_CODE_SESSION_ID) plus the raw hook input — gates that scope state
15
+ // to its owning session (e.g. reorient run records) key off it; every existing gate ignores it.
13
16
  //
14
- // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, cwd, … };
17
+ // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, session_id, cwd, … };
15
18
  // exit 0 = allow the model to stop; exit 2 = block, and stderr becomes the message it sees.
16
19
 
17
20
  /** Read all of stdin as a string (empty string on a TTY, so it's safe to run by hand). */
@@ -32,17 +35,21 @@ export function readStdin() {
32
35
  export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
33
36
  if (input && input.stop_hook_active === true) return { block: false, message: "", warnings: "" };
34
37
  const start = process.env.CLAUDE_PROJECT_DIR || (input && input.cwd) || cwd;
38
+ const ctx = {
39
+ sessionId: (input && input.session_id) || process.env.CLAUDE_CODE_SESSION_ID || null,
40
+ input: input || {},
41
+ };
35
42
  const problems = [];
36
43
  const warnings = [];
37
44
  for (const gate of gates) {
38
45
  let roots = [];
39
- try { roots = gate.resolveRoots(start) || []; } catch { roots = []; }
46
+ try { roots = gate.resolveRoots(start, ctx) || []; } catch { roots = []; }
40
47
  for (const root of roots) {
41
- try { problems.push(...(gate.check(root) || [])); } catch (e) {
48
+ try { problems.push(...(gate.check(root, ctx) || [])); } catch (e) {
42
49
  problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);
43
50
  }
44
51
  if (typeof gate.warn === "function") {
45
- try { warnings.push(...(gate.warn(root) || [])); } catch { /* warnings are best-effort */ }
52
+ try { warnings.push(...(gate.warn(root, ctx) || [])); } catch { /* warnings are best-effort */ }
46
53
  }
47
54
  }
48
55
  }
@@ -52,12 +59,17 @@ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
52
59
  /**
53
60
  * Full harness: read stdin, evaluate the gates, exit 0 (allow) or 2 (block, message on stderr).
54
61
  * Blocking problems win; otherwise any non-blocking warnings are written to stderr and we still
55
- * allow the stop (exit 0).
62
+ * allow the stop (exit 0). Optional `before(input)` runs after parsing and before the gates —
63
+ * the hook point for a plugin's own writer to do session-owned upkeep (e.g. heartbeat a run
64
+ * record); it is best-effort and never blocks the stop.
56
65
  */
57
- export async function runStopHook({ gates, exit = process.exit } = {}) {
66
+ export async function runStopHook({ gates, before, exit = process.exit } = {}) {
58
67
  const raw = await readStdin();
59
68
  let input = {};
60
69
  try { input = JSON.parse(raw || "{}"); } catch { input = {}; }
70
+ if (typeof before === "function") {
71
+ try { await before(input); } catch { /* upkeep is best-effort */ }
72
+ }
61
73
  const { block, message, warnings } = evaluate(input, gates);
62
74
  if (block) {
63
75
  process.stderr.write([message, warnings].filter(Boolean).join("\n") + "\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisflux/gates",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "praxisflux gate checks as a zero-dependency CLI (spec-bridge, wiki-freshness, course) — status can't exceed proven artifacts",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -5,13 +5,16 @@
5
5
  // "run every applicable gate" happens naturally across plugins; within one plugin the runner
6
6
  // runs that plugin's gates additively over every root they resolve.
7
7
  //
8
- // A gate is: { name, resolveRoots(startDir) -> string[], check(root) -> string[] (problems),
9
- // warn?(root) -> string[] (non-blocking notices) }.
8
+ // A gate is: { name, resolveRoots(startDir, ctx) -> string[], check(root, ctx) -> string[]
9
+ // (problems), warn?(root, ctx) -> string[] (non-blocking notices) }.
10
10
  // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
11
  // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
12
  // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ // `ctx` is { sessionId, input }: the invoking session's identity (hook input `session_id`,
14
+ // falling back to $CLAUDE_CODE_SESSION_ID) plus the raw hook input — gates that scope state
15
+ // to its owning session (e.g. reorient run records) key off it; every existing gate ignores it.
13
16
  //
14
- // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, cwd, … };
17
+ // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, session_id, cwd, … };
15
18
  // exit 0 = allow the model to stop; exit 2 = block, and stderr becomes the message it sees.
16
19
 
17
20
  /** Read all of stdin as a string (empty string on a TTY, so it's safe to run by hand). */
@@ -32,17 +35,21 @@ export function readStdin() {
32
35
  export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
33
36
  if (input && input.stop_hook_active === true) return { block: false, message: "", warnings: "" };
34
37
  const start = process.env.CLAUDE_PROJECT_DIR || (input && input.cwd) || cwd;
38
+ const ctx = {
39
+ sessionId: (input && input.session_id) || process.env.CLAUDE_CODE_SESSION_ID || null,
40
+ input: input || {},
41
+ };
35
42
  const problems = [];
36
43
  const warnings = [];
37
44
  for (const gate of gates) {
38
45
  let roots = [];
39
- try { roots = gate.resolveRoots(start) || []; } catch { roots = []; }
46
+ try { roots = gate.resolveRoots(start, ctx) || []; } catch { roots = []; }
40
47
  for (const root of roots) {
41
- try { problems.push(...(gate.check(root) || [])); } catch (e) {
48
+ try { problems.push(...(gate.check(root, ctx) || [])); } catch (e) {
42
49
  problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);
43
50
  }
44
51
  if (typeof gate.warn === "function") {
45
- try { warnings.push(...(gate.warn(root) || [])); } catch { /* warnings are best-effort */ }
52
+ try { warnings.push(...(gate.warn(root, ctx) || [])); } catch { /* warnings are best-effort */ }
46
53
  }
47
54
  }
48
55
  }
@@ -52,12 +59,17 @@ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
52
59
  /**
53
60
  * Full harness: read stdin, evaluate the gates, exit 0 (allow) or 2 (block, message on stderr).
54
61
  * Blocking problems win; otherwise any non-blocking warnings are written to stderr and we still
55
- * allow the stop (exit 0).
62
+ * allow the stop (exit 0). Optional `before(input)` runs after parsing and before the gates —
63
+ * the hook point for a plugin's own writer to do session-owned upkeep (e.g. heartbeat a run
64
+ * record); it is best-effort and never blocks the stop.
56
65
  */
57
- export async function runStopHook({ gates, exit = process.exit } = {}) {
66
+ export async function runStopHook({ gates, before, exit = process.exit } = {}) {
58
67
  const raw = await readStdin();
59
68
  let input = {};
60
69
  try { input = JSON.parse(raw || "{}"); } catch { input = {}; }
70
+ if (typeof before === "function") {
71
+ try { await before(input); } catch { /* upkeep is best-effort */ }
72
+ }
61
73
  const { block, message, warnings } = evaluate(input, gates);
62
74
  if (block) {
63
75
  process.stderr.write([message, warnings].filter(Boolean).join("\n") + "\n");