@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.es.md +39 -4
- package/README.md +34 -5
- package/cli/config.mjs +126 -124
- package/cli/init.mjs +303 -276
- package/cli/install.mjs +281 -175
- package/cli/materialize.mjs +103 -102
- package/cli/registry.mjs +139 -136
- package/cli/smoke-fixtures.json +65 -0
- package/cli/task.mjs +140 -140
- package/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/ask-adoption.mjs +147 -147
- package/plugins/gates/hooks/doctor.mjs +207 -207
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +216 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
- package/plugins/gates/hooks/gates/capability-map/index.mjs +701 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
- package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
- package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
- package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
- package/plugins/gates/hooks/hooks.json +61 -0
- package/plugins/gates/hooks/lib/config.mjs +179 -172
- package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
- package/plugins/gates/hooks/lib/signals.mjs +172 -127
- package/plugins/gates/hooks/wiring-check.mjs +227 -227
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/hooks.json +26 -26
- package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
- package/plugins/tasks/hooks/register-requests.mjs +145 -145
- package/plugins/tasks/hooks/session-tasks.mjs +108 -108
- 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
|
-
|
|
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();
|