@devrik-tools/claude-gates 0.3.1 → 0.6.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 (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +25 -4
  3. package/README.md +75 -43
  4. package/cli/config.mjs +126 -124
  5. package/cli/index.mjs +154 -104
  6. package/cli/init.mjs +303 -276
  7. package/cli/install.mjs +281 -175
  8. package/cli/materialize.mjs +103 -102
  9. package/cli/registry.mjs +139 -136
  10. package/cli/smoke-fixtures.json +441 -0
  11. package/cli/smoke.mjs +129 -0
  12. package/cli/task.mjs +140 -140
  13. package/package.json +1 -1
  14. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  15. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  16. package/plugins/gates/hooks/doctor.mjs +207 -207
  17. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  18. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  19. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  20. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  21. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  22. package/plugins/gates/hooks/gates/capability-map/index.mjs +377 -0
  23. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  24. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  25. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  26. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  27. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  28. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  29. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  30. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  31. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  32. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  33. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  34. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  35. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  36. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  37. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  38. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  39. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  40. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  41. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  42. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  43. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  44. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  45. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  46. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  47. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  48. package/plugins/gates/hooks/hooks.json +51 -0
  49. package/plugins/gates/hooks/lib/config.mjs +179 -172
  50. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  51. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  52. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  53. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  54. package/plugins/tasks/hooks/hooks.json +26 -26
  55. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  56. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  57. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  58. package/registry.json +171 -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();