@devrik-tools/claude-gates 0.1.2 → 0.2.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 (106) hide show
  1. package/.claude-plugin/marketplace.json +7 -1
  2. package/cli/__tests__/config.test.mjs +113 -101
  3. package/cli/__tests__/install.test.mjs +102 -0
  4. package/cli/__tests__/materialize.test.mjs +95 -0
  5. package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
  6. package/cli/__tests__/task.test.mjs +124 -0
  7. package/cli/__tests__/version-consistency.test.mjs +44 -0
  8. package/cli/config.mjs +124 -113
  9. package/cli/index.mjs +100 -97
  10. package/cli/init.mjs +259 -256
  11. package/cli/install.mjs +150 -80
  12. package/cli/materialize.mjs +102 -51
  13. package/cli/registry.mjs +136 -127
  14. package/cli/task.mjs +140 -0
  15. package/package.json +2 -1
  16. package/plugins/gates/.claude-plugin/plugin.json +8 -2
  17. package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
  18. package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
  19. package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
  20. package/plugins/gates/hooks/ask-adoption.mjs +147 -0
  21. package/plugins/gates/hooks/doctor.mjs +207 -0
  22. package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
  23. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
  24. package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
  25. package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
  26. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
  27. package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
  28. package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
  29. package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
  30. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
  31. package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
  32. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
  33. package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
  34. package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
  35. package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
  36. package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
  37. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
  38. package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
  39. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
  40. package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
  41. package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
  42. package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
  43. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
  44. package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
  45. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
  46. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
  47. package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
  48. package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
  49. package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
  50. package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
  51. package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
  52. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
  53. package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
  54. package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
  55. package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
  56. package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
  57. package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
  58. package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
  59. package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
  60. package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
  61. package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
  62. package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
  63. package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
  64. package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
  65. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
  66. package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
  67. package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
  68. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
  69. package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
  70. package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
  71. package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
  72. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
  73. package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
  74. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
  75. package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
  76. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
  77. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
  78. package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
  79. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
  80. package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
  81. package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
  82. package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
  83. package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
  84. package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
  85. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
  86. package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
  87. package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
  88. package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
  89. package/plugins/gates/hooks/hooks.json +346 -286
  90. package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
  91. package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
  92. package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
  93. package/plugins/gates/hooks/lib/config.mjs +172 -165
  94. package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
  95. package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
  96. package/plugins/gates/hooks/lib/signals.mjs +127 -0
  97. package/plugins/gates/hooks/wiring-check.mjs +227 -0
  98. package/plugins/tasks/.claude-plugin/plugin.json +14 -0
  99. package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
  100. package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
  101. package/plugins/tasks/hooks/hooks.json +26 -0
  102. package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
  103. package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
  104. package/plugins/tasks/hooks/register-requests.mjs +141 -108
  105. package/plugins/tasks/hooks/session-tasks.mjs +108 -0
  106. package/registry.json +793 -686
@@ -1,132 +1,180 @@
1
- import assert from 'node:assert/strict';
2
- import {
3
- mkdtempSync,
4
- mkdirSync,
5
- existsSync,
6
- readFileSync,
7
- writeFileSync,
8
- } from 'node:fs';
9
- import { tmpdir } from 'node:os';
10
- import { join } from 'node:path';
11
- import { test } from 'node:test';
12
- import { openTaskStore, STATUS } from '../task-store.mjs';
13
-
14
- // A temp project with a .git marker, so the store anchors its .ai/tasks/ there.
15
- function makeProject() {
16
- const project = mkdtempSync(join(tmpdir(), 'task-store-'));
17
- mkdirSync(join(project, '.git'));
18
- return project;
19
- }
20
-
21
- function sampleTask(id) {
22
- return {
23
- id,
24
- title: `task ${id}`,
25
- description: 'desc',
26
- status: STATUS.OPEN,
27
- size: 'trivial',
28
- createdAt: new Date().toISOString(),
29
- messages: [],
30
- };
31
- }
32
-
33
- test('openTaskStore anchors at the nearest project marker (.ai/ counts, not only .git)', () => {
34
- // A project rooted only by .ai/ (no .git) must still resolve — the repo-less case.
35
- const project = mkdtempSync(join(tmpdir(), 'ai-only-'));
36
- mkdirSync(join(project, '.ai'));
37
- const nested = join(project, 'src', 'deep');
38
- mkdirSync(nested, { recursive: true });
39
- const store = openTaskStore(nested);
40
- assert.ok(
41
- store,
42
- 'store resolves from a nested dir under an .ai/-rooted project',
43
- );
44
- assert.equal(store.root, project);
45
- });
46
-
47
- test('add and active: a new task shows up in active', () => {
48
- const store = openTaskStore(makeProject());
49
- store.add(sampleTask('t1'));
50
- const active = store.active();
51
- assert.equal(active.length, 1);
52
- assert.equal(active[0].id, 't1');
53
- assert.deepEqual(store.history(), []);
54
- });
55
-
56
- test('update merges fields into the active task', () => {
57
- const store = openTaskStore(makeProject());
58
- store.add(sampleTask('t1'));
59
- store.update('t1', { status: STATUS.BLOCKED, note: 'waiting' });
60
- const task = store.active()[0];
61
- assert.equal(task.status, STATUS.BLOCKED);
62
- assert.equal(task.note, 'waiting');
63
- assert.equal(store.update('missing', {}), null);
64
- });
65
-
66
- test('close MOVES a task from active to history, never deletes', () => {
67
- const store = openTaskStore(makeProject());
68
- store.add(sampleTask('t1'));
69
- store.add(sampleTask('t2'));
70
-
71
- const closed = store.close('t1', STATUS.DONE, 'finished');
72
- assert.equal(closed.status, STATUS.DONE);
73
- assert.equal(closed.closeReason, 'finished');
74
- assert.ok(closed.closedAt);
75
-
76
- assert.deepEqual(
77
- store.active().map((task) => task.id),
78
- ['t2'],
79
- );
80
- assert.deepEqual(
81
- store.history().map((task) => task.id),
82
- ['t1'],
83
- );
84
- });
85
-
86
- test('abandon is a terminal close: moves to history with reason', () => {
87
- const store = openTaskStore(makeProject());
88
- store.add(sampleTask('t1'));
89
- store.close('t1', STATUS.ABANDONED, 'no longer needed');
90
- assert.equal(store.active().length, 0);
91
- assert.equal(store.history()[0].status, STATUS.ABANDONED);
92
- assert.equal(store.history()[0].closeReason, 'no longer needed');
93
- });
94
-
95
- test('close rejects a non-terminal status and an unknown id', () => {
96
- const store = openTaskStore(makeProject());
97
- store.add(sampleTask('t1'));
98
- assert.equal(store.close('t1', STATUS.OPEN, 'x'), null);
99
- assert.equal(store.close('missing', STATUS.DONE, 'x'), null);
100
- assert.equal(store.active().length, 1);
101
- });
102
-
103
- test('counter persists and reads back', () => {
104
- const store = openTaskStore(makeProject());
105
- assert.equal(store.counter(), 0);
106
- store.setCounter(7);
107
- assert.equal(store.counter(), 7);
108
- });
109
-
110
- test('history is append-only across multiple closes', () => {
111
- const store = openTaskStore(makeProject());
112
- store.add(sampleTask('t1'));
113
- store.add(sampleTask('t2'));
114
- store.close('t1', STATUS.DONE, 'a');
115
- store.close('t2', STATUS.ABANDONED, 'b');
116
- assert.deepEqual(
117
- store.history().map((task) => task.id),
118
- ['t1', 't2'],
119
- );
120
- });
121
-
122
- test('a corrupt active.json is treated as empty, then healed by a write', () => {
123
- const project = makeProject();
124
- const store = openTaskStore(project);
125
- mkdirSync(join(project, '.ai', 'tasks'), { recursive: true });
126
- const path = join(project, '.ai', 'tasks', 'active.json');
127
- writeFileSync(path, '{broken');
128
- assert.deepEqual(store.active(), []);
129
- store.add(sampleTask('t1'));
130
- assert.ok(existsSync(path));
131
- assert.equal(JSON.parse(readFileSync(path, 'utf8')).tasks.length, 1);
132
- });
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ mkdtempSync,
4
+ mkdirSync,
5
+ existsSync,
6
+ readFileSync,
7
+ writeFileSync,
8
+ } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { test } from 'node:test';
12
+ import { openTaskStore, STATUS } from '../task-store.mjs';
13
+
14
+ // A temp project with a .git marker, so the store anchors its .ai/tasks/ there.
15
+ function makeProject() {
16
+ const project = mkdtempSync(join(tmpdir(), 'task-store-'));
17
+ mkdirSync(join(project, '.git'));
18
+ return project;
19
+ }
20
+
21
+ function sampleTask(id) {
22
+ return {
23
+ id,
24
+ title: `task ${id}`,
25
+ description: 'desc',
26
+ status: STATUS.OPEN,
27
+ size: 'trivial',
28
+ createdAt: new Date().toISOString(),
29
+ messages: [],
30
+ };
31
+ }
32
+
33
+ test('openTaskStore anchors at the nearest project marker (.ai/ counts, not only .git)', () => {
34
+ // A project rooted only by .ai/ (no .git) must still resolve — the repo-less case.
35
+ const project = mkdtempSync(join(tmpdir(), 'ai-only-'));
36
+ mkdirSync(join(project, '.ai'));
37
+ const nested = join(project, 'src', 'deep');
38
+ mkdirSync(nested, { recursive: true });
39
+ const store = openTaskStore(nested);
40
+ assert.ok(
41
+ store,
42
+ 'store resolves from a nested dir under an .ai/-rooted project',
43
+ );
44
+ assert.equal(store.root, project);
45
+ });
46
+
47
+ test('add and active: a new task shows up in active', () => {
48
+ const store = openTaskStore(makeProject());
49
+ store.add(sampleTask('t1'));
50
+ const active = store.active();
51
+ assert.equal(active.length, 1);
52
+ assert.equal(active[0].id, 't1');
53
+ assert.deepEqual(store.history(), []);
54
+ });
55
+
56
+ test('update merges fields into the active task', () => {
57
+ const store = openTaskStore(makeProject());
58
+ store.add(sampleTask('t1'));
59
+ store.update('t1', { status: STATUS.BLOCKED, note: 'waiting' });
60
+ const task = store.active()[0];
61
+ assert.equal(task.status, STATUS.BLOCKED);
62
+ assert.equal(task.note, 'waiting');
63
+ assert.equal(store.update('missing', {}), null);
64
+ });
65
+
66
+ test('close MOVES a task from active to history, never deletes', () => {
67
+ const store = openTaskStore(makeProject());
68
+ store.add(sampleTask('t1'));
69
+ store.add(sampleTask('t2'));
70
+
71
+ const { task: closed } = store.close('t1', STATUS.DONE, {
72
+ reason: 'finished',
73
+ evidence: 'npm test -> 12 passing',
74
+ });
75
+ assert.equal(closed.status, STATUS.DONE);
76
+ assert.equal(closed.closeReason, 'finished');
77
+ assert.equal(closed.evidence, 'npm test -> 12 passing');
78
+ assert.ok(closed.closedAt);
79
+
80
+ assert.deepEqual(
81
+ store.active().map((task) => task.id),
82
+ ['t2'],
83
+ );
84
+ assert.deepEqual(
85
+ store.history().map((task) => task.id),
86
+ ['t1'],
87
+ );
88
+ });
89
+
90
+ test('closing as done WITHOUT evidence is refused; the task stays active', () => {
91
+ const store = openTaskStore(makeProject());
92
+ store.add(sampleTask('t1'));
93
+ const result = store.close('t1', STATUS.DONE, { reason: 'trust me' });
94
+ assert.ok(result.error, 'done without evidence must return an error');
95
+ assert.match(result.error, /evidence/i);
96
+ assert.equal(store.active().length, 1, 'the task is not moved to history');
97
+ });
98
+
99
+ test('abandon is a terminal close: moves to history with reason, no evidence needed', () => {
100
+ const store = openTaskStore(makeProject());
101
+ store.add(sampleTask('t1'));
102
+ const { task } = store.close('t1', STATUS.ABANDONED, { reason: 'no longer needed' });
103
+ assert.equal(task.status, STATUS.ABANDONED);
104
+ assert.equal(store.active().length, 0);
105
+ assert.equal(store.history()[0].status, STATUS.ABANDONED);
106
+ assert.equal(store.history()[0].closeReason, 'no longer needed');
107
+ });
108
+
109
+ test('promoteToForge links a run and keeps the task active as in_forge', () => {
110
+ const store = openTaskStore(makeProject());
111
+ store.add(sampleTask('t1'));
112
+ const promoted = store.promoteToForge('t1', 'run-42');
113
+ assert.equal(promoted.status, STATUS.IN_FORGE);
114
+ assert.equal(promoted.forgeRunId, 'run-42');
115
+ assert.equal(store.active().length, 1, 'in_forge is still active, not closed');
116
+ });
117
+
118
+ test('close rejects a non-terminal status and an unknown id', () => {
119
+ const store = openTaskStore(makeProject());
120
+ store.add(sampleTask('t1'));
121
+ assert.ok(store.close('t1', STATUS.OPEN, { reason: 'x' }).error);
122
+ assert.ok(store.close('missing', STATUS.DONE, { evidence: 'x' }).error);
123
+ assert.equal(store.active().length, 1);
124
+ });
125
+
126
+ test('counter persists and reads back', () => {
127
+ const store = openTaskStore(makeProject());
128
+ assert.equal(store.counter(), 0);
129
+ store.setCounter(7);
130
+ assert.equal(store.counter(), 7);
131
+ });
132
+
133
+ test('history is append-only across multiple closes', () => {
134
+ const store = openTaskStore(makeProject());
135
+ store.add(sampleTask('t1'));
136
+ store.add(sampleTask('t2'));
137
+ store.close('t1', STATUS.DONE, { reason: 'a', evidence: 'tests green' });
138
+ store.close('t2', STATUS.ABANDONED, { reason: 'b' });
139
+ assert.deepEqual(
140
+ store.history().map((task) => task.id),
141
+ ['t1', 't2'],
142
+ );
143
+ });
144
+
145
+ test('a corrupt active.json is treated as empty, then healed by a write', () => {
146
+ const project = makeProject();
147
+ const store = openTaskStore(project);
148
+ mkdirSync(join(project, '.ai', 'tasks'), { recursive: true });
149
+ const path = join(project, '.ai', 'tasks', 'active.json');
150
+ writeFileSync(path, '{broken');
151
+ assert.deepEqual(store.active(), []);
152
+ store.add(sampleTask('t1'));
153
+ assert.ok(existsSync(path));
154
+ assert.equal(JSON.parse(readFileSync(path, 'utf8')).tasks.length, 1);
155
+ });
156
+
157
+ test('a UTF-8 BOM on active.json/counter.json does not make the store treat them as empty/corrupt', () => {
158
+ const project = makeProject();
159
+ const tasksDir = join(project, '.ai', 'tasks');
160
+ mkdirSync(tasksDir, { recursive: true });
161
+
162
+ const bom = '';
163
+ writeFileSync(
164
+ join(tasksDir, 'active.json'),
165
+ bom + JSON.stringify({ tasks: [sampleTask('t1')] }),
166
+ );
167
+ writeFileSync(join(tasksDir, 'counter.json'), bom + JSON.stringify({ count: 3 }));
168
+
169
+ const store = openTaskStore(project);
170
+ assert.deepEqual(
171
+ store.active().map((task) => task.id),
172
+ ['t1'],
173
+ 'a BOM-prefixed active.json must still be read as its real content, not as empty',
174
+ );
175
+ assert.equal(
176
+ store.counter(),
177
+ 3,
178
+ 'a BOM-prefixed counter.json must still be read as its real value, not 0',
179
+ );
180
+ });