@devrik-tools/claude-gates 0.4.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +39 -4
  3. package/README.md +34 -5
  4. package/cli/config.mjs +126 -124
  5. package/cli/init.mjs +303 -276
  6. package/cli/install.mjs +281 -175
  7. package/cli/materialize.mjs +103 -102
  8. package/cli/registry.mjs +139 -136
  9. package/cli/smoke-fixtures.json +65 -0
  10. package/cli/task.mjs +140 -140
  11. package/package.json +1 -1
  12. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  13. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  14. package/plugins/gates/hooks/doctor.mjs +207 -207
  15. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  16. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  17. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  18. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  19. package/plugins/gates/hooks/gates/brief-approved/index.mjs +216 -0
  20. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  21. package/plugins/gates/hooks/gates/capability-map/index.mjs +701 -0
  22. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  23. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  24. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  25. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  26. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  27. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  28. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  29. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  30. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  31. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  32. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  33. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  34. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  35. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  36. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  37. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  38. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  39. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  40. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  41. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  42. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  43. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  44. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  45. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  46. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  47. package/plugins/gates/hooks/hooks.json +61 -0
  48. package/plugins/gates/hooks/lib/config.mjs +179 -172
  49. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  50. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  51. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  52. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  53. package/plugins/tasks/hooks/hooks.json +26 -26
  54. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  55. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  56. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  57. package/registry.json +192 -1
@@ -1,145 +1,145 @@
1
- // register-requests — UserPromptSubmit hook. Runs on every user message, BEFORE the
2
- // assistant responds. It is the deterministic half of the task system: it never decides
3
- // what is a task (that judgment is the assistant's — the hook only asks it to judge), it
4
- // only makes sure the judgment is asked and open tasks are not forgotten. Two jobs:
5
- // 1. On every message, ask the assistant to classify: if this message is a new task,
6
- // register it via `claude-gates task add` (persistence is deterministic — the store —
7
- // but the classification itself is the model's judgment, never a regex here).
8
- // 2. Every `remindEveryMessages` messages (counted via the store's counter), also recite
9
- // the currently active tasks so a long or compacted conversation never loses the
10
- // thread of what is still pending.
11
- //
12
- // A UserPromptSubmit hook's stdout is added to the assistant's context as plain text, so
13
- // both are simply written to stdout. Self-contained: Node built-ins + the task-store lib
14
- // only, no npm at runtime.
15
- //
16
- // justification: no existing tool covers this. The task store and the audit were done when
17
- // task-store.mjs was written (see its header); this file is the hook wiring on top.
18
-
19
- import { existsSync, readFileSync } from 'node:fs';
20
- import { homedir } from 'node:os';
21
- import { dirname, join } from 'node:path';
22
- import { openTaskStore } from './lib/task-store.mjs';
23
-
24
- const STDIN_FILE_DESCRIPTOR = 0;
25
- const CONFIG_KEY = 'remindOpenTasks';
26
- const DEFAULT_REMIND_EVERY_MESSAGES = 5;
27
- const MAX_TASKS_SHOWN = 12;
28
- const DUMP_ENV = 'CLAUDE_GATES_DUMP_DEFAULTS';
29
-
30
- // Config lookup mirrors the gates' config.mjs (project → global), kept local so this plugin
31
- // stays independent of the gates plugin.
32
- const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
33
- const PROJECT_CONFIG = join('.ai', 'config.json');
34
- const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
35
-
36
- function readJson(path) {
37
- try {
38
- return JSON.parse(readFileSync(path, 'utf8'));
39
- } catch {
40
- return null;
41
- }
42
- }
43
-
44
- function projectRootOf(startDirectory) {
45
- let current = startDirectory;
46
- while (true) {
47
- if (
48
- PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
49
- ) {
50
- return current;
51
- }
52
- const parent = dirname(current);
53
- if (parent === current) return null;
54
- current = parent;
55
- }
56
- }
57
-
58
- /** The tasks gate config entry (project overrides global), or null. */
59
- function taskConfig(startDirectory) {
60
- const root = projectRootOf(startDirectory);
61
- const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
62
- const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
63
- const layer = projectData?.gates ?? globalData?.gates ?? {};
64
- const entry = layer[CONFIG_KEY];
65
- if (typeof entry === 'boolean') return { enabled: entry };
66
- if (entry && typeof entry === 'object') return entry;
67
- return null;
68
- }
69
-
70
- function readPayload() {
71
- try {
72
- return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
73
- } catch {
74
- return {};
75
- }
76
- }
77
-
78
- // Asked on every message. The hook does not classify — it asks the model to, and the
79
- // model is the one that persists (via the CLI, which enforces the store's own rules,
80
- // e.g. evidence on close). This is legitimate: the hook is not depending on the model to
81
- // make something OCCUR deterministically; it is depending on the model's judgment for a
82
- // judgment call (whether this message describes a new task), same shape as ask-adoption.
83
- const CLASSIFY_PROMPT =
84
- '[tasks] MANDATORY before you respond: judge whether the message you just received describes ' +
85
- 'a new actionable task (not a question, not small talk, not a follow-up on an already-registered ' +
86
- 'task). If it does, you MUST register it now, before writing your reply — run ' +
87
- '`claude-gates task add "<title>" [--description <text>] [--size <size>]` from the project root ' +
88
- '(or the CLI\'s absolute path if `claude-gates` is not on PATH). Do not defer this, do not decide ' +
89
- 'to register it "later", do not silently skip it because the answer seems obvious. If it is not a ' +
90
- 'new task, skip registration — do not fabricate one.';
91
-
92
- function formatReminder(tasks) {
93
- const shown = tasks.slice(0, MAX_TASKS_SHOWN);
94
- const lines = shown.map(
95
- (task) => ` - [${task.status}] ${task.id}: ${task.title}`,
96
- );
97
- const extra = tasks.length - shown.length;
98
- if (extra > 0) lines.push(` - …and ${extra} more`);
99
- return (
100
- '[tasks] OPEN TASKS — recite these pending tasks to the user and confirm whether any ' +
101
- `should be dropped:\n${lines.join('\n')}`
102
- );
103
- }
104
-
105
- function main() {
106
- // Same defaults-dump protocol as the gates plugin's runGate (see hook-io.mjs): the CLI's
107
- // materialize.mjs spawns this script with the env var set to read its built-in params as
108
- // the single source of truth, before touching stdin.
109
- if (process.env[DUMP_ENV]) {
110
- process.stdout.write(
111
- JSON.stringify({
112
- id: 'remind-open-tasks',
113
- configKey: CONFIG_KEY,
114
- enabledByDefault: true,
115
- defaultParams: { remindEveryMessages: DEFAULT_REMIND_EVERY_MESSAGES },
116
- }),
117
- );
118
- return;
119
- }
120
-
121
- const payload = readPayload();
122
- const cwd = payload.cwd || process.cwd();
123
-
124
- const config = taskConfig(cwd);
125
- if (config && config.enabled === false) return; // gate turned off for this project
126
-
127
- const store = openTaskStore(cwd);
128
- if (!store) return; // no project: nowhere to track
129
-
130
- const lines = [CLASSIFY_PROMPT];
131
-
132
- const tasks = store.active();
133
- if (tasks.length > 0) {
134
- const remindEveryMessages =
135
- Number(config?.remindEveryMessages) || DEFAULT_REMIND_EVERY_MESSAGES;
136
- const next = store.counter() + 1;
137
- const forced = next >= remindEveryMessages;
138
- store.setCounter(forced ? 0 : next);
139
- if (forced) lines.push(formatReminder(tasks));
140
- }
141
-
142
- process.stdout.write(`${lines.join('\n')}\n`);
143
- }
144
-
145
- main();
1
+ // register-requests — UserPromptSubmit hook. Runs on every user message, BEFORE the
2
+ // assistant responds. It is the deterministic half of the task system: it never decides
3
+ // what is a task (that judgment is the assistant's — the hook only asks it to judge), it
4
+ // only makes sure the judgment is asked and open tasks are not forgotten. Two jobs:
5
+ // 1. On every message, ask the assistant to classify: if this message is a new task,
6
+ // register it via `claude-gates task add` (persistence is deterministic — the store —
7
+ // but the classification itself is the model's judgment, never a regex here).
8
+ // 2. Every `remindEveryMessages` messages (counted via the store's counter), also recite
9
+ // the currently active tasks so a long or compacted conversation never loses the
10
+ // thread of what is still pending.
11
+ //
12
+ // A UserPromptSubmit hook's stdout is added to the assistant's context as plain text, so
13
+ // both are simply written to stdout. Self-contained: Node built-ins + the task-store lib
14
+ // only, no npm at runtime.
15
+ //
16
+ // justification: no existing tool covers this. The task store and the audit were done when
17
+ // task-store.mjs was written (see its header); this file is the hook wiring on top.
18
+
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { dirname, join } from 'node:path';
22
+ import { openTaskStore } from './lib/task-store.mjs';
23
+
24
+ const STDIN_FILE_DESCRIPTOR = 0;
25
+ const CONFIG_KEY = 'remindOpenTasks';
26
+ const DEFAULT_REMIND_EVERY_MESSAGES = 5;
27
+ const MAX_TASKS_SHOWN = 12;
28
+ const DUMP_ENV = 'CLAUDE_GATES_DUMP_DEFAULTS';
29
+
30
+ // Config lookup mirrors the gates' config.mjs (project → global), kept local so this plugin
31
+ // stays independent of the gates plugin.
32
+ const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
33
+ const PROJECT_CONFIG = join('.ai', 'config.json');
34
+ const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
35
+
36
+ function readJson(path) {
37
+ try {
38
+ return JSON.parse(readFileSync(path, 'utf8'));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function projectRootOf(startDirectory) {
45
+ let current = startDirectory;
46
+ while (true) {
47
+ if (
48
+ PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
49
+ ) {
50
+ return current;
51
+ }
52
+ const parent = dirname(current);
53
+ if (parent === current) return null;
54
+ current = parent;
55
+ }
56
+ }
57
+
58
+ /** The tasks gate config entry (project overrides global), or null. */
59
+ function taskConfig(startDirectory) {
60
+ const root = projectRootOf(startDirectory);
61
+ const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
62
+ const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
63
+ const layer = projectData?.gates ?? globalData?.gates ?? {};
64
+ const entry = layer[CONFIG_KEY];
65
+ if (typeof entry === 'boolean') return { enabled: entry };
66
+ if (entry && typeof entry === 'object') return entry;
67
+ return null;
68
+ }
69
+
70
+ function readPayload() {
71
+ try {
72
+ return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
73
+ } catch {
74
+ return {};
75
+ }
76
+ }
77
+
78
+ // Asked on every message. The hook does not classify — it asks the model to, and the
79
+ // model is the one that persists (via the CLI, which enforces the store's own rules,
80
+ // e.g. evidence on close). This is legitimate: the hook is not depending on the model to
81
+ // make something OCCUR deterministically; it is depending on the model's judgment for a
82
+ // judgment call (whether this message describes a new task), same shape as ask-adoption.
83
+ const CLASSIFY_PROMPT =
84
+ '[tasks] MANDATORY before you respond: judge whether the message you just received describes ' +
85
+ 'a new actionable task (not a question, not small talk, not a follow-up on an already-registered ' +
86
+ 'task). If it does, you MUST register it now, before writing your reply — run ' +
87
+ '`claude-gates task add "<title>" [--description <text>] [--size <size>]` from the project root ' +
88
+ "(or the CLI's absolute path if `claude-gates` is not on PATH). Do not defer this, do not decide " +
89
+ 'to register it "later", do not silently skip it because the answer seems obvious. If it is not a ' +
90
+ 'new task, skip registration — do not fabricate one.';
91
+
92
+ function formatReminder(tasks) {
93
+ const shown = tasks.slice(0, MAX_TASKS_SHOWN);
94
+ const lines = shown.map(
95
+ (task) => ` - [${task.status}] ${task.id}: ${task.title}`,
96
+ );
97
+ const extra = tasks.length - shown.length;
98
+ if (extra > 0) lines.push(` - …and ${extra} more`);
99
+ return (
100
+ '[tasks] OPEN TASKS — recite these pending tasks to the user and confirm whether any ' +
101
+ `should be dropped:\n${lines.join('\n')}`
102
+ );
103
+ }
104
+
105
+ function main() {
106
+ // Same defaults-dump protocol as the gates plugin's runGate (see hook-io.mjs): the CLI's
107
+ // materialize.mjs spawns this script with the env var set to read its built-in params as
108
+ // the single source of truth, before touching stdin.
109
+ if (process.env[DUMP_ENV]) {
110
+ process.stdout.write(
111
+ JSON.stringify({
112
+ id: 'remind-open-tasks',
113
+ configKey: CONFIG_KEY,
114
+ enabledByDefault: true,
115
+ defaultParams: { remindEveryMessages: DEFAULT_REMIND_EVERY_MESSAGES },
116
+ }),
117
+ );
118
+ return;
119
+ }
120
+
121
+ const payload = readPayload();
122
+ const cwd = payload.cwd || process.cwd();
123
+
124
+ const config = taskConfig(cwd);
125
+ if (config && config.enabled === false) return; // gate turned off for this project
126
+
127
+ const store = openTaskStore(cwd);
128
+ if (!store) return; // no project: nowhere to track
129
+
130
+ const lines = [CLASSIFY_PROMPT];
131
+
132
+ const tasks = store.active();
133
+ if (tasks.length > 0) {
134
+ const remindEveryMessages =
135
+ Number(config?.remindEveryMessages) || DEFAULT_REMIND_EVERY_MESSAGES;
136
+ const next = store.counter() + 1;
137
+ const forced = next >= remindEveryMessages;
138
+ store.setCounter(forced ? 0 : next);
139
+ if (forced) lines.push(formatReminder(tasks));
140
+ }
141
+
142
+ process.stdout.write(`${lines.join('\n')}\n`);
143
+ }
144
+
145
+ main();
@@ -1,108 +1,108 @@
1
- // session-tasks — SessionStart hook. Runs once when a session opens. Lists this project's
2
- // ACTIVE tasks (open/blocked/in_forge) so a fresh session (or one that just compacted) does
3
- // not lose track of what was already registered. Silent when there is nothing active: this
4
- // hook only speaks when it has something worth saying.
5
- //
6
- // Self-contained: Node built-ins + the task-store lib only, no npm at runtime.
7
- //
8
- // justification: no existing tool covers this. task-store.mjs (and its audit) already
9
- // exists; this is the SessionStart wiring on top of it, mirroring register-requests.mjs's
10
- // UserPromptSubmit wiring for the same store.
11
-
12
- import { existsSync, readFileSync } from 'node:fs';
13
- import { homedir } from 'node:os';
14
- import { dirname, join } from 'node:path';
15
- import { openTaskStore, STATUS } from './lib/task-store.mjs';
16
-
17
- const STDIN_FILE_DESCRIPTOR = 0;
18
- const SESSION_START_EVENT = 'SessionStart';
19
- const CONFIG_KEY = 'listTasksOnSessionStart';
20
- const MAX_TASKS_SHOWN = 20;
21
-
22
- // Same config lookup as register-requests.mjs: project overrides global. Kept local so this
23
- // plugin stays independent of the gates plugin.
24
- const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
25
- const PROJECT_CONFIG = join('.ai', 'config.json');
26
- const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
27
-
28
- function readJson(path) {
29
- try {
30
- return JSON.parse(readFileSync(path, 'utf8'));
31
- } catch {
32
- return null;
33
- }
34
- }
35
-
36
- function projectRootOf(startDirectory) {
37
- let current = startDirectory;
38
- while (true) {
39
- if (
40
- PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
41
- ) {
42
- return current;
43
- }
44
- const parent = dirname(current);
45
- if (parent === current) return null;
46
- current = parent;
47
- }
48
- }
49
-
50
- /** The tasks gate config entry (project overrides global), or null. */
51
- function sessionTasksConfig(startDirectory) {
52
- const root = projectRootOf(startDirectory);
53
- const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
54
- const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
55
- const layer = projectData?.gates ?? globalData?.gates ?? {};
56
- const entry = layer[CONFIG_KEY];
57
- if (typeof entry === 'boolean') return { enabled: entry };
58
- if (entry && typeof entry === 'object') return entry;
59
- return null;
60
- }
61
-
62
- function readPayload() {
63
- try {
64
- return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
65
- } catch {
66
- return {};
67
- }
68
- }
69
-
70
- function formatActiveTasks(tasks) {
71
- const shown = tasks.slice(0, MAX_TASKS_SHOWN);
72
- const lines = shown.map((task) => {
73
- const marker = task.status === STATUS.IN_FORGE ? ' (in_forge)' : '';
74
- return ` - ${task.id}: ${task.title} [${task.status}]${marker}`;
75
- });
76
- const extra = tasks.length - shown.length;
77
- if (extra > 0) lines.push(` - …and ${extra} more`);
78
- return `[tasks] Active tasks for this project:\n${lines.join('\n')}`;
79
- }
80
-
81
- function speak(context) {
82
- process.stdout.write(
83
- JSON.stringify({
84
- hookSpecificOutput: {
85
- hookEventName: SESSION_START_EVENT,
86
- additionalContext: context,
87
- },
88
- }),
89
- );
90
- }
91
-
92
- function main() {
93
- const payload = readPayload();
94
- const cwd = payload.cwd || process.cwd();
95
-
96
- const config = sessionTasksConfig(cwd);
97
- if (config && config.enabled === false) return; // gate turned off for this project
98
-
99
- const store = openTaskStore(cwd);
100
- if (!store) return; // no project: nowhere to track
101
-
102
- const tasks = store.active();
103
- if (tasks.length === 0) return; // nothing active: stay silent
104
-
105
- speak(formatActiveTasks(tasks));
106
- }
107
-
108
- main();
1
+ // session-tasks — SessionStart hook. Runs once when a session opens. Lists this project's
2
+ // ACTIVE tasks (open/blocked/in_forge) so a fresh session (or one that just compacted) does
3
+ // not lose track of what was already registered. Silent when there is nothing active: this
4
+ // hook only speaks when it has something worth saying.
5
+ //
6
+ // Self-contained: Node built-ins + the task-store lib only, no npm at runtime.
7
+ //
8
+ // justification: no existing tool covers this. task-store.mjs (and its audit) already
9
+ // exists; this is the SessionStart wiring on top of it, mirroring register-requests.mjs's
10
+ // UserPromptSubmit wiring for the same store.
11
+
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { dirname, join } from 'node:path';
15
+ import { openTaskStore, STATUS } from './lib/task-store.mjs';
16
+
17
+ const STDIN_FILE_DESCRIPTOR = 0;
18
+ const SESSION_START_EVENT = 'SessionStart';
19
+ const CONFIG_KEY = 'listTasksOnSessionStart';
20
+ const MAX_TASKS_SHOWN = 20;
21
+
22
+ // Same config lookup as register-requests.mjs: project overrides global. Kept local so this
23
+ // plugin stays independent of the gates plugin.
24
+ const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
25
+ const PROJECT_CONFIG = join('.ai', 'config.json');
26
+ const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
27
+
28
+ function readJson(path) {
29
+ try {
30
+ return JSON.parse(readFileSync(path, 'utf8'));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function projectRootOf(startDirectory) {
37
+ let current = startDirectory;
38
+ while (true) {
39
+ if (
40
+ PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
41
+ ) {
42
+ return current;
43
+ }
44
+ const parent = dirname(current);
45
+ if (parent === current) return null;
46
+ current = parent;
47
+ }
48
+ }
49
+
50
+ /** The tasks gate config entry (project overrides global), or null. */
51
+ function sessionTasksConfig(startDirectory) {
52
+ const root = projectRootOf(startDirectory);
53
+ const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
54
+ const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
55
+ const layer = projectData?.gates ?? globalData?.gates ?? {};
56
+ const entry = layer[CONFIG_KEY];
57
+ if (typeof entry === 'boolean') return { enabled: entry };
58
+ if (entry && typeof entry === 'object') return entry;
59
+ return null;
60
+ }
61
+
62
+ function readPayload() {
63
+ try {
64
+ return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
70
+ function formatActiveTasks(tasks) {
71
+ const shown = tasks.slice(0, MAX_TASKS_SHOWN);
72
+ const lines = shown.map((task) => {
73
+ const marker = task.status === STATUS.IN_FORGE ? ' (in_forge)' : '';
74
+ return ` - ${task.id}: ${task.title} [${task.status}]${marker}`;
75
+ });
76
+ const extra = tasks.length - shown.length;
77
+ if (extra > 0) lines.push(` - …and ${extra} more`);
78
+ return `[tasks] Active tasks for this project:\n${lines.join('\n')}`;
79
+ }
80
+
81
+ function speak(context) {
82
+ process.stdout.write(
83
+ JSON.stringify({
84
+ hookSpecificOutput: {
85
+ hookEventName: SESSION_START_EVENT,
86
+ additionalContext: context,
87
+ },
88
+ }),
89
+ );
90
+ }
91
+
92
+ function main() {
93
+ const payload = readPayload();
94
+ const cwd = payload.cwd || process.cwd();
95
+
96
+ const config = sessionTasksConfig(cwd);
97
+ if (config && config.enabled === false) return; // gate turned off for this project
98
+
99
+ const store = openTaskStore(cwd);
100
+ if (!store) return; // no project: nowhere to track
101
+
102
+ const tasks = store.active();
103
+ if (tasks.length === 0) return; // nothing active: stay silent
104
+
105
+ speak(formatActiveTasks(tasks));
106
+ }
107
+
108
+ main();