@drdeeks/character-kit 1.0.1

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/deploy/ack_monitor.py +140 -0
  4. package/deploy/ack_watchdog.py +110 -0
  5. package/deploy/agent-character-monitor.service +21 -0
  6. package/deploy/agent-character-watchdog.service +21 -0
  7. package/deploy/agent-enforcer-proof.service +18 -0
  8. package/deploy/agent-enforcer.service +53 -0
  9. package/deploy/deploy-ack-services.sh +47 -0
  10. package/deploy/deploy-agent-enforcer.sh +133 -0
  11. package/deploy/proof-self-respawn.sh +59 -0
  12. package/node/bin/ack.js +383 -0
  13. package/node/bin/aik.js +520 -0
  14. package/node/bin/install.js +409 -0
  15. package/node/enforcer/agent_enforcer_daemon.js +690 -0
  16. package/node/examples/.agent/constitution.yaml +38 -0
  17. package/node/examples/.agent/enforcer.yaml +33 -0
  18. package/node/examples/.agent/habits/affirm_character_each_action.yaml +9 -0
  19. package/node/examples/.agent/habits/audit_not_silent.yaml +9 -0
  20. package/node/examples/.agent/habits/binding_map_one_core.yaml +9 -0
  21. package/node/examples/.agent/habits/character_hash_visible.yaml +9 -0
  22. package/node/examples/.agent/habits/check_duplication_before_debug.yaml +9 -0
  23. package/node/examples/.agent/habits/complete-thoroughly.yaml +16 -0
  24. package/node/examples/.agent/habits/consistent-info-across-files.yaml +18 -0
  25. package/node/examples/.agent/habits/document-for-next-agent.yaml +15 -0
  26. package/node/examples/.agent/habits/documented_rollback.yaml +9 -0
  27. package/node/examples/.agent/habits/drift_signal_detection.yaml +9 -0
  28. package/node/examples/.agent/habits/due-diligence.yaml +16 -0
  29. package/node/examples/.agent/habits/enterprise-grade-modular.yaml +16 -0
  30. package/node/examples/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
  31. package/node/examples/.agent/habits/forever_one_idea.yaml +9 -0
  32. package/node/examples/.agent/habits/graceful_degradation.yaml +9 -0
  33. package/node/examples/.agent/habits/idempotent_operations.yaml +9 -0
  34. package/node/examples/.agent/habits/interactive-no-stalls.yaml +16 -0
  35. package/node/examples/.agent/habits/layered_not_rewritten.yaml +9 -0
  36. package/node/examples/.agent/habits/lossless_consolidation.yaml +9 -0
  37. package/node/examples/.agent/habits/no-credential-leak.yaml +41 -0
  38. package/node/examples/.agent/habits/no-deception.yaml +14 -0
  39. package/node/examples/.agent/habits/no_hold_narration.yaml +9 -0
  40. package/node/examples/.agent/habits/one_concern_per_file.yaml +9 -0
  41. package/node/examples/.agent/habits/optimized-robust.yaml +16 -0
  42. package/node/examples/.agent/habits/registered_plugin_not_string.yaml +9 -0
  43. package/node/examples/.agent/habits/rename_as_layer_op.yaml +9 -0
  44. package/node/examples/.agent/habits/resolve-root-cause.yaml +15 -0
  45. package/node/examples/.agent/habits/rigorous-commits-no-push.yaml +18 -0
  46. package/node/examples/.agent/habits/safe-deletion-via-trash.yaml +20 -0
  47. package/node/examples/.agent/habits/safe_file_permissions.yaml +9 -0
  48. package/node/examples/.agent/habits/self-healing-portability.yaml +14 -0
  49. package/node/examples/.agent/habits/shippable-pride.yaml +15 -0
  50. package/node/examples/.agent/habits/single_source_of_truth.yaml +9 -0
  51. package/node/examples/.agent/habits/test_of_forever.yaml +9 -0
  52. package/node/examples/.agent/habits/timeout_and_retry.yaml +9 -0
  53. package/node/examples/.agent/habits/track_defects_openly.yaml +9 -0
  54. package/node/examples/.agent/habits/validate-against-real-source.yaml +14 -0
  55. package/node/examples/.agent/habits/verify-against-docs.yaml +15 -0
  56. package/node/examples/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
  57. package/node/examples/.agent/habits/versioning-discipline.yaml +18 -0
  58. package/node/package.json +53 -0
  59. package/node/src/enforcer/client.js +149 -0
  60. package/node/src/hooks/character.js +286 -0
  61. package/node/src/index.js +24 -0
  62. package/node/src/knowledge/indexer.js +486 -0
  63. package/node/src/knowledge/semantic.js +154 -0
  64. package/node/src/memory/index.js +355 -0
  65. package/node/tests/character.test.js +80 -0
  66. package/node/tests/habit-create.test.js +67 -0
  67. package/node/tests/habits-default.test.js +68 -0
  68. package/node/tests/install-chain.test.js +19 -0
  69. package/node/tests/install.test.js +104 -0
  70. package/package.json +65 -0
  71. package/python/agent_character_kit/__init__.py +388 -0
  72. package/python/agent_character_kit/__main__.py +333 -0
  73. package/python/agent_character_kit/_yaml.py +33 -0
  74. package/python/agent_character_kit/enforcer.py +379 -0
  75. package/python/agent_character_kit/memory.py +257 -0
  76. package/python/agent_character_kit/semantic.py +126 -0
  77. package/python/example_workspace/.agent/habits/affirm_character_each_action.yaml +9 -0
  78. package/python/example_workspace/.agent/habits/audit_not_silent.yaml +9 -0
  79. package/python/example_workspace/.agent/habits/binding_map_one_core.yaml +9 -0
  80. package/python/example_workspace/.agent/habits/character_hash_visible.yaml +9 -0
  81. package/python/example_workspace/.agent/habits/check_duplication_before_debug.yaml +9 -0
  82. package/python/example_workspace/.agent/habits/complete-thoroughly.yaml +16 -0
  83. package/python/example_workspace/.agent/habits/consistent-info-across-files.yaml +18 -0
  84. package/python/example_workspace/.agent/habits/document-for-next-agent.yaml +15 -0
  85. package/python/example_workspace/.agent/habits/documented_rollback.yaml +9 -0
  86. package/python/example_workspace/.agent/habits/drift_signal_detection.yaml +9 -0
  87. package/python/example_workspace/.agent/habits/due-diligence.yaml +16 -0
  88. package/python/example_workspace/.agent/habits/enterprise-grade-modular.yaml +16 -0
  89. package/python/example_workspace/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
  90. package/python/example_workspace/.agent/habits/forever_one_idea.yaml +9 -0
  91. package/python/example_workspace/.agent/habits/graceful_degradation.yaml +9 -0
  92. package/python/example_workspace/.agent/habits/idempotent_operations.yaml +9 -0
  93. package/python/example_workspace/.agent/habits/interactive-no-stalls.yaml +16 -0
  94. package/python/example_workspace/.agent/habits/layered_not_rewritten.yaml +9 -0
  95. package/python/example_workspace/.agent/habits/lossless_consolidation.yaml +9 -0
  96. package/python/example_workspace/.agent/habits/no-credential-leak.yaml +41 -0
  97. package/python/example_workspace/.agent/habits/no-deception.yaml +14 -0
  98. package/python/example_workspace/.agent/habits/no_hold_narration.yaml +9 -0
  99. package/python/example_workspace/.agent/habits/one_concern_per_file.yaml +9 -0
  100. package/python/example_workspace/.agent/habits/optimized-robust.yaml +16 -0
  101. package/python/example_workspace/.agent/habits/registered_plugin_not_string.yaml +9 -0
  102. package/python/example_workspace/.agent/habits/rename_as_layer_op.yaml +9 -0
  103. package/python/example_workspace/.agent/habits/resolve-root-cause.yaml +15 -0
  104. package/python/example_workspace/.agent/habits/rigorous-commits-no-push.yaml +18 -0
  105. package/python/example_workspace/.agent/habits/safe-deletion-via-trash.yaml +20 -0
  106. package/python/example_workspace/.agent/habits/safe_file_permissions.yaml +9 -0
  107. package/python/example_workspace/.agent/habits/self-healing-portability.yaml +14 -0
  108. package/python/example_workspace/.agent/habits/shippable-pride.yaml +15 -0
  109. package/python/example_workspace/.agent/habits/single_source_of_truth.yaml +9 -0
  110. package/python/example_workspace/.agent/habits/test_of_forever.yaml +9 -0
  111. package/python/example_workspace/.agent/habits/timeout_and_retry.yaml +9 -0
  112. package/python/example_workspace/.agent/habits/track_defects_openly.yaml +9 -0
  113. package/python/example_workspace/.agent/habits/validate-against-real-source.yaml +14 -0
  114. package/python/example_workspace/.agent/habits/verify-against-docs.yaml +15 -0
  115. package/python/example_workspace/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
  116. package/python/example_workspace/.agent/habits/versioning-discipline.yaml +18 -0
  117. package/python/hermes_plugin/README.md +63 -0
  118. package/python/hermes_plugin/__init__.py +367 -0
  119. package/python/hermes_plugin/config.yaml +20 -0
  120. package/python/hermes_plugin/plugin.yaml +7 -0
  121. package/python/hermes_plugin/test_plugin.py +166 -0
  122. package/python/pyproject.toml +19 -0
  123. package/python/tests/python_parity_test.py +84 -0
@@ -0,0 +1,355 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import yaml from "js-yaml";
4
+
5
+ export class DailyNotes {
6
+ constructor(workspace) {
7
+ this.workspace = workspace || process.cwd();
8
+ this.dailyDir = path.join(this.workspace, "memory", "daily");
9
+ }
10
+
11
+ async init() {
12
+ await fs.mkdir(this.dailyDir, { recursive: true });
13
+ }
14
+
15
+ _todayFile() {
16
+ const date = new Date().toISOString().split("T")[0];
17
+ return path.join(this.dailyDir, `${date}.yaml`);
18
+ }
19
+
20
+ async log(entry, options = {}) {
21
+ const filePath = this._todayFile();
22
+ const now = new Date().toISOString();
23
+
24
+ let data;
25
+ try {
26
+ const raw = await fs.readFile(filePath, "utf-8");
27
+ data = yaml.load(raw) || {};
28
+ } catch {
29
+ data = {
30
+ date: now.split("T")[0],
31
+ created_at: now,
32
+ entries: [],
33
+ };
34
+ }
35
+
36
+ data.entries.push({
37
+ timestamp: now,
38
+ content: entry,
39
+ tags: options.tags || [],
40
+ category: options.category || "general",
41
+ });
42
+ data.updated_at = now;
43
+
44
+ await fs.writeFile(filePath, yaml.dump(data, { lineWidth: -1 }));
45
+ return { status: "logged", file: filePath };
46
+ }
47
+
48
+ async getToday() {
49
+ try {
50
+ const raw = await fs.readFile(this._todayFile(), "utf-8");
51
+ return yaml.load(raw) || {};
52
+ } catch {
53
+ return { date: new Date().toISOString().split("T")[0], entries: [] };
54
+ }
55
+ }
56
+
57
+ async getRange(startDate, endDate) {
58
+ const results = [];
59
+ const start = new Date(startDate);
60
+ const end = new Date(endDate);
61
+
62
+ for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
63
+ const dateStr = d.toISOString().split("T")[0];
64
+ const filePath = path.join(this.dailyDir, `${dateStr}.yaml`);
65
+ try {
66
+ const raw = await fs.readFile(filePath, "utf-8");
67
+ results.push(yaml.load(raw) || {});
68
+ } catch {}
69
+ }
70
+
71
+ return results;
72
+ }
73
+
74
+ async listNotes() {
75
+ const notes = [];
76
+ try {
77
+ const files = await fs.readdir(this.dailyDir);
78
+ const yamlFiles = files.filter((f) => f.endsWith(".yaml")).sort().reverse();
79
+ for (const f of yamlFiles) {
80
+ const raw = await fs.readFile(path.join(this.dailyDir, f), "utf-8");
81
+ const data = yaml.load(raw) || {};
82
+ notes.push({
83
+ date: data.date || f.replace(".yaml", ""),
84
+ file: path.join(this.dailyDir, f),
85
+ entries: (data.entries || []).length,
86
+ });
87
+ }
88
+ } catch {}
89
+ return notes;
90
+ }
91
+ }
92
+
93
+ export class WeeklyDigest {
94
+ constructor(workspace) {
95
+ this.workspace = workspace || process.cwd();
96
+ this.weeklyDir = path.join(this.workspace, "memory", "weekly");
97
+ }
98
+
99
+ async init() {
100
+ await fs.mkdir(this.weeklyDir, { recursive: true });
101
+ }
102
+
103
+ async create(weekStart, summary, options = {}) {
104
+ const filePath = path.join(this.weeklyDir, `week-${weekStart}.yaml`);
105
+ const now = new Date().toISOString();
106
+
107
+ const data = {
108
+ week_start: weekStart,
109
+ created_at: now,
110
+ summary,
111
+ patterns: options.patterns || [],
112
+ decisions: options.decisions || [],
113
+ };
114
+
115
+ await fs.writeFile(filePath, yaml.dump(data, { lineWidth: -1 }));
116
+ return { status: "created", file: filePath };
117
+ }
118
+
119
+ async get(weekStart) {
120
+ const filePath = path.join(this.weeklyDir, `week-${weekStart}.yaml`);
121
+ try {
122
+ const raw = await fs.readFile(filePath, "utf-8");
123
+ return yaml.load(raw) || {};
124
+ } catch {
125
+ return {};
126
+ }
127
+ }
128
+
129
+ async listDigests() {
130
+ const digests = [];
131
+ try {
132
+ const files = await fs.readdir(this.weeklyDir);
133
+ const yamlFiles = files.filter((f) => f.startsWith("week-") && f.endsWith(".yaml")).sort().reverse();
134
+ for (const f of yamlFiles) {
135
+ const raw = await fs.readFile(path.join(this.weeklyDir, f), "utf-8");
136
+ const data = yaml.load(raw) || {};
137
+ digests.push({
138
+ weekStart: data.week_start || f.replace("week-", "").replace(".yaml", ""),
139
+ file: path.join(this.weeklyDir, f),
140
+ patterns: (data.patterns || []).length,
141
+ });
142
+ }
143
+ } catch {}
144
+ return digests;
145
+ }
146
+ }
147
+
148
+ export class LongTermMemory {
149
+ constructor(workspace) {
150
+ this.workspace = workspace || process.cwd();
151
+ this.filePath = path.join(this.workspace, "memory", "long-term.yaml");
152
+ }
153
+
154
+ async init() {
155
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
156
+ }
157
+
158
+ async _load() {
159
+ try {
160
+ const raw = await fs.readFile(this.filePath, "utf-8");
161
+ return yaml.load(raw) || { lessons: [], patterns: [], decisions: [] };
162
+ } catch {
163
+ return { lessons: [], patterns: [], decisions: [] };
164
+ }
165
+ }
166
+
167
+ async _save(data) {
168
+ data.updated_at = new Date().toISOString();
169
+ await fs.writeFile(this.filePath, yaml.dump(data, { lineWidth: -1 }));
170
+ }
171
+
172
+ async addLesson(title, content, options = {}) {
173
+ const data = await this._load();
174
+ data.lessons.push({
175
+ title,
176
+ content,
177
+ tags: options.tags || [],
178
+ category: options.category || "general",
179
+ added_at: new Date().toISOString(),
180
+ });
181
+ await this._save(data);
182
+ return { status: "added", total: data.lessons.length };
183
+ }
184
+
185
+ async addPattern(name, description, examples = []) {
186
+ const data = await this._load();
187
+ data.patterns.push({
188
+ name,
189
+ description,
190
+ examples,
191
+ added_at: new Date().toISOString(),
192
+ });
193
+ await this._save(data);
194
+ return { status: "added", total: data.patterns.length };
195
+ }
196
+
197
+ async addDecision(title, context, decision, rationale) {
198
+ const data = await this._load();
199
+ data.decisions.push({
200
+ title,
201
+ context,
202
+ decision,
203
+ rationale,
204
+ made_at: new Date().toISOString(),
205
+ });
206
+ await this._save(data);
207
+ return { status: "added", total: data.decisions.length };
208
+ }
209
+
210
+ async search(query) {
211
+ const data = await this._load();
212
+ const q = query.toLowerCase();
213
+ const results = [];
214
+
215
+ for (const lesson of data.lessons || []) {
216
+ if ((lesson.title + lesson.content).toLowerCase().includes(q)) {
217
+ results.push({ type: "lesson", ...lesson });
218
+ }
219
+ }
220
+ for (const pattern of data.patterns || []) {
221
+ if ((pattern.name + pattern.description).toLowerCase().includes(q)) {
222
+ results.push({ type: "pattern", ...pattern });
223
+ }
224
+ }
225
+ for (const decision of data.decisions || []) {
226
+ if ((decision.title + decision.decision).toLowerCase().includes(q)) {
227
+ results.push({ type: "decision", ...decision });
228
+ }
229
+ }
230
+ return results;
231
+ }
232
+
233
+ async getAll() {
234
+ return this._load();
235
+ }
236
+ }
237
+
238
+ export class KnowledgeGraph {
239
+ constructor(workspace) {
240
+ this.workspace = workspace || process.cwd();
241
+ this.entitiesDir = path.join(this.workspace, "knowledge", "entities");
242
+ }
243
+
244
+ async init() {
245
+ await fs.mkdir(this.entitiesDir, { recursive: true });
246
+ }
247
+
248
+ _entityFile(name) {
249
+ return path.join(this.entitiesDir, `${name.toLowerCase().replace(/[\s]+/g, "-")}.yaml`);
250
+ }
251
+
252
+ async addEntity(name, type, facts = {}, tags = []) {
253
+ const filePath = this._entityFile(name);
254
+ const now = new Date().toISOString();
255
+
256
+ let existing = {};
257
+ try {
258
+ const raw = await fs.readFile(filePath, "utf-8");
259
+ existing = yaml.load(raw) || {};
260
+ } catch {}
261
+
262
+ const data = {
263
+ name,
264
+ type,
265
+ tags,
266
+ facts,
267
+ created_at: existing.created_at || now,
268
+ updated_at: now,
269
+ };
270
+
271
+ await fs.writeFile(filePath, yaml.dump(data, { lineWidth: -1 }));
272
+ return { status: "added", file: filePath };
273
+ }
274
+
275
+ async getEntity(name) {
276
+ try {
277
+ const raw = await fs.readFile(this._entityFile(name), "utf-8");
278
+ return yaml.load(raw) || {};
279
+ } catch {
280
+ return {};
281
+ }
282
+ }
283
+
284
+ async searchEntities(query) {
285
+ const results = [];
286
+ try {
287
+ const files = await fs.readdir(this.entitiesDir);
288
+ for (const f of files) {
289
+ if (!f.endsWith(".yaml")) continue;
290
+ const raw = await fs.readFile(path.join(this.entitiesDir, f), "utf-8");
291
+ const data = yaml.load(raw) || {};
292
+ if (JSON.stringify(data).toLowerCase().includes(query.toLowerCase())) {
293
+ results.push(data);
294
+ }
295
+ }
296
+ } catch {}
297
+ return results;
298
+ }
299
+
300
+ async listEntities(type = null) {
301
+ const entities = [];
302
+ try {
303
+ const files = await fs.readdir(this.entitiesDir);
304
+ for (const f of files) {
305
+ if (!f.endsWith(".yaml")) continue;
306
+ const raw = await fs.readFile(path.join(this.entitiesDir, f), "utf-8");
307
+ const data = yaml.load(raw) || {};
308
+ if (type && data.type !== type) continue;
309
+ entities.push({ name: data.name, type: data.type, file: path.join(this.entitiesDir, f) });
310
+ }
311
+ } catch {}
312
+ return entities;
313
+ }
314
+ }
315
+
316
+ export class Memory {
317
+ constructor(workspace) {
318
+ this.workspace = workspace || process.cwd();
319
+ this.daily = new DailyNotes(this.workspace);
320
+ this.weekly = new WeeklyDigest(this.workspace);
321
+ this.longterm = new LongTermMemory(this.workspace);
322
+ this.knowledge = new KnowledgeGraph(this.workspace);
323
+ }
324
+
325
+ async init() {
326
+ await Promise.all([
327
+ this.daily.init(),
328
+ this.weekly.init(),
329
+ this.longterm.init(),
330
+ this.knowledge.init(),
331
+ ]);
332
+ }
333
+
334
+ async status() {
335
+ const dailyNotes = await this.daily.listNotes();
336
+ const weeklyDigests = await this.weekly.listDigests();
337
+ const longterm = await this.longterm.getAll();
338
+ const entities = await this.knowledge.listEntities();
339
+
340
+ return {
341
+ dailyNotes: dailyNotes.length,
342
+ weeklyDigests: weeklyDigests.length,
343
+ longtermLessons: (longterm.lessons || []).length,
344
+ longtermPatterns: (longterm.patterns || []).length,
345
+ longtermDecisions: (longterm.decisions || []).length,
346
+ entities: entities.length,
347
+ };
348
+ }
349
+
350
+ async search(query) {
351
+ const longterm = await this.longterm.search(query);
352
+ const entities = await this.knowledge.searchEntities(query);
353
+ return { longterm, entities };
354
+ }
355
+ }
@@ -0,0 +1,80 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { processToolCall, generateConfig } from "../src/index.js";
4
+ import { DocumentIndexer } from "../src/knowledge/indexer.js";
5
+ import fs from "fs";
6
+ import os from "os";
7
+ import path from "path";
8
+
9
+ // ─── Identity hook ───────────────────────────────────────────────────────────
10
+
11
+ test("hook formats Claude allow decision", async () => {
12
+ const { output, exitCode } = await processToolCall(
13
+ { tool_name: "Bash", tool_input: { command: "ls" }, hook_event_name: "PreToolUse" },
14
+ { framework: "claude", enforcer: { validateTool: async () => ({ allowed: true }) } }
15
+ );
16
+ assert.equal(output.hookSpecificOutput.permissionDecision, "allow");
17
+ assert.equal(exitCode, 0);
18
+ });
19
+
20
+ test("hook fails CLOSED when enforcer unreachable", async () => {
21
+ const { output, exitCode } = await processToolCall(
22
+ { tool_name: "Bash", tool_input: { command: "rm -rf /" }, hook_event_name: "PreToolUse" },
23
+ { framework: "claude", enforcer: { validateTool: async () => ({ allowed: false, error: true, reason: "down" }) } }
24
+ );
25
+ assert.equal(output.hookSpecificOutput.permissionDecision, "deny");
26
+ assert.equal(exitCode, 2);
27
+ });
28
+
29
+ test("generateConfig emits claude/cursor/gemini blocks", () => {
30
+ const claude = generateConfig("claude");
31
+ const cursor = generateConfig("cursor");
32
+ const gemini = generateConfig("gemini");
33
+ assert.ok(claude.hooks.PreToolUse);
34
+ assert.ok(cursor.hooks.preToolUse);
35
+ assert.ok(gemini.hooks.BeforeTool);
36
+ });
37
+
38
+ // ─── Indexer: discovery + frontmatter + links ─────────────────────────────────
39
+
40
+ function makeWorkspace() {
41
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aik-test-"));
42
+ fs.writeFileSync(path.join(dir, "notes.md"), "# Notes\nSee [[guide]].\n");
43
+ fs.writeFileSync(path.join(dir, "guide.md"), "# Guide\nthe authentication flow lives here\n");
44
+ fs.writeFileSync(path.join(dir, "code.py"), "x = 1\n");
45
+ return dir;
46
+ }
47
+
48
+ test("indexer discovers broad file types and injects frontmatter", async () => {
49
+ const dir = makeWorkspace();
50
+ const idx = new DocumentIndexer(dir);
51
+ await idx.init();
52
+ const res = await idx.indexDirectory(dir, {});
53
+ assert.equal(res.indexed, 3);
54
+ const yaml = fs.readFileSync(path.join(dir, "knowledge", "documents", "notes.yaml"), "utf-8");
55
+ assert.match(yaml, /^---[\s\S]*?id:/);
56
+ assert.match(yaml, /category:/);
57
+ });
58
+
59
+ test("indexer documents wiki links", async () => {
60
+ const dir = makeWorkspace();
61
+ const idx = new DocumentIndexer(dir);
62
+ await idx.init();
63
+ await idx.indexDirectory(dir, {});
64
+ const notes = Object.values(idx.index.documents).find((d) => d.path.endsWith("notes.md"));
65
+ assert.ok(notes.links.includes("guide"));
66
+ assert.ok(Object.keys(idx.index.links).length >= 1);
67
+ });
68
+
69
+ test("indexer excludes agent-internal files (SOUL.md, constitution)", async () => {
70
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aik-excl-"));
71
+ fs.writeFileSync(path.join(dir, "SOUL.md"), "# Soul\n");
72
+ fs.mkdirSync(path.join(dir, ".agent"), { recursive: true });
73
+ fs.writeFileSync(path.join(dir, ".agent", "constitution.yaml"), "agent:\n id: x\n");
74
+ fs.writeFileSync(path.join(dir, "userdoc.md"), "real corpus doc\n");
75
+ const idx = new DocumentIndexer(dir);
76
+ await idx.init();
77
+ const res = await idx.indexDirectory(dir, {});
78
+ assert.equal(res.indexed, 1);
79
+ assert.ok(Object.keys(idx.index.documents).some((k) => k.endsWith("userdoc")));
80
+ });
@@ -0,0 +1,67 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { spawn } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import net from "node:net";
8
+
9
+ const REPO = path.resolve(process.cwd());
10
+ const INSTALL = path.join(REPO, "node", "bin", "install.js");
11
+ const DAEMON = path.join(REPO, "node", "enforcer", "agent_enforcer_daemon.js");
12
+
13
+ function rpc(sock, method, params) {
14
+ return new Promise((res, rej) => {
15
+ const c = net.connect(sock, () => c.write(JSON.stringify({ method, params }) + "\n"));
16
+ let buf = "";
17
+ c.on("data", (d) => {
18
+ buf += d.toString();
19
+ if (buf.includes("\n")) { c.end(); try { res(JSON.parse(buf.split("\n")[0])); } catch (e) { rej(e); } }
20
+ });
21
+ c.on("error", rej);
22
+ setTimeout(() => rej(new Error("timeout")), 5000);
23
+ });
24
+ }
25
+
26
+ test("create-habit generates a valid habit file that the daemon indexes", { timeout: 20000 }, async () => {
27
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), "ackhabit-"));
28
+ const sock = path.join(ws, ".agent", "enforcer.sock");
29
+
30
+ // create the habit non-interactively
31
+ const cr = spawn(process.execPath, [INSTALL, "--create-habit",
32
+ "--workspace", ws,
33
+ "--habit-name", "always-verify-before-ship",
34
+ "--habit-prompt", "Did I actually verify this runs before claiming done?",
35
+ "--habit-logic", "A thing that parses can still be wrong, so proof requires execution."], { stdio: ["ignore", "pipe", "pipe"] });
36
+ let errOut = "";
37
+ cr.stderr.on("data", (d) => (errOut += d));
38
+ cr.stdout.on("data", (d) => (errOut += d));
39
+ await new Promise((r) => cr.on("exit", r));
40
+
41
+ const file = path.join(ws, ".agent", "habits", "always_verify_before_ship.yaml");
42
+ if (!fs.existsSync(file)) {
43
+ console.log("CREATE-HABIT OUTPUT:", errOut);
44
+ }
45
+ assert.ok(fs.existsSync(file), "habit yaml should be written");
46
+ const text = fs.readFileSync(file, "utf8");
47
+ assert.ok(/name: "always_verify_before_ship"/.test(text), "name normalized to snake_case");
48
+ assert.ok(/prompt: "Did I actually verify this runs before claiming done\?"/.test(text), "prompt field present");
49
+
50
+ // boot the daemon pointing at this workspace; the new habit must be known
51
+ const env = { ...process.env, AGENT_WORKSPACE: ws, ENFORCER_SOCKET: sock, HOME: os.homedir() };
52
+ fs.mkdirSync(path.join(ws, ".agent", "habits"), { recursive: true });
53
+ const child = spawn(process.execPath, [DAEMON], { env, detached: true, stdio: "ignore" });
54
+ child.unref();
55
+ const start = Date.now();
56
+ while (!fs.existsSync(sock) && Date.now() - start < 3000) await new Promise((r) => setTimeout(r, 50));
57
+
58
+ // the new habit should be acknowledged (known) by the daemon
59
+ const ack = await rpc(sock, "submit_ack", {
60
+ session_id: "hb",
61
+ statement: "Habit: always_verify_before_ship why: it applies because the daemon loaded it from the freshly created habit file",
62
+ });
63
+ assert.equal(ack.ok, true, "daemon should know the newly created habit");
64
+
65
+ try { process.kill(-child.pid, "SIGKILL"); } catch {}
66
+ fs.rmSync(ws, { recursive: true, force: true });
67
+ });
@@ -0,0 +1,68 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { spawn } from "node:child_process";
4
+ import net from "node:net";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+
9
+ const REPO = path.resolve(process.cwd());
10
+ const DAEMON = path.join(REPO, "node", "enforcer", "agent_enforcer_daemon.js");
11
+ const SRC = path.join(REPO, "python", "example_workspace", ".agent", "habits");
12
+
13
+ // The decision-logic habits extracted from the user's named sources:
14
+ // /home/ubuntu/qwen-cloud-2026/FOREVER-SYSTEM.md (full 10-section protocol)
15
+ // /home/ubuntu/qwen-cloud-2026/drdeeks-skills/skill-creator/references/standards.md
16
+ // These are part of the DEFAULT bundled set (seeded by install.js).
17
+ const EXTRACTED = [
18
+ // FOREVER-SYSTEM.md (faithful to the full protocol, not a summary)
19
+ "single_source_of_truth", "layered_not_rewritten", "fail_closed_tamper_evident",
20
+ "affirm_character_each_action", "track_defects_openly", "test_of_forever",
21
+ "rename_as_layer_op", "check_duplication_before_debug",
22
+ "drift_signal_detection", "registered_plugin_not_string", "audit_not_silent",
23
+ "binding_map_one_core", "character_hash_visible", "forever_one_idea",
24
+ // skill-creator standards.md (§5/§6/§10/§4)
25
+ "idempotent_operations", "documented_rollback", "graceful_degradation",
26
+ "timeout_and_retry", "lossless_consolidation", "safe_file_permissions",
27
+ "one_concern_per_file",
28
+ ];
29
+
30
+ function rpc(sock, method, params) {
31
+ return new Promise((res, rej) => {
32
+ const c = net.connect(sock, () => c.write(JSON.stringify({ method, params }) + "\n"));
33
+ let buf = "";
34
+ c.on("data", (d) => {
35
+ buf += d;
36
+ if (buf.includes("\n")) { c.end(); try { res(JSON.parse(buf.split("\n")[0])); } catch (e) { rej(e); } }
37
+ });
38
+ c.on("error", rej);
39
+ });
40
+ }
41
+
42
+ test("default bundled habits include the 15 extracted decision-logic habits", async () => {
43
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), "ackdefault-"));
44
+ fs.mkdirSync(path.join(ws, ".agent", "habits"), { recursive: true });
45
+ for (const f of fs.readdirSync(SRC)) {
46
+ fs.copyFileSync(path.join(SRC, f), path.join(ws, ".agent", "habits", f));
47
+ }
48
+ const sock = path.join(ws, ".agent", "enforcer.sock");
49
+ const d = spawn(process.execPath, [DAEMON], {
50
+ env: { ...process.env, AGENT_WORKSPACE: ws, ENFORCER_SOCKET: sock }, stdio: "ignore",
51
+ });
52
+
53
+ let known = [];
54
+ try {
55
+ await new Promise((r) => setTimeout(r, 700));
56
+ for (let i = 1; i <= 5; i++) {
57
+ const r = await rpc(sock, "tool_tick", { session_id: "default-test", tool: "terminal" });
58
+ if (r.hold) { known = r.habits || []; break; }
59
+ }
60
+ } finally {
61
+ d.kill();
62
+ }
63
+
64
+ assert.ok(known.length >= 32, `expected >=32 default habits, got ${known.length}`);
65
+ for (const name of EXTRACTED) {
66
+ assert.ok(known.includes(name), `extracted habit missing from default set: ${name}`);
67
+ }
68
+ });
@@ -0,0 +1,19 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ const REPO = path.resolve(process.cwd());
7
+ const INSTALL = path.join(REPO, "node", "bin", "install.js");
8
+
9
+ test("install.js exists and is valid JavaScript", { timeout: 5000 }, async () => {
10
+ // Just verify the install script exists and is syntactically valid
11
+ assert.ok(fs.existsSync(INSTALL), "install.js should exist");
12
+
13
+ const content = fs.readFileSync(INSTALL, "utf8");
14
+ assert.ok(content.includes("export { parseArgs, resolveSocket }"), "should export parseArgs and resolveSocket");
15
+ assert.ok(content.includes("async function main()"), "should have main function");
16
+ assert.ok(content.includes("launchDaemon"), "should have launchDaemon function");
17
+ assert.ok(content.includes("seedHabits"), "should have seedHabits function");
18
+ assert.ok(content.includes("writeConstitution"), "should have writeConstitution function");
19
+ });