@cryptiklemur/lattice 5.8.3 → 5.10.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 (93) hide show
  1. package/dist/client/assets/{angular-html-BpO2Tp5w.js → angular-html-BDIcxkJq.js} +1 -1
  2. package/dist/client/assets/{angular-ts-DgHuzSgH.js → angular-ts-Bt22ouNH.js} +1 -1
  3. package/dist/client/assets/{apl-DN-3g-kA.js → apl-p8qkxzEK.js} +1 -1
  4. package/dist/client/assets/{astro-DC7TkZXH.js → astro-CIaMc49M.js} +1 -1
  5. package/dist/client/assets/{blade-DJeBezn7.js → blade-BR56EAMD.js} +1 -1
  6. package/dist/client/assets/{c-CWPHtG_V.js → c-Dli0HzAh.js} +1 -1
  7. package/dist/client/assets/{cobol-TtN3pbhH.js → cobol-Cad15ECy.js} +1 -1
  8. package/dist/client/assets/{coffee-DR36C-Bj.js → coffee-DpyATEbF.js} +1 -1
  9. package/dist/client/assets/{cpp-C9XaUQ0i.js → cpp-KN8_NFsf.js} +1 -1
  10. package/dist/client/assets/{crystal-T-sTBJEW.js → crystal-CuyGv0kh.js} +1 -1
  11. package/dist/client/assets/{css-BX5yO7SA.js → css-Cm3q4bxn.js} +1 -1
  12. package/dist/client/assets/{dist-VJaucYqW.js → dist-BjxsMc4u.js} +2 -2
  13. package/dist/client/assets/{edge-B6UK8iH4.js → edge-B6S7CSbx.js} +1 -1
  14. package/dist/client/assets/{elixir-CgftVsub.js → elixir-CNUy9H8T.js} +1 -1
  15. package/dist/client/assets/{elm-Bm5ehGFJ.js → elm-CNfcWmb9.js} +1 -1
  16. package/dist/client/assets/{erb-BGdFwtqx.js → erb-DWebzDaI.js} +1 -1
  17. package/dist/client/assets/{git-rebase-Dgch70i-.js → git-rebase-B_Pt2ZBK.js} +1 -1
  18. package/dist/client/assets/{glimmer-js-CO2m7K3o.js → glimmer-js-CVwoOd72.js} +1 -1
  19. package/dist/client/assets/{glimmer-ts-Rh9OBnOT.js → glimmer-ts-CjtFSxjz.js} +1 -1
  20. package/dist/client/assets/{glsl-DKLX4cjW.js → glsl-CP4rggAA.js} +1 -1
  21. package/dist/client/assets/{graphql-CirzzkU1.js → graphql-Dbm6sAtp.js} +1 -1
  22. package/dist/client/assets/{hack-CQC9znUp.js → hack-Bj9y3SGf.js} +1 -1
  23. package/dist/client/assets/{haml-ze151Tzg.js → haml-DRGrdf3f.js} +1 -1
  24. package/dist/client/assets/{handlebars-CxjP8Lo0.js → handlebars-CFKjcBMg.js} +1 -1
  25. package/dist/client/assets/{html-CqXIaUHF.js → html-Vcd4eHHg.js} +1 -1
  26. package/dist/client/assets/{html-derivative-BzQtEeTI.js → html-derivative-BF0YbD4L.js} +1 -1
  27. package/dist/client/assets/{http-DuZ92gpQ.js → http-CGVTa2NT.js} +1 -1
  28. package/dist/client/assets/{hurl-CAbh6Y6a.js → hurl-B0GrsGqd.js} +1 -1
  29. package/dist/client/assets/{index-E8YNABWy.js → index-CX1tudsF.js} +132 -132
  30. package/dist/client/assets/index-DlfI20Gn.css +2 -0
  31. package/dist/client/assets/{java-DzcsTbJs.js → java-BJHQqHsm.js} +1 -1
  32. package/dist/client/assets/{javascript-DClRq2ts.js → javascript-CmuMsKrc.js} +1 -1
  33. package/dist/client/assets/{jinja-B5sT9_-9.js → jinja-JxCLeq1j.js} +1 -1
  34. package/dist/client/assets/{jison-CU3zhnCb.js → jison-BdgAUhei.js} +1 -1
  35. package/dist/client/assets/{json-CQp0L0ej.js → json-DtPissHL.js} +1 -1
  36. package/dist/client/assets/{jsx-Bf-5FvbF.js → jsx-DUAxxDkP.js} +1 -1
  37. package/dist/client/assets/{julia-CydtGy78.js → julia-DxDlbL6e.js} +1 -1
  38. package/dist/client/assets/{just-DdklfRff.js → just-CVmAAx2R.js} +1 -1
  39. package/dist/client/assets/{latex-Br5dIruj.js → latex-uwxggTWA.js} +1 -1
  40. package/dist/client/assets/{liquid-BzrfNGvH.js → liquid-xsETAJJy.js} +1 -1
  41. package/dist/client/assets/{lua-Cj4dlLGr.js → lua-B2Hh8PgD.js} +1 -1
  42. package/dist/client/assets/{marko-BSCcyzMZ.js → marko-yDeGxD87.js} +1 -1
  43. package/dist/client/assets/{mdc-BPOjCacH.js → mdc-QMp4ieYR.js} +1 -1
  44. package/dist/client/assets/{nginx-DbzWTwI6.js → nginx-7gmRmcqz.js} +1 -1
  45. package/dist/client/assets/{nim-CRdChtbV.js → nim-CA8SNY_7.js} +1 -1
  46. package/dist/client/assets/{perl-C3QVEeKS.js → perl-lx5nW4VC.js} +1 -1
  47. package/dist/client/assets/{php-C1EdFiJm.js → php-DgHiW953.js} +1 -1
  48. package/dist/client/assets/{pug-CO6P9E1X.js → pug-CbbB1vwb.js} +1 -1
  49. package/dist/client/assets/{qml-iW4zlehx.js → qml-COrzwCIh.js} +1 -1
  50. package/dist/client/assets/{r-C6XBkrUL.js → r-Dv7pZJDH.js} +1 -1
  51. package/dist/client/assets/{razor-CeQnxDgb.js → razor-D2m8EDP5.js} +1 -1
  52. package/dist/client/assets/{regexp-Dy8aAKap.js → regexp-BXLT-jPc.js} +1 -1
  53. package/dist/client/assets/{rst-OwTtwL0i.js → rst-_S6rrUYh.js} +1 -1
  54. package/dist/client/assets/{ruby-D7JWM2N5.js → ruby-C3XO7tYY.js} +1 -1
  55. package/dist/client/assets/{sas-DiK66SDU.js → sas-DP2k4iuN.js} +1 -1
  56. package/dist/client/assets/{scss-qrlTvxMb.js → scss-lhLFMXGn.js} +1 -1
  57. package/dist/client/assets/{shellscript-D7AOrbZb.js → shellscript-BYlBPHen.js} +1 -1
  58. package/dist/client/assets/{shellsession-DjQiM7TM.js → shellsession-CbVyQKWZ.js} +1 -1
  59. package/dist/client/assets/{soy-BL7E9JSD.js → soy-Be8a0lHq.js} +1 -1
  60. package/dist/client/assets/{sql-CteFkLc2.js → sql-2KxvU9YS.js} +1 -1
  61. package/dist/client/assets/{stata-B3MgNvuI.js → stata-BxlWftTS.js} +1 -1
  62. package/dist/client/assets/{surrealql-COKgmBsN.js → surrealql-CJ-q86nR.js} +1 -1
  63. package/dist/client/assets/{svelte-Gjt4fCGF.js → svelte-Q1ml0OiY.js} +1 -1
  64. package/dist/client/assets/{templ-Bj518YFy.js → templ-BbfPZhtu.js} +1 -1
  65. package/dist/client/assets/{tex-DRQn3t1e.js → tex-Dcth4Gi6.js} +1 -1
  66. package/dist/client/assets/{ts-tags-Ca2ut20u.js → ts-tags-BKhSOXI3.js} +1 -1
  67. package/dist/client/assets/{tsx-q8zyOWFk.js → tsx-CS6iQ0XH.js} +1 -1
  68. package/dist/client/assets/{twig-TjCAErzr.js → twig-BHp31ZxS.js} +1 -1
  69. package/dist/client/assets/{typescript-p7KFNEGW.js → typescript-16YJBTaO.js} +1 -1
  70. package/dist/client/assets/{vue-BPqqN4qD.js → vue-CMKwTi4r.js} +1 -1
  71. package/dist/client/assets/{vue-html-DWzvGKv5.js → vue-html-Dr8VUA2G.js} +1 -1
  72. package/dist/client/assets/{vue-vine-ooxqXIjP.js → vue-vine-DZUqDerl.js} +1 -1
  73. package/dist/client/assets/{xml-B8jNsjt9.js → xml-CBbBKKDC.js} +1 -1
  74. package/dist/client/assets/{xsl-B6pyn4Tp.js → xsl-DWEX6PKX.js} +1 -1
  75. package/dist/client/assets/{yaml-BN6nvuC4.js → yaml-DvKvvh3X.js} +1 -1
  76. package/dist/client/index.html +2 -2
  77. package/dist/client/sw.js +1 -1
  78. package/dist/server/daemon.js +42 -2
  79. package/dist/server/features/context-analyzer.js +239 -0
  80. package/dist/server/features/session-history.js +127 -0
  81. package/dist/server/features/specs.js +87 -1
  82. package/dist/server/features/superpowers.js +173 -0
  83. package/dist/server/handlers/chat.js +4 -0
  84. package/dist/server/handlers/context-hooks.js +171 -0
  85. package/dist/server/handlers/hooks.js +233 -0
  86. package/dist/server/handlers/session.js +1 -1
  87. package/dist/server/handlers/specs.js +57 -0
  88. package/dist/server/handlers/superpowers.js +13 -0
  89. package/dist/server/logger.js +1 -0
  90. package/dist/server/project/sdk-bridge.js +67 -2
  91. package/dist/server/project/session.js +2 -1
  92. package/package.json +1 -1
  93. package/dist/client/assets/index-etW8QK5W.css +0 -2
@@ -0,0 +1,127 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getLatticeHome } from "../config.js";
4
+ import { log } from "../logger.js";
5
+ function getSessionDir() {
6
+ const dir = join(getLatticeHome(), "session-history");
7
+ if (!existsSync(dir)) {
8
+ mkdirSync(dir, { recursive: true });
9
+ }
10
+ return dir;
11
+ }
12
+ function sessionPath(hookSessionId) {
13
+ const safe = hookSessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
14
+ return join(getSessionDir(), safe + ".json");
15
+ }
16
+ const sessionCache = new Map();
17
+ let indexLoaded = false;
18
+ export function loadSessionHistory() {
19
+ if (indexLoaded)
20
+ return Array.from(sessionCache.values());
21
+ const dir = getSessionDir();
22
+ try {
23
+ const files = readdirSync(dir).filter(function (f) { return f.endsWith(".json"); });
24
+ for (const file of files) {
25
+ try {
26
+ const raw = readFileSync(join(dir, file), "utf-8");
27
+ const session = JSON.parse(raw);
28
+ if (session.hookSessionId) {
29
+ sessionCache.set(session.hookSessionId, session);
30
+ }
31
+ }
32
+ catch {
33
+ log.server("Failed to read session history file: %s", file);
34
+ }
35
+ }
36
+ indexLoaded = true;
37
+ log.server("Loaded %d historical sessions", sessionCache.size);
38
+ }
39
+ catch {
40
+ indexLoaded = true;
41
+ }
42
+ return Array.from(sessionCache.values());
43
+ }
44
+ export function saveSession(session) {
45
+ sessionCache.set(session.hookSessionId, session);
46
+ try {
47
+ const path = sessionPath(session.hookSessionId);
48
+ writeFileSync(path, JSON.stringify(session), "utf-8");
49
+ }
50
+ catch (err) {
51
+ log.server("Failed to save session history: %s", String(err));
52
+ }
53
+ }
54
+ export function getSession(hookSessionId) {
55
+ return sessionCache.get(hookSessionId) || null;
56
+ }
57
+ export function upsertFromSnapshot(hookSessionId, snapshot) {
58
+ let existing = sessionCache.get(hookSessionId);
59
+ if (!existing) {
60
+ existing = {
61
+ hookSessionId,
62
+ ...snapshot,
63
+ timestamp: Date.now(),
64
+ endedAt: null,
65
+ active: true,
66
+ toolEvents: [],
67
+ toolDeltas: [],
68
+ anomalyCount: 0,
69
+ };
70
+ }
71
+ else {
72
+ existing.inputTokens = snapshot.inputTokens;
73
+ existing.outputTokens = snapshot.outputTokens;
74
+ existing.cacheReadTokens = snapshot.cacheReadTokens;
75
+ existing.cacheCreationTokens = snapshot.cacheCreationTokens;
76
+ existing.contextWindow = snapshot.contextWindow;
77
+ existing.usedPercent = snapshot.usedPercent;
78
+ existing.costUsd = snapshot.costUsd;
79
+ existing.durationMs = snapshot.durationMs;
80
+ existing.modelId = snapshot.modelId || existing.modelId;
81
+ existing.modelName = snapshot.modelName || existing.modelName;
82
+ existing.projectName = snapshot.projectName || existing.projectName;
83
+ existing.projectSlug = snapshot.projectSlug || existing.projectSlug;
84
+ existing.timestamp = Date.now();
85
+ }
86
+ saveSession(existing);
87
+ }
88
+ export function addToolEventToHistory(hookSessionId, event) {
89
+ const existing = sessionCache.get(hookSessionId);
90
+ if (!existing)
91
+ return;
92
+ if (existing.toolEvents.length >= 500) {
93
+ existing.toolEvents = existing.toolEvents.slice(-499);
94
+ }
95
+ existing.toolEvents.push(event);
96
+ }
97
+ export function addToolDeltaToHistory(hookSessionId, delta) {
98
+ const existing = sessionCache.get(hookSessionId);
99
+ if (!existing)
100
+ return;
101
+ if (existing.toolDeltas.length >= 500) {
102
+ existing.toolDeltas = existing.toolDeltas.slice(-499);
103
+ }
104
+ existing.toolDeltas.push(delta);
105
+ }
106
+ export function markSessionEnded(hookSessionId) {
107
+ const existing = sessionCache.get(hookSessionId);
108
+ if (!existing)
109
+ return;
110
+ existing.active = false;
111
+ existing.endedAt = Date.now();
112
+ saveSession(existing);
113
+ }
114
+ export function listSessions(options) {
115
+ let sessions = Array.from(sessionCache.values());
116
+ if (options?.projectSlug) {
117
+ sessions = sessions.filter(function (s) { return s.projectSlug === options.projectSlug; });
118
+ }
119
+ if (options?.active !== undefined) {
120
+ sessions = sessions.filter(function (s) { return s.active === options.active; });
121
+ }
122
+ sessions.sort(function (a, b) { return b.timestamp - a.timestamp; });
123
+ if (options?.limit) {
124
+ sessions = sessions.slice(0, options.limit);
125
+ }
126
+ return sessions;
127
+ }
@@ -197,7 +197,92 @@ export function deleteSpec(id) {
197
197
  }
198
198
  return false;
199
199
  }
200
- export function linkSession(specId, sessionId, note) {
200
+ export function populateSpec(id, fields, sessionId) {
201
+ for (var i = 0; i < specs.length; i++) {
202
+ if (specs[i].id !== id)
203
+ continue;
204
+ var spec = specs[i];
205
+ if (fields.title && typeof fields.title === "string")
206
+ spec.title = fields.title;
207
+ if (fields.tagline && typeof fields.tagline === "string")
208
+ spec.tagline = fields.tagline;
209
+ if (fields.priority && typeof fields.priority === "string")
210
+ spec.priority = fields.priority;
211
+ if (fields.estimatedEffort && typeof fields.estimatedEffort === "string")
212
+ spec.estimatedEffort = fields.estimatedEffort;
213
+ if (fields.tags && Array.isArray(fields.tags))
214
+ spec.tags = fields.tags;
215
+ if (fields.summary && typeof fields.summary === "string")
216
+ spec.sections.summary = fields.summary;
217
+ if (fields.currentState && typeof fields.currentState === "string")
218
+ spec.sections.currentState = fields.currentState;
219
+ if (fields.requirements && typeof fields.requirements === "string")
220
+ spec.sections.requirements = fields.requirements;
221
+ if (fields.implementationPlan && typeof fields.implementationPlan === "string")
222
+ spec.sections.implementationPlan = fields.implementationPlan;
223
+ if (fields.testing && typeof fields.testing === "string")
224
+ spec.sections.testing = fields.testing;
225
+ spec.updatedAt = Date.now();
226
+ spec.activity.push({
227
+ timestamp: Date.now(),
228
+ type: "ai-note",
229
+ detail: "Spec populated from brainstorm session",
230
+ sessionId,
231
+ });
232
+ saveSpecs();
233
+ return spec;
234
+ }
235
+ return null;
236
+ }
237
+ export function parseSpecPopulate(text) {
238
+ var startTag = "<spec-populate>";
239
+ var endTag = "</spec-populate>";
240
+ var startIdx = text.indexOf(startTag);
241
+ var endIdx = text.indexOf(endTag);
242
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
243
+ return null;
244
+ var jsonStr = text.slice(startIdx + startTag.length, endIdx).trim();
245
+ try {
246
+ return JSON.parse(jsonStr);
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ export function parsePlanContent(text) {
253
+ var startTag = "<plan-content>";
254
+ var endTag = "</plan-content>";
255
+ var startIdx = text.indexOf(startTag);
256
+ var endIdx = text.indexOf(endTag);
257
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
258
+ return null;
259
+ return text.slice(startIdx + startTag.length, endIdx).trim();
260
+ }
261
+ export function parseSpecActivity(text) {
262
+ var startTag = "<spec-activity>";
263
+ var endTag = "</spec-activity>";
264
+ var startIdx = text.indexOf(startTag);
265
+ var endIdx = text.indexOf(endTag);
266
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
267
+ return null;
268
+ var jsonStr = text.slice(startIdx + startTag.length, endIdx).trim();
269
+ try {
270
+ return JSON.parse(jsonStr);
271
+ }
272
+ catch {
273
+ return null;
274
+ }
275
+ }
276
+ export function findSpecBySession(sessionId) {
277
+ for (var i = 0; i < specs.length; i++) {
278
+ for (var j = 0; j < specs[i].linkedSessions.length; j++) {
279
+ if (specs[i].linkedSessions[j].sessionId === sessionId)
280
+ return specs[i];
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+ export function linkSession(specId, sessionId, note, sessionType) {
201
286
  for (var i = 0; i < specs.length; i++) {
202
287
  if (specs[i].id !== specId)
203
288
  continue;
@@ -207,6 +292,7 @@ export function linkSession(specId, sessionId, note) {
207
292
  sessionId,
208
293
  linkedAt: now,
209
294
  note,
295
+ sessionType,
210
296
  });
211
297
  spec.activity.push({
212
298
  timestamp: now,
@@ -0,0 +1,173 @@
1
+ import { existsSync, readFileSync, watch } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { log } from "../logger.js";
5
+ var TRACKED_SKILLS = [
6
+ "brainstorming",
7
+ "writing-plans",
8
+ "subagent-driven-development",
9
+ "executing-plans",
10
+ ];
11
+ var installed = false;
12
+ var version = null;
13
+ var installPath = null;
14
+ var skillContent = new Map();
15
+ var watcher = null;
16
+ function getPluginsFilePath() {
17
+ return join(homedir(), ".claude", "plugins", "installed_plugins.json");
18
+ }
19
+ function detectSuperpowers() {
20
+ var pluginsFile = getPluginsFilePath();
21
+ if (!existsSync(pluginsFile)) {
22
+ installed = false;
23
+ version = null;
24
+ installPath = null;
25
+ skillContent.clear();
26
+ return;
27
+ }
28
+ try {
29
+ var raw = readFileSync(pluginsFile, "utf-8");
30
+ var data = JSON.parse(raw);
31
+ var plugins = data.plugins || {};
32
+ var found = false;
33
+ for (var key of Object.keys(plugins)) {
34
+ if (key.startsWith("superpowers@")) {
35
+ var entries = plugins[key];
36
+ if (Array.isArray(entries) && entries.length > 0) {
37
+ var entry = entries[0];
38
+ installed = true;
39
+ version = entry.version || null;
40
+ installPath = entry.installPath || null;
41
+ found = true;
42
+ break;
43
+ }
44
+ }
45
+ }
46
+ if (!found) {
47
+ installed = false;
48
+ version = null;
49
+ installPath = null;
50
+ skillContent.clear();
51
+ return;
52
+ }
53
+ loadSkills();
54
+ }
55
+ catch (err) {
56
+ log.superpowers("Failed to read plugins file: %O", err);
57
+ installed = false;
58
+ version = null;
59
+ installPath = null;
60
+ skillContent.clear();
61
+ }
62
+ }
63
+ function loadSkills() {
64
+ skillContent.clear();
65
+ if (!installPath)
66
+ return;
67
+ for (var name of TRACKED_SKILLS) {
68
+ var skillPath = join(installPath, "skills", name, "SKILL.md");
69
+ if (existsSync(skillPath)) {
70
+ try {
71
+ var content = readFileSync(skillPath, "utf-8");
72
+ skillContent.set(name, content);
73
+ }
74
+ catch (err) {
75
+ log.superpowers("Failed to read skill %s: %O", name, err);
76
+ }
77
+ }
78
+ }
79
+ log.superpowers("Superpowers v%s detected with %d skills", version, skillContent.size);
80
+ }
81
+ export function initSuperpowers() {
82
+ detectSuperpowers();
83
+ var pluginsDir = join(homedir(), ".claude", "plugins");
84
+ if (existsSync(pluginsDir)) {
85
+ try {
86
+ watcher = watch(pluginsDir, function (_eventType, filename) {
87
+ if (filename === "installed_plugins.json") {
88
+ detectSuperpowers();
89
+ }
90
+ });
91
+ }
92
+ catch (err) {
93
+ log.superpowers("Failed to watch plugins directory: %O", err);
94
+ }
95
+ }
96
+ }
97
+ export function isSuperpowersInstalled() {
98
+ return installed;
99
+ }
100
+ export function getSuperpowersVersion() {
101
+ return version;
102
+ }
103
+ export function getSkillContent(skillName) {
104
+ return skillContent.get(skillName) ?? null;
105
+ }
106
+ export function getAvailableSkills() {
107
+ return Array.from(skillContent.keys());
108
+ }
109
+ export function buildBrainstormPrompt(spec, projectSlug) {
110
+ var content = getSkillContent("brainstorming");
111
+ if (!content)
112
+ return "";
113
+ var append = content + "\n\n---\n\n";
114
+ append += "## Lattice Integration Instructions\n\n";
115
+ append += "You are helping design a spec in the Lattice project management tool.\n\n";
116
+ append += "**Project:** " + projectSlug + "\n";
117
+ append += "**Spec ID:** " + spec.id + "\n\n";
118
+ append += "When you reach the 'write design doc' phase, instead of writing a file to disk, ";
119
+ append += "output the design as a JSON block inside `<spec-populate>` tags. The JSON should contain ";
120
+ append += "any of these fields that you have determined from the conversation:\n\n";
121
+ append += "```json\n";
122
+ append += '{\n "title": "Spec title",\n "tagline": "One-line summary",\n';
123
+ append += ' "summary": "Full summary section content",\n';
124
+ append += ' "currentState": "What exists today",\n';
125
+ append += ' "requirements": "What needs to change",\n';
126
+ append += ' "implementationPlan": "High-level approach",\n';
127
+ append += ' "testing": "How to verify",\n';
128
+ append += ' "priority": "high|medium|low",\n';
129
+ append += ' "estimatedEffort": "small|medium|large|xl",\n';
130
+ append += ' "tags": ["tag1", "tag2"]\n}\n```\n\n';
131
+ append += "Lattice will automatically populate the spec fields from this output.\n";
132
+ append += "After outputting the spec-populate block, suggest that the user can now write an implementation plan from the spec editor.\n";
133
+ return append;
134
+ }
135
+ export function buildWritePlanPrompt(spec, projectSlug) {
136
+ var content = getSkillContent("writing-plans");
137
+ if (!content)
138
+ return "";
139
+ var append = content + "\n\n---\n\n";
140
+ append += "## Lattice Integration Instructions\n\n";
141
+ append += "You are writing an implementation plan for a spec in Lattice.\n\n";
142
+ append += "**Project:** " + projectSlug + "\n";
143
+ append += "**Spec:** " + spec.title + "\n\n";
144
+ append += "### Spec Content\n\n";
145
+ if (spec.sections.summary)
146
+ append += "**Summary:** " + spec.sections.summary + "\n\n";
147
+ if (spec.sections.currentState)
148
+ append += "**Current State:** " + spec.sections.currentState + "\n\n";
149
+ if (spec.sections.requirements)
150
+ append += "**Requirements:** " + spec.sections.requirements + "\n\n";
151
+ if (spec.sections.testing)
152
+ append += "**Testing:** " + spec.sections.testing + "\n\n";
153
+ append += "When you have finished writing the plan, output it inside `<plan-content>` tags.\n";
154
+ append += "Lattice will store it in the spec's implementation plan section.\n";
155
+ return append;
156
+ }
157
+ export function buildExecutePrompt(spec, projectSlug) {
158
+ var content = getSkillContent("subagent-driven-development") || getSkillContent("executing-plans");
159
+ if (!content)
160
+ return "";
161
+ var append = content + "\n\n---\n\n";
162
+ append += "## Lattice Integration Instructions\n\n";
163
+ append += "You are executing an implementation plan from a Lattice spec.\n\n";
164
+ append += "**Project:** " + projectSlug + "\n";
165
+ append += "**Spec:** " + spec.title + "\n\n";
166
+ append += "### Implementation Plan\n\n";
167
+ if (spec.sections.implementationPlan) {
168
+ append += spec.sections.implementationPlan + "\n\n";
169
+ }
170
+ append += "As you complete milestones, report progress by outputting `<spec-activity>` blocks:\n";
171
+ append += '```\n<spec-activity>{"type": "ai-note", "detail": "Completed task N: description"}</spec-activity>\n```\n';
172
+ return append;
173
+ }
@@ -8,6 +8,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { getDailySpend } from "../analytics/engine.js";
10
10
  import { log } from "../logger.js";
11
+ import { findSpecBySession } from "../features/specs.js";
11
12
  function formatSdkRule(rule) {
12
13
  if (!rule.ruleContent)
13
14
  return rule.toolName;
@@ -176,6 +177,7 @@ registerHandler("chat", function (clientId, message) {
176
177
  var attachments = sendMsg.attachmentIds
177
178
  ? getAttachments(clientId, sendMsg.attachmentIds)
178
179
  : [];
180
+ var linkedSpec = findSpecBySession(active.sessionId);
179
181
  startChatStream({
180
182
  projectSlug: active.projectSlug,
181
183
  sessionId: active.sessionId,
@@ -186,6 +188,8 @@ registerHandler("chat", function (clientId, message) {
186
188
  env: Object.keys(env).length > 0 ? env : undefined,
187
189
  model: sendMsg.model,
188
190
  effort: sendMsg.effort,
191
+ systemPrompt: sendMsg.systemPrompt,
192
+ specId: linkedSpec ? linkedSpec.id : undefined,
189
193
  });
190
194
  return;
191
195
  }
@@ -0,0 +1,171 @@
1
+ import { registerHandler } from "../ws/router.js";
2
+ import { sendTo } from "../ws/broadcast.js";
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from "node:fs";
4
+ import { join, dirname, resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { fileURLToPath } from "node:url";
7
+ import { getLatticeHome } from "../config.js";
8
+ import { log } from "../logger.js";
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const HOOKS_SRC_DIR = join(__dirname, "..", "hooks");
11
+ function getClaudeSettingsPath() {
12
+ return process.env.CLAUDE_SETTINGS_PATH || join(homedir(), ".claude", "settings.json");
13
+ }
14
+ function getHooksInstallDir() {
15
+ return resolve(join(getLatticeHome(), "hooks"));
16
+ }
17
+ function readClaudeSettings() {
18
+ const path = getClaudeSettingsPath();
19
+ if (!existsSync(path))
20
+ return {};
21
+ try {
22
+ return JSON.parse(readFileSync(path, "utf-8"));
23
+ }
24
+ catch {
25
+ return {};
26
+ }
27
+ }
28
+ function writeClaudeSettings(settings) {
29
+ const path = getClaudeSettingsPath();
30
+ const dir = dirname(path);
31
+ if (!existsSync(dir))
32
+ mkdirSync(dir, { recursive: true });
33
+ writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
34
+ }
35
+ function buildHookEntry(command) {
36
+ return {
37
+ matcher: "",
38
+ hooks: [{
39
+ type: "command",
40
+ command,
41
+ timeout: 5,
42
+ }],
43
+ };
44
+ }
45
+ function installHookScripts() {
46
+ const installDir = getHooksInstallDir();
47
+ if (!existsSync(installDir))
48
+ mkdirSync(installDir, { recursive: true });
49
+ const scripts = ["post_tool_use.sh", "event_forward.sh", "statusline.sh"];
50
+ for (const script of scripts) {
51
+ const src = join(HOOKS_SRC_DIR, script);
52
+ const dest = join(installDir, script);
53
+ if (existsSync(src)) {
54
+ copyFileSync(src, dest);
55
+ chmodSync(dest, 0o755);
56
+ }
57
+ }
58
+ return installDir;
59
+ }
60
+ export function installHooks() {
61
+ try {
62
+ const hooksDir = installHookScripts();
63
+ const settings = readClaudeSettings();
64
+ const postToolUse = join(hooksDir, "post_tool_use.sh");
65
+ const eventForward = join(hooksDir, "event_forward.sh");
66
+ const statusline = join(hooksDir, "statusline.sh");
67
+ // Merge hooks without overwriting existing non-Lattice hooks
68
+ const existingHooks = (settings.hooks || {});
69
+ function addLatticeHook(eventType, command) {
70
+ const entries = existingHooks[eventType] || [];
71
+ // Remove any existing Lattice hooks
72
+ const filtered = entries.filter(function (e) {
73
+ return !e.hooks.some(function (h) { return h.command.includes("lattice") || h.command.includes(hooksDir); });
74
+ });
75
+ filtered.push(buildHookEntry(command));
76
+ existingHooks[eventType] = filtered;
77
+ }
78
+ addLatticeHook("PostToolUse", '"' + postToolUse + '"');
79
+ addLatticeHook("SessionStart", '"' + eventForward + '" SessionStart');
80
+ addLatticeHook("Stop", '"' + eventForward + '" Stop');
81
+ addLatticeHook("PreCompact", '"' + eventForward + '" PreCompact');
82
+ addLatticeHook("PostCompact", '"' + eventForward + '" PostCompact');
83
+ settings.hooks = existingHooks;
84
+ // Add statusLine entry
85
+ settings.statusLine = {
86
+ type: "command",
87
+ command: '"' + statusline + '"',
88
+ };
89
+ writeClaudeSettings(settings);
90
+ log.server("Context analyzer hooks installed to %s", hooksDir);
91
+ return { success: true, message: "Hooks installed. Claude Code will now report context data to Lattice." };
92
+ }
93
+ catch (err) {
94
+ log.server("Failed to install hooks: %O", err);
95
+ return { success: false, message: "Failed to install hooks: " + (err.message || String(err)) };
96
+ }
97
+ }
98
+ function uninstallHooks() {
99
+ try {
100
+ const hooksDir = getHooksInstallDir();
101
+ const settings = readClaudeSettings();
102
+ const existingHooks = (settings.hooks || {});
103
+ // Remove Lattice hooks from each event type
104
+ for (const eventType of Object.keys(existingHooks)) {
105
+ existingHooks[eventType] = existingHooks[eventType].filter(function (e) {
106
+ return !e.hooks.some(function (h) { return h.command.includes("lattice") || h.command.includes(hooksDir); });
107
+ });
108
+ if (existingHooks[eventType].length === 0) {
109
+ delete existingHooks[eventType];
110
+ }
111
+ }
112
+ settings.hooks = existingHooks;
113
+ if (Object.keys(existingHooks).length === 0) {
114
+ delete settings.hooks;
115
+ }
116
+ // Remove statusLine if it points to our script
117
+ const sl = settings.statusLine;
118
+ if (sl && sl.command && (sl.command.includes("lattice") || sl.command.includes(hooksDir))) {
119
+ delete settings.statusLine;
120
+ }
121
+ writeClaudeSettings(settings);
122
+ log.server("Context analyzer hooks uninstalled");
123
+ return { success: true, message: "Hooks removed from Claude Code settings." };
124
+ }
125
+ catch (err) {
126
+ return { success: false, message: "Failed to uninstall hooks: " + (err.message || String(err)) };
127
+ }
128
+ }
129
+ export function checkHooksInstalled() {
130
+ try {
131
+ const hooksDir = getHooksInstallDir();
132
+ const settings = readClaudeSettings();
133
+ const existingHooks = (settings.hooks || {});
134
+ const postToolUse = existingHooks["PostToolUse"] || [];
135
+ return postToolUse.some(function (e) {
136
+ return e.hooks.some(function (h) { return h.command.includes("lattice") || h.command.includes(hooksDir); });
137
+ });
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ registerHandler("context", function (clientId, message) {
144
+ const msg = message;
145
+ const action = msg.action;
146
+ if (action === "install_hooks") {
147
+ const result = installHooks();
148
+ sendTo(clientId, {
149
+ type: "context:hooks_status",
150
+ installed: result.success ? true : checkHooksInstalled(),
151
+ message: result.message,
152
+ });
153
+ return;
154
+ }
155
+ if (action === "uninstall_hooks") {
156
+ const result = uninstallHooks();
157
+ sendTo(clientId, {
158
+ type: "context:hooks_status",
159
+ installed: checkHooksInstalled(),
160
+ message: result.message,
161
+ });
162
+ return;
163
+ }
164
+ if (action === "check_hooks") {
165
+ sendTo(clientId, {
166
+ type: "context:hooks_status",
167
+ installed: checkHooksInstalled(),
168
+ });
169
+ return;
170
+ }
171
+ });