@devrik-tools/claude-gates 0.7.1 → 0.7.2
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/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +10 -2
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +15 -10
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +13 -3
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +4 -1
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +6 -1
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +46 -10
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +12 -6
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +30 -9
- package/plugins/gates/hooks/lib/hook-io.mjs +9 -1
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"name": "gates",
|
|
12
12
|
"source": "./plugins/gates",
|
|
13
13
|
"description": "PreToolUse gates: destructive-command blocks, protected paths, delegation/spec/quality rules, tool-discovery and forge-pipeline enforcement. Which gates run is decided by .ai/config.json (project) or ~/.claude/claude-gates/config.json (global).",
|
|
14
|
-
"version": "0.7.
|
|
14
|
+
"version": "0.7.2"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"name": "tasks",
|
|
18
18
|
"source": "./plugins/tasks",
|
|
19
19
|
"description": "Deterministic task tracking: the model registers tasks via the CLI, a UserPromptSubmit hook reminds of open tasks every few messages, and a SessionStart hook lists active tasks when a session opens.",
|
|
20
|
-
"version": "0.7.
|
|
20
|
+
"version": "0.7.2"
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devrik-tools/claude-gates",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Installable, deterministic gates (hooks) for Claude Code: block destructive commands, protected paths, and enforce delegation/spec/quality rules. Configurable per project.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gates",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Deterministic gates for Claude Code: destructive-command blocks, protected paths, delegation briefs, spec-driven flow and session-start validation. Selection lives in config, not in code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Devrik"
|
|
@@ -54,7 +54,11 @@ function checkDelegation(toolInput) {
|
|
|
54
54
|
|
|
55
55
|
deny(
|
|
56
56
|
CONFIG_KEY,
|
|
57
|
-
'
|
|
57
|
+
'Blocked: this delegation prompt asks to create a new script/checker/gate/hook/linter/' +
|
|
58
|
+
'tool, and nothing in the prompt shows a prior audit. No filesystem exploration is ' +
|
|
59
|
+
'needed to fix this — add ONE phrase to the prompt matching ' +
|
|
60
|
+
`/${AUDIT_EVIDENCE_PATTERN.source}/i, e.g.: "audited and no existing tool covers this" ` +
|
|
61
|
+
'(this exact phrase is guaranteed to match). Then relaunch the same delegation.',
|
|
58
62
|
);
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -85,7 +89,11 @@ function checkWrite(toolInput, parameters) {
|
|
|
85
89
|
|
|
86
90
|
deny(
|
|
87
91
|
CONFIG_KEY,
|
|
88
|
-
`
|
|
92
|
+
`Blocked: '${filePath}' is a new executable tool with no justification comment in its ` +
|
|
93
|
+
'content. No filesystem exploration is needed to fix this — add ONE line to the ' +
|
|
94
|
+
`file's content matching /${INLINE_JUSTIFICATION_PATTERN.source}/i, e.g.: ` +
|
|
95
|
+
'"// justification: no existing tool covers this" (this exact phrase is guaranteed ' +
|
|
96
|
+
'to match). Then retry the exact same write.',
|
|
89
97
|
);
|
|
90
98
|
}
|
|
91
99
|
|
|
@@ -194,21 +194,26 @@ runGate(
|
|
|
194
194
|
const treeRoot = contractTreeRootFor(catalogPath);
|
|
195
195
|
if (!treeRoot) return; // no SDD harness adopted: stay silent
|
|
196
196
|
|
|
197
|
-
const unapproved = citedFeatures
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
const unapproved = citedFeatures
|
|
198
|
+
.map((feature) => ({
|
|
199
|
+
feature,
|
|
200
|
+
contractPath: contractFileFor(treeRoot, feature),
|
|
201
|
+
}))
|
|
202
|
+
.filter(({ contractPath }) => contractPath) // no contractPath: sdd-specs already denies that case
|
|
203
|
+
.filter(({ contractPath }) => !isApproved(contractPath));
|
|
202
204
|
|
|
203
205
|
if (unapproved.length === 0) return;
|
|
204
206
|
|
|
207
|
+
const fileList = unapproved
|
|
208
|
+
.map(({ feature, contractPath }) => `${feature} -> ${contractPath}`)
|
|
209
|
+
.join('\n ');
|
|
205
210
|
deny(
|
|
206
211
|
CONFIG_KEY,
|
|
207
|
-
`This implementation delegation cites feature(s)
|
|
208
|
-
'
|
|
209
|
-
'
|
|
210
|
-
'
|
|
211
|
-
'the
|
|
212
|
+
`This implementation delegation cites unapproved feature(s):\n ${fileList}\n` +
|
|
213
|
+
'Paste the FULL brief into the chat, get an explicit confirmation from the user ' +
|
|
214
|
+
'(not a vague "dale"/"sigamos" — an actual sentence confirming they read it), ' +
|
|
215
|
+
'then add this frontmatter to the TOP of that exact contract file before ' +
|
|
216
|
+
'relaunching (no filesystem exploration needed — the path above is the file to edit):\n' +
|
|
212
217
|
'---\nstatus: approved\napproved_at: <ISO timestamp>\n' +
|
|
213
218
|
'approval_quote: "<the user\'s own confirming words>"\n---',
|
|
214
219
|
);
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
warn,
|
|
4
4
|
toolInGroups,
|
|
5
5
|
writtenContentOf,
|
|
6
|
+
writtenPathOf,
|
|
6
7
|
} from '../../lib/hook-io.mjs';
|
|
7
8
|
|
|
8
9
|
const GATE_ID = 'diagnosis-before-patch';
|
|
@@ -37,12 +38,21 @@ runGate(
|
|
|
37
38
|
const patterns = parameters.timeoutPatterns.map(
|
|
38
39
|
(source) => new RegExp(source, 'i'),
|
|
39
40
|
);
|
|
40
|
-
const
|
|
41
|
-
|
|
41
|
+
const matched = patterns
|
|
42
|
+
.map((pattern) => pattern.exec(text))
|
|
43
|
+
.find((match) => match !== null);
|
|
44
|
+
if (!matched) return;
|
|
42
45
|
|
|
46
|
+
const filePath = writtenPathOf(toolInput) || '(unknown path)';
|
|
43
47
|
warn(
|
|
44
48
|
CONFIG_KEY,
|
|
45
|
-
|
|
49
|
+
`You are writing '${matched[0].trim()}' into ${filePath} — a timeout/deadline/retry ` +
|
|
50
|
+
'value. Before changing a value to fix a symptom ("X is slow/fails"), confirm you ' +
|
|
51
|
+
'have the evidence that proves the cause: a log line from the failing provider/' +
|
|
52
|
+
"process, not a hypothesis. If you don't have that log line yet, get it before " +
|
|
53
|
+
'writing this change — do not guess a new number. A timeout should measure ' +
|
|
54
|
+
'inactivity, not total time: a process that is still progressing should not be cut ' +
|
|
55
|
+
'off. This is a warning, not a block — the write proceeds either way.',
|
|
46
56
|
);
|
|
47
57
|
},
|
|
48
58
|
);
|
|
@@ -147,7 +147,10 @@ function checkLiveTask(pointerPath) {
|
|
|
147
147
|
CONFIG_KEY,
|
|
148
148
|
`The active task '${slug}' has no contract on disk: none of ` +
|
|
149
149
|
`${TASK_CONTRACT_FILES.join(', ')} exists under ${taskDirectory}. ` +
|
|
150
|
-
|
|
150
|
+
`Write ONE of those files there before implementing (this gate's contract root is ` +
|
|
151
|
+
`.ai/pipeline/${slug}/ — a brief.md under .ai/features/${slug}/ satisfies a ` +
|
|
152
|
+
'different gate, sdd-specs, but NOT this one; if you already wrote a brief there, ' +
|
|
153
|
+
`create a short asserts.md under ${taskDirectory} referencing it).`,
|
|
151
154
|
);
|
|
152
155
|
}
|
|
153
156
|
}
|
|
@@ -76,6 +76,7 @@ runGate(
|
|
|
76
76
|
({ toolName, parameters }) => {
|
|
77
77
|
if (!toolInGroups(toolName, ['execution', 'delegation'])) return;
|
|
78
78
|
|
|
79
|
+
const recurrencesFile = join('.ai', 'reincidencias.json');
|
|
79
80
|
const openRecurrences = loadOpenRecurrences(
|
|
80
81
|
process.cwd(),
|
|
81
82
|
parameters.thresholdAppearances,
|
|
@@ -85,7 +86,11 @@ runGate(
|
|
|
85
86
|
const names = openRecurrences.map((entry) => entry.class).join(', ');
|
|
86
87
|
deny(
|
|
87
88
|
CONFIG_KEY,
|
|
88
|
-
`Registered recurring issue
|
|
89
|
+
`Registered recurring issue class(es) still open and at/above the ` +
|
|
90
|
+
`${parameters.thresholdAppearances}-occurrence threshold: ${names}. Fix the root ` +
|
|
91
|
+
`cause of the class (not this one instance), then in ${recurrencesFile} set that ` +
|
|
92
|
+
'class\'s "status" to "closed" (or "cerrada") before proceeding — no other file to ' +
|
|
93
|
+
'find, this is the only source this gate reads.',
|
|
89
94
|
);
|
|
90
95
|
},
|
|
91
96
|
);
|
|
@@ -196,14 +196,50 @@ function isToolLikePath(filePath, parameters) {
|
|
|
196
196
|
);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
199
|
+
// The exact phrase AUDIT_DONE_PATTERN matches — quoted verbatim in the deny message so the
|
|
200
|
+
// model can copy it instead of guessing a paraphrase the regex might not recognize (a vague
|
|
201
|
+
// "I checked" or "no lo encontré" does NOT match AUDIT_DONE_PATTERN and the gate fires again).
|
|
202
|
+
const AUDIT_DONE_EXAMPLE_PHRASE = 'no existing tool covers this';
|
|
203
|
+
|
|
204
|
+
/** The concrete deny message for a blocked WRITE: names the exact file and gives the exact
|
|
205
|
+
* text to add (copy-pasteable, matches AUDIT_DONE_PATTERN verbatim) — no exploration needed
|
|
206
|
+
* to find any of this, it is the gate's own job to hand it over. Adding the phrase also
|
|
207
|
+
* satisfies the tool-map gate (maintainToolMap), which records the file + this same line
|
|
208
|
+
* into .ai/tool-map.json automatically — no separate command to run. */
|
|
209
|
+
function writeDenyMessage(filePath) {
|
|
210
|
+
return (
|
|
211
|
+
`Blocked: '${filePath}' looks like a new tool/helper, and nothing shows the wheel was ` +
|
|
212
|
+
'checked first. Pick ONE, then retry the exact same write:\n' +
|
|
213
|
+
` 1. It already exists here or in an installed dep — don't create '${filePath}'; ` +
|
|
214
|
+
'reuse/import the existing one instead.\n' +
|
|
215
|
+
' 2. It does not exist anywhere you checked (repo, installed deps, npm/marketplaces) — ' +
|
|
216
|
+
"add ONE line to the file's content stating that, e.g.: " +
|
|
217
|
+
`"// ${AUDIT_DONE_EXAMPLE_PHRASE}" (any phrase matching /${AUDIT_DONE_PATTERN.source}/i ` +
|
|
218
|
+
'works, this one is guaranteed to). That line also gets this file auto-recorded into ' +
|
|
219
|
+
'.ai/tool-map.json — nothing further to run.\n' +
|
|
220
|
+
'No filesystem exploration is required to satisfy this gate — the audit is a sentence ' +
|
|
221
|
+
'in the file or a matching package.json dependency, nothing else.'
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The concrete deny message for a blocked DELEGATION prompt: same two options, but no file
|
|
226
|
+
* name exists yet (only a prompt), so both talk about the prompt text, not a file. */
|
|
227
|
+
function delegationDenyMessage() {
|
|
228
|
+
return (
|
|
229
|
+
"Blocked: this delegation's prompt asks to build a new tool/helper, and nothing in " +
|
|
230
|
+
'the prompt shows the wheel was checked first. Pick ONE, then relaunch the same ' +
|
|
231
|
+
'delegation with the prompt updated:\n' +
|
|
232
|
+
' 1. It already exists (repo, installed dep) — do not delegate a new build; ' +
|
|
233
|
+
'reference the existing one in the prompt instead.\n' +
|
|
234
|
+
' 2. It does not exist anywhere you checked — add ONE line to the prompt stating ' +
|
|
235
|
+
`that, e.g.: "${AUDIT_DONE_EXAMPLE_PHRASE}" (any phrase matching ` +
|
|
236
|
+
`/${AUDIT_DONE_PATTERN.source}/i works, this one is guaranteed to). Once the delegate ` +
|
|
237
|
+
'actually writes the file, include the same phrase in its content so it also gets ' +
|
|
238
|
+
'auto-recorded into .ai/tool-map.json.\n' +
|
|
239
|
+
'No filesystem exploration is required to satisfy this gate — the audit is a sentence ' +
|
|
240
|
+
'in the prompt, or a matching package.json dependency, nothing else.'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
207
243
|
|
|
208
244
|
/** A build is cleared when its text declares an audit, its name is already recorded in the
|
|
209
245
|
* map, or an installed dependency covers it. `toolBaseName` is used for the last two. */
|
|
@@ -224,7 +260,7 @@ function checkDelegation(toolInput, cwd, parameters) {
|
|
|
224
260
|
const prompt = delegationPromptOf(toolInput);
|
|
225
261
|
if (!isBuildIntent(prompt)) return;
|
|
226
262
|
if (buildIsCleared(prompt, cwd, parameters, null)) return;
|
|
227
|
-
deny(CONFIG_KEY,
|
|
263
|
+
deny(CONFIG_KEY, delegationDenyMessage());
|
|
228
264
|
}
|
|
229
265
|
|
|
230
266
|
/** Write: block a new tool-like file that is not cleared by content, map, or an installed dep. */
|
|
@@ -237,7 +273,7 @@ function checkWrite(toolInput, cwd, parameters) {
|
|
|
237
273
|
const content = writtenContentOf(toolInput);
|
|
238
274
|
const toolBaseName = basename(filePath).replace(/\.[^.]+$/, '');
|
|
239
275
|
if (buildIsCleared(content, cwd, parameters, toolBaseName)) return;
|
|
240
|
-
deny(CONFIG_KEY,
|
|
276
|
+
deny(CONFIG_KEY, writeDenyMessage(filePath));
|
|
241
277
|
}
|
|
242
278
|
|
|
243
279
|
runGate(
|
|
@@ -150,14 +150,14 @@ runGate(
|
|
|
150
150
|
for (const script of scripts) {
|
|
151
151
|
const before = snapshotConfigs(watchedConfigPaths);
|
|
152
152
|
|
|
153
|
-
let
|
|
153
|
+
let failure = null;
|
|
154
154
|
try {
|
|
155
155
|
execFileSync(process.execPath, [script], {
|
|
156
|
-
stdio: 'ignore',
|
|
156
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
157
157
|
timeout: 5000,
|
|
158
158
|
});
|
|
159
|
-
} catch {
|
|
160
|
-
|
|
159
|
+
} catch (error) {
|
|
160
|
+
failure = error;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
if (configsTampered(before)) {
|
|
@@ -169,10 +169,16 @@ runGate(
|
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
if (
|
|
172
|
+
if (failure) {
|
|
173
|
+
const stderr = String(failure.stderr ?? '').trim();
|
|
174
|
+
const detail = stderr || failure.message || String(failure);
|
|
173
175
|
deny(
|
|
174
176
|
CONFIG_KEY,
|
|
175
|
-
`Sub-gate
|
|
177
|
+
`Sub-gate '${relative(projectRoot, script)}' failed (exit ` +
|
|
178
|
+
`${failure.status ?? 'unknown'}): ${detail}\n` +
|
|
179
|
+
'No filesystem exploration is needed — fix that script (open it at the path ' +
|
|
180
|
+
'above and address the error shown), or remove it from ' +
|
|
181
|
+
`${parameters.rulesDir}/ if it should not run as a gate.`,
|
|
176
182
|
);
|
|
177
183
|
return;
|
|
178
184
|
}
|
|
@@ -147,15 +147,31 @@ function parseJsonOrNull(text) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/** Denies the single first advanced-status feature in the new catalog content that has
|
|
150
|
-
* no contract on disk, or does nothing when every advanced feature has one.
|
|
150
|
+
* no contract on disk, or does nothing when every advanced feature has one. A feature
|
|
151
|
+
* object missing its `name` field is denied too (not silently skipped) — it is malformed
|
|
152
|
+
* catalog data, not a feature this gate has decided has no obligations. */
|
|
151
153
|
function denyIfAdvancedFeatureLacksContract(features, treeRoot) {
|
|
152
154
|
for (const feature of features) {
|
|
153
|
-
if (!
|
|
155
|
+
if (!ADVANCED_STATUSES.has(feature?.status)) continue;
|
|
156
|
+
if (!feature?.name) {
|
|
157
|
+
deny(
|
|
158
|
+
CONFIG_KEY,
|
|
159
|
+
`A feature entry has status '${feature.status}' but no 'name' field: ` +
|
|
160
|
+
`${JSON.stringify(feature)}. Every feature needs a 'name' so its contract tree ` +
|
|
161
|
+
'(.ai/features/<name>/) can be located — add it before writing this status.',
|
|
162
|
+
);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
154
165
|
if (contractExistsFor(treeRoot, feature.name)) continue;
|
|
166
|
+
const featureDirectory = join(
|
|
167
|
+
treeRoot ?? join(process.cwd(), '.ai', 'features'),
|
|
168
|
+
feature.name,
|
|
169
|
+
);
|
|
155
170
|
deny(
|
|
156
171
|
CONFIG_KEY,
|
|
157
172
|
`Feature '${feature.name}' is set to '${feature.status}' but has no non-empty ` +
|
|
158
|
-
`contract
|
|
173
|
+
`contract on disk. Write ONE of these files under ${featureDirectory}/ (any one ` +
|
|
174
|
+
`is enough): ${CONTRACT_FILES.join(', ')}.`,
|
|
159
175
|
);
|
|
160
176
|
}
|
|
161
177
|
}
|
|
@@ -172,9 +188,10 @@ function checkCatalogWrite(toolInput, catalogPath, treeRoot) {
|
|
|
172
188
|
// hidden behind a malformed payload must not be silently allowed through.
|
|
173
189
|
deny(
|
|
174
190
|
CONFIG_KEY,
|
|
175
|
-
`The write to ${CATALOG_FILE_NAME} does not parse as JSON.
|
|
176
|
-
'
|
|
177
|
-
'or
|
|
191
|
+
`The write to ${CATALOG_FILE_NAME} does not parse as JSON (JSON.parse threw). This ` +
|
|
192
|
+
'is a syntax problem in the content being written, not a missing-field one — check ' +
|
|
193
|
+
'for a trailing comma, unquoted key, or unclosed bracket in what you are about to ' +
|
|
194
|
+
'write. No filesystem exploration needed; the payload itself is the thing to fix.',
|
|
178
195
|
);
|
|
179
196
|
}
|
|
180
197
|
const features = Array.isArray(parsed?.features) ? parsed.features : [];
|
|
@@ -212,11 +229,15 @@ function checkDelegation(toolInput, treeRoot, exemptSubagents) {
|
|
|
212
229
|
(feature) => !contractExistsFor(treeRoot, feature),
|
|
213
230
|
);
|
|
214
231
|
if (missing.length === citedFeatures.length) {
|
|
232
|
+
const root = treeRoot ?? join(process.cwd(), '.ai', 'features');
|
|
233
|
+
const fileList = missing
|
|
234
|
+
.map((feature) => `${feature} -> ${join(root, feature)}/`)
|
|
235
|
+
.join('\n ');
|
|
215
236
|
deny(
|
|
216
237
|
CONFIG_KEY,
|
|
217
|
-
`This implementation delegation cites feature(s)
|
|
218
|
-
`
|
|
219
|
-
'
|
|
238
|
+
`This implementation delegation cites feature(s) with no contract on disk:\n ${fileList}\n` +
|
|
239
|
+
`Write ONE of these files in each directory above (any one is enough): ` +
|
|
240
|
+
`${CONTRACT_FILES.join(', ')}. Then relaunch.`,
|
|
220
241
|
);
|
|
221
242
|
}
|
|
222
243
|
}
|
|
@@ -178,7 +178,15 @@ const SHELL_WRITE_PATTERNS = [
|
|
|
178
178
|
/\b(?:touch|tee)\s+(?:-\S+\s+)*(['"]?)([^\s'"|;&<>]+)\1/g,
|
|
179
179
|
// cp / mv / install destination is the LAST path; capture the first arg after the command
|
|
180
180
|
// as a cheap proxy (over-detects the source too, which is acceptable — a gate re-checks).
|
|
181
|
-
|
|
181
|
+
// Anchored to the START of a command (start of string, or right after a separator like
|
|
182
|
+
// `;`/`&&`/`||`/`|`), NOT `\b`, which matched "install" as a bare word anywhere — including
|
|
183
|
+
// as npm/pip/yarn's SUBCOMMAND (`npm install -D daisyui` was misread as the Unix `install`
|
|
184
|
+
// utility, capturing the package name "daisyui" as a phantom root-level file target and
|
|
185
|
+
// tripping root-whitelist on a plain dependency install). `npm install`/`pip install`/
|
|
186
|
+
// `yarn install` never start a shell command with the bare word "install" as argv[0], so
|
|
187
|
+
// anchoring at the command boundary excludes them while still catching a real `install ...`
|
|
188
|
+
// invocation (the coreutils/BSD command) at the start of a command.
|
|
189
|
+
/(?:^|[;&|])\s*(?:cp|mv|install)\s+(?:-\S+\s+)*(['"]?)([^\s'"|;&<>]+)\1/g,
|
|
182
190
|
];
|
|
183
191
|
|
|
184
192
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tasks",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Deterministic task tracking for Claude Code: persists tasks the model registers via the CLI, reminds of open tasks on a message counter, and lists active tasks on session start.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Devrik"
|