@hadooppei/hwcode 1.0.9 → 1.0.10
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/.pi/dist/lib/knowledge/extractor.js +32 -0
- package/.pi/dist/lib/knowledge/review-worker.js +292 -0
- package/.pi/dist/lib/knowledge/sanitize.js +24 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +165 -0
- package/.pi/dist/lib/knowledge/store.js +370 -0
- package/.pi/dist/lib/knowledge/types.js +1 -0
- package/.pi/dist/lib/knowledge/worker-protocol.js +1 -0
- package/.pi/dist/lib/runtime/defaults.js +50 -0
- package/.pi/dist/lib/runtime/paths.js +55 -0
- package/.pi/extensions/knowledge.ts +56 -5
- package/.pi/lib/knowledge/review-worker.ts +2 -0
- package/.pi/lib/knowledge/worker-protocol.ts +1 -0
- package/package.json +5 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
3
|
+
export const KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT = `You are HWCode's background knowledge reviewer.
|
|
4
|
+
Review only the supplied conversation delta. Return strict JSON and no markdown.
|
|
5
|
+
Persist only knowledge that is likely to be useful in future sessions:
|
|
6
|
+
- explicit user corrections or stable preferences;
|
|
7
|
+
- verified engineering rules, successful procedures, or expensive failed approaches;
|
|
8
|
+
- reusable architecture, testing, debugging, deployment, or operational knowledge.
|
|
9
|
+
Do not persist task summaries, guesses, temporary IDs, credentials, secrets, private data, or facts recoverable by simply reading the repository.
|
|
10
|
+
Use storageHint "rule" only for short, precise, high-value instructions. Use "topic" for multi-step SOPs and detailed experience.
|
|
11
|
+
Use action "revise" only when the delta explicitly corrects prior knowledge; otherwise use "add" or "reinforce".
|
|
12
|
+
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
13
|
+
Return: {"candidates":[{"key":"stable semantic key","title":"...","summary":"...","keywords":["..."],"scope":"global|project","body":"markdown body","evidence":["verified evidence"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|revise","explicitUserDirective":false}]}
|
|
14
|
+
Return {"candidates":[]} when nothing meets the threshold.`;
|
|
15
|
+
export function buildKnowledgeExtractionPrompt(projectRoot, delta) {
|
|
16
|
+
return `Project root: ${projectRoot}\n\n<conversation_delta>\n${delta}\n</conversation_delta>`;
|
|
17
|
+
}
|
|
18
|
+
export function parseCandidateEnvelope(text) {
|
|
19
|
+
const trimmed = text.trim();
|
|
20
|
+
const unfenced = trimmed.replace(/^```(?:json)?\s*/iu, "").replace(/\s*```$/u, "");
|
|
21
|
+
const start = unfenced.indexOf("{");
|
|
22
|
+
const end = unfenced.lastIndexOf("}");
|
|
23
|
+
if (start < 0 || end <= start)
|
|
24
|
+
throw new Error("Knowledge reviewer did not return a JSON object");
|
|
25
|
+
const parsed = JSON.parse(unfenced.slice(start, end + 1));
|
|
26
|
+
if (!Array.isArray(parsed.candidates))
|
|
27
|
+
throw new Error("Knowledge reviewer response is missing candidates[]");
|
|
28
|
+
return parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates);
|
|
29
|
+
}
|
|
30
|
+
export function knowledgeDeltaDigest(delta) {
|
|
31
|
+
return createHash("sha256").update(delta).digest("hex");
|
|
32
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createConnection, createServer } from "node:net";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
6
|
+
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
7
|
+
import { userRuntimePaths } from "../runtime/paths.js";
|
|
8
|
+
import { parseCandidateEnvelope } from "./extractor.js";
|
|
9
|
+
import { findNextReviewTask } from "./session-scanner.js";
|
|
10
|
+
import { commitKnowledgeReview, ensureKnowledgeDirectories, loadCurrentManifest } from "./store.js";
|
|
11
|
+
if (!parentPort)
|
|
12
|
+
throw new Error("Knowledge review worker requires a parent port");
|
|
13
|
+
const home = workerData?.home ?? homedir();
|
|
14
|
+
const paths = userRuntimePaths(home);
|
|
15
|
+
const standbySockets = new Set();
|
|
16
|
+
let eligible = false;
|
|
17
|
+
let stopped = false;
|
|
18
|
+
let sessionsRoot = "";
|
|
19
|
+
let leaderServer;
|
|
20
|
+
let leaderToken = "";
|
|
21
|
+
let standbySocket;
|
|
22
|
+
let electionPending = false;
|
|
23
|
+
let activeTask;
|
|
24
|
+
function send(message) {
|
|
25
|
+
parentPort.postMessage(message);
|
|
26
|
+
}
|
|
27
|
+
function coordinatorPath() {
|
|
28
|
+
return process.platform === "win32"
|
|
29
|
+
? `\\\\.\\pipe\\hwcode-knowledge-v3-${Buffer.from(home).toString("hex").slice(0, 16)}`
|
|
30
|
+
: paths.knowledgeCoordinatorSocket;
|
|
31
|
+
}
|
|
32
|
+
function writeLeaderMetadata() {
|
|
33
|
+
writeFileSync(paths.knowledgeLeader, `${JSON.stringify({
|
|
34
|
+
protocolVersion: 3, leaderToken, pid: process.pid, electedAt: new Date().toISOString(),
|
|
35
|
+
}, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
36
|
+
chmodSync(paths.knowledgeLeader, 0o600);
|
|
37
|
+
}
|
|
38
|
+
function removeLeaderMetadata(token) {
|
|
39
|
+
try {
|
|
40
|
+
const current = JSON.parse(readFileSync(paths.knowledgeLeader, "utf8"));
|
|
41
|
+
if (current.leaderToken === token)
|
|
42
|
+
rmSync(paths.knowledgeLeader, { force: true });
|
|
43
|
+
}
|
|
44
|
+
catch { /* Another contender may already have replaced it. */ }
|
|
45
|
+
}
|
|
46
|
+
function broadcast(message) {
|
|
47
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
48
|
+
for (const socket of standbySockets) {
|
|
49
|
+
if (socket.writable)
|
|
50
|
+
socket.write(line);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function scheduleElection(delay = Math.floor(Math.random() * 500)) {
|
|
54
|
+
if (!eligible || stopped || electionPending || leaderServer || standbySocket)
|
|
55
|
+
return;
|
|
56
|
+
electionPending = true;
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
electionPending = false;
|
|
59
|
+
attemptElection();
|
|
60
|
+
}, delay);
|
|
61
|
+
timer.unref();
|
|
62
|
+
}
|
|
63
|
+
function stepDown(reason) {
|
|
64
|
+
if (activeTask)
|
|
65
|
+
send({ type: "review_cancel", requestId: activeTask.requestId, reason });
|
|
66
|
+
activeTask = undefined;
|
|
67
|
+
const token = leaderToken;
|
|
68
|
+
leaderToken = "";
|
|
69
|
+
for (const socket of standbySockets)
|
|
70
|
+
socket.destroy();
|
|
71
|
+
standbySockets.clear();
|
|
72
|
+
const server = leaderServer;
|
|
73
|
+
leaderServer = undefined;
|
|
74
|
+
if (server) {
|
|
75
|
+
try {
|
|
76
|
+
server.close();
|
|
77
|
+
}
|
|
78
|
+
catch { /* The failing server may already be closed. */ }
|
|
79
|
+
if (process.platform !== "win32") {
|
|
80
|
+
try {
|
|
81
|
+
unlinkSync(coordinatorPath());
|
|
82
|
+
}
|
|
83
|
+
catch { /* Socket may already be gone. */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (token)
|
|
87
|
+
removeLeaderMetadata(token);
|
|
88
|
+
}
|
|
89
|
+
function becomeStandby(socket) {
|
|
90
|
+
standbySocket = socket;
|
|
91
|
+
socket.unref();
|
|
92
|
+
send({ type: "leadership", state: "standby" });
|
|
93
|
+
let buffer = "";
|
|
94
|
+
socket.on("data", (chunk) => {
|
|
95
|
+
buffer += chunk.toString("utf8");
|
|
96
|
+
for (;;) {
|
|
97
|
+
const newline = buffer.indexOf("\n");
|
|
98
|
+
if (newline < 0)
|
|
99
|
+
break;
|
|
100
|
+
const line = buffer.slice(0, newline);
|
|
101
|
+
buffer = buffer.slice(newline + 1);
|
|
102
|
+
try {
|
|
103
|
+
const message = JSON.parse(line);
|
|
104
|
+
if (message.type === "generation_changed")
|
|
105
|
+
send(message);
|
|
106
|
+
}
|
|
107
|
+
catch { /* Ignore malformed coordinator notifications. */ }
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
socket.once("close", () => {
|
|
111
|
+
if (standbySocket === socket)
|
|
112
|
+
standbySocket = undefined;
|
|
113
|
+
scheduleElection();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function cleanStaleSocketAndRetry() {
|
|
117
|
+
if (process.platform === "win32") {
|
|
118
|
+
scheduleElection(250);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(paths.knowledgeElectionLock, { mode: 0o700 });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
scheduleElection(250);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
if (existsSync(coordinatorPath()))
|
|
130
|
+
unlinkSync(coordinatorPath());
|
|
131
|
+
}
|
|
132
|
+
catch { /* Retrying listen will surface persistent failures. */ }
|
|
133
|
+
finally {
|
|
134
|
+
rmSync(paths.knowledgeElectionLock, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
scheduleElection();
|
|
137
|
+
}
|
|
138
|
+
function connectToLeader(probe = 0) {
|
|
139
|
+
if (!eligible || stopped || leaderServer || standbySocket)
|
|
140
|
+
return;
|
|
141
|
+
const socket = createConnection(coordinatorPath());
|
|
142
|
+
let connected = false;
|
|
143
|
+
socket.once("connect", () => { connected = true; becomeStandby(socket); });
|
|
144
|
+
socket.once("error", (error) => {
|
|
145
|
+
socket.destroy();
|
|
146
|
+
if (connected)
|
|
147
|
+
return;
|
|
148
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOENT") {
|
|
149
|
+
if (probe < 2) {
|
|
150
|
+
const timer = setTimeout(() => connectToLeader(probe + 1), 100);
|
|
151
|
+
timer.unref();
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
cleanStaleSocketAndRetry();
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
send({ type: "review_failed", error: `Coordinator connection failed: ${error.message}` });
|
|
158
|
+
scheduleElection(1_000);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function becomeLeader(server) {
|
|
163
|
+
leaderServer = server;
|
|
164
|
+
leaderToken = randomUUID();
|
|
165
|
+
server.unref();
|
|
166
|
+
server.on("connection", (socket) => {
|
|
167
|
+
standbySockets.add(socket);
|
|
168
|
+
socket.unref();
|
|
169
|
+
socket.once("close", () => standbySockets.delete(socket));
|
|
170
|
+
const generationId = loadCurrentManifest(home).generationId;
|
|
171
|
+
if (generationId)
|
|
172
|
+
socket.write(`${JSON.stringify({ type: "generation_changed", generationId })}\n`);
|
|
173
|
+
});
|
|
174
|
+
server.on("error", (error) => {
|
|
175
|
+
send({ type: "review_failed", error: `Coordinator server failed: ${error.message}` });
|
|
176
|
+
stepDown("coordinator-server-failed");
|
|
177
|
+
scheduleElection();
|
|
178
|
+
});
|
|
179
|
+
writeLeaderMetadata();
|
|
180
|
+
send({ type: "leadership", state: "leader", leaderToken });
|
|
181
|
+
void scanForReview();
|
|
182
|
+
}
|
|
183
|
+
function attemptElection() {
|
|
184
|
+
if (!eligible || stopped || leaderServer || standbySocket)
|
|
185
|
+
return;
|
|
186
|
+
ensureKnowledgeDirectories(home);
|
|
187
|
+
const server = createServer();
|
|
188
|
+
const electionError = (error) => {
|
|
189
|
+
if (error.code === "EADDRINUSE")
|
|
190
|
+
connectToLeader();
|
|
191
|
+
else {
|
|
192
|
+
send({ type: "review_failed", error: `Coordinator election failed: ${error.message}` });
|
|
193
|
+
scheduleElection(1_000);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
server.once("error", electionError);
|
|
197
|
+
server.listen(coordinatorPath(), () => {
|
|
198
|
+
server.off("error", electionError);
|
|
199
|
+
if (process.platform !== "win32")
|
|
200
|
+
chmodSync(coordinatorPath(), 0o600);
|
|
201
|
+
becomeLeader(server);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async function scanForReview() {
|
|
205
|
+
if (!leaderServer || !leaderToken || activeTask || !sessionsRoot || !eligible)
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
const manifest = loadCurrentManifest(home);
|
|
209
|
+
const task = findNextReviewTask(sessionsRoot, manifest.reviews);
|
|
210
|
+
if (!task)
|
|
211
|
+
return;
|
|
212
|
+
activeTask = task;
|
|
213
|
+
if (!task.delta) {
|
|
214
|
+
const result = commitKnowledgeReview([], task, leaderToken, home);
|
|
215
|
+
const generationId = loadCurrentManifest(home).generationId;
|
|
216
|
+
send({ type: "review_saved", requestId: task.requestId, generationId, result });
|
|
217
|
+
if (generationId) {
|
|
218
|
+
send({ type: "generation_changed", generationId });
|
|
219
|
+
broadcast({ type: "generation_changed", generationId });
|
|
220
|
+
}
|
|
221
|
+
activeTask = undefined;
|
|
222
|
+
queueMicrotask(() => void scanForReview());
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
send({ type: "review_request", leaderToken, requestId: task.requestId, task });
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
activeTask = undefined;
|
|
229
|
+
send({ type: "review_failed", error: error instanceof Error ? error.message : String(error) });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function handleReviewResult(message) {
|
|
233
|
+
const task = activeTask;
|
|
234
|
+
if (!task || message.requestId !== task.requestId || message.leaderToken !== leaderToken || !leaderServer)
|
|
235
|
+
return;
|
|
236
|
+
let committed = false;
|
|
237
|
+
try {
|
|
238
|
+
if (message.error || message.raw === undefined)
|
|
239
|
+
throw new Error(message.error || "Knowledge review returned no content");
|
|
240
|
+
const result = commitKnowledgeReview(parseCandidateEnvelope(message.raw), task, leaderToken, home);
|
|
241
|
+
const generationId = loadCurrentManifest(home).generationId;
|
|
242
|
+
send({ type: "review_saved", requestId: task.requestId, generationId, result });
|
|
243
|
+
if (generationId) {
|
|
244
|
+
send({ type: "generation_changed", generationId });
|
|
245
|
+
broadcast({ type: "generation_changed", generationId });
|
|
246
|
+
}
|
|
247
|
+
committed = true;
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
send({ type: "review_failed", requestId: task.requestId, error: error instanceof Error ? error.message : String(error) });
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
activeTask = undefined;
|
|
254
|
+
if (committed)
|
|
255
|
+
queueMicrotask(() => void scanForReview());
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const scanTimer = setInterval(() => { void scanForReview(); }, KNOWLEDGE_RUNTIME_DEFAULTS.review.intervalMs);
|
|
259
|
+
scanTimer.unref();
|
|
260
|
+
parentPort.on("message", (message) => {
|
|
261
|
+
if (message.type === "configure") {
|
|
262
|
+
sessionsRoot = message.sessionsRoot;
|
|
263
|
+
if (eligible === message.modelAvailable)
|
|
264
|
+
return;
|
|
265
|
+
eligible = message.modelAvailable;
|
|
266
|
+
if (!eligible) {
|
|
267
|
+
standbySocket?.destroy();
|
|
268
|
+
standbySocket = undefined;
|
|
269
|
+
stepDown("model-unavailable");
|
|
270
|
+
send({ type: "leadership", state: "ineligible" });
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
scheduleElection(0);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (message.type === "review_result") {
|
|
277
|
+
handleReviewResult(message);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (message.type === "scan_now") {
|
|
281
|
+
void scanForReview();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
stopped = true;
|
|
285
|
+
eligible = false;
|
|
286
|
+
clearInterval(scanTimer);
|
|
287
|
+
standbySocket?.destroy();
|
|
288
|
+
standbySocket = undefined;
|
|
289
|
+
stepDown("worker-stopped");
|
|
290
|
+
process.exit(0);
|
|
291
|
+
});
|
|
292
|
+
send({ type: "ready" });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const PRIVATE_KEY_RE = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/giu;
|
|
2
|
+
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/giu;
|
|
3
|
+
const JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gu;
|
|
4
|
+
const SECRET_ASSIGNMENT_RE = /\b(access[_-]?key|secret(?:[_-]?(?:access|key))?|password|passwd|token|credential|client[_-]?secret)\b(\s*[:=]\s*)[^\s,;]+/giu;
|
|
5
|
+
const URL_CREDENTIAL_RE = /(https?:\/\/)[^\s/@:]+:[^\s/@]+@/giu;
|
|
6
|
+
export function sanitizeKnowledgeText(value) {
|
|
7
|
+
return value
|
|
8
|
+
.replace(PRIVATE_KEY_RE, "<redacted-private-key>")
|
|
9
|
+
.replace(BEARER_RE, "Bearer <redacted>")
|
|
10
|
+
.replace(JWT_RE, "<redacted-jwt>")
|
|
11
|
+
.replace(SECRET_ASSIGNMENT_RE, (_match, key, separator) => `${key}${separator}<redacted>`)
|
|
12
|
+
.replace(URL_CREDENTIAL_RE, "$1<redacted>@")
|
|
13
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/giu, "<runtime-uuid>")
|
|
14
|
+
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/gu, "<runtime-ip>")
|
|
15
|
+
.replace(/\b(vpc|subnet|secgroup|sg|vol|snap|img|inst|eip)-[0-9a-zA-Z]+/gu, "<$1-runtime-id>")
|
|
16
|
+
.replace(/\/(?:Users|home)\/[a-zA-Z0-9_-]+/gu, "~");
|
|
17
|
+
}
|
|
18
|
+
export function compactKnowledgeText(value, maxChars) {
|
|
19
|
+
return sanitizeKnowledgeText(value)
|
|
20
|
+
.replace(/\r\n/gu, "\n")
|
|
21
|
+
.replace(/[ \t]+$/gmu, "")
|
|
22
|
+
.trim()
|
|
23
|
+
.slice(0, maxChars);
|
|
24
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { buildContextEntries, parseSessionEntries, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
6
|
+
import { knowledgeDeltaDigest } from "./extractor.js";
|
|
7
|
+
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
8
|
+
import { projectKnowledgeKey } from "./store.js";
|
|
9
|
+
function hash(value, length = 64) {
|
|
10
|
+
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
11
|
+
}
|
|
12
|
+
function textContent(content) {
|
|
13
|
+
if (typeof content === "string")
|
|
14
|
+
return content;
|
|
15
|
+
if (!Array.isArray(content))
|
|
16
|
+
return "";
|
|
17
|
+
return content.map((item) => {
|
|
18
|
+
if (!item || typeof item !== "object")
|
|
19
|
+
return "";
|
|
20
|
+
const value = item;
|
|
21
|
+
return typeof value.text === "string" ? value.text : "";
|
|
22
|
+
}).filter(Boolean).join("\n");
|
|
23
|
+
}
|
|
24
|
+
function reviewPiece(entry, index) {
|
|
25
|
+
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
26
|
+
return { index, priority: 1, category: "summary", text: `[summary]\n${sanitizeKnowledgeText(entry.summary).slice(0, 6_000)}` };
|
|
27
|
+
}
|
|
28
|
+
if (entry.type === "custom" && entry.customType.startsWith("hwcode-workflow")) {
|
|
29
|
+
return {
|
|
30
|
+
index, priority: 0, category: "workflow",
|
|
31
|
+
text: `[workflow_state:${entry.customType}]\n${sanitizeKnowledgeText(JSON.stringify(entry.data ?? {})).slice(0, 6_000)}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (entry.type !== "message" || !entry.message || typeof entry.message !== "object")
|
|
35
|
+
return undefined;
|
|
36
|
+
const message = entry.message;
|
|
37
|
+
const role = typeof message.role === "string" ? message.role : "message";
|
|
38
|
+
const content = sanitizeKnowledgeText(textContent(message.content));
|
|
39
|
+
if (!content)
|
|
40
|
+
return undefined;
|
|
41
|
+
const priority = role === "user" ? 0 : role === "toolResult" ? 3 : 2;
|
|
42
|
+
const cap = role === "user" ? 8_000 : role === "toolResult" ? 1_500 : 4_000;
|
|
43
|
+
const category = role === "user" ? "user" : role === "toolResult" ? "tool" : "assistant";
|
|
44
|
+
return { index, priority, category, text: `[${role}]\n${content.slice(0, cap)}` };
|
|
45
|
+
}
|
|
46
|
+
function buildDelta(entries) {
|
|
47
|
+
const pieces = entries.map(reviewPiece).filter((piece) => Boolean(piece));
|
|
48
|
+
const selected = [];
|
|
49
|
+
const categoryRemaining = {
|
|
50
|
+
user: 14_000, workflow: 6_000, summary: 4_000, assistant: 8_000, tool: 4_000,
|
|
51
|
+
};
|
|
52
|
+
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars;
|
|
53
|
+
for (const piece of pieces.slice().sort((left, right) => left.priority - right.priority || left.index - right.index)) {
|
|
54
|
+
if (remaining <= 0)
|
|
55
|
+
break;
|
|
56
|
+
const text = piece.text.slice(0, Math.min(remaining, categoryRemaining[piece.category]));
|
|
57
|
+
if (text)
|
|
58
|
+
selected.push({ ...piece, text });
|
|
59
|
+
remaining -= text.length;
|
|
60
|
+
categoryRemaining[piece.category] -= text.length;
|
|
61
|
+
}
|
|
62
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n");
|
|
63
|
+
}
|
|
64
|
+
function projectRootForSession(header, branch) {
|
|
65
|
+
for (const entry of branch.slice().reverse()) {
|
|
66
|
+
if (entry.type !== "custom" || entry.customType !== "hwcode-working-directory"
|
|
67
|
+
|| !entry.data || typeof entry.data !== "object")
|
|
68
|
+
continue;
|
|
69
|
+
const cwd = entry.data.cwd;
|
|
70
|
+
if (typeof cwd === "string" && cwd)
|
|
71
|
+
return resolve(cwd);
|
|
72
|
+
}
|
|
73
|
+
return resolve(header.cwd);
|
|
74
|
+
}
|
|
75
|
+
export function discoverSessionFiles(root) {
|
|
76
|
+
const files = [];
|
|
77
|
+
const visit = (directory) => {
|
|
78
|
+
let entries;
|
|
79
|
+
try {
|
|
80
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const path = resolve(directory, entry.name);
|
|
87
|
+
if (entry.isDirectory())
|
|
88
|
+
visit(path);
|
|
89
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
90
|
+
files.push(path);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
visit(resolve(root));
|
|
94
|
+
return files.sort();
|
|
95
|
+
}
|
|
96
|
+
export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
97
|
+
let stat;
|
|
98
|
+
try {
|
|
99
|
+
stat = statSync(sessionFile);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
if (!stat.isFile() || now - stat.mtimeMs < KNOWLEDGE_RUNTIME_DEFAULTS.review.idleMs)
|
|
105
|
+
return undefined;
|
|
106
|
+
let content;
|
|
107
|
+
try {
|
|
108
|
+
content = readFileSync(sessionFile, "utf8");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const lastNewline = content.lastIndexOf("\n");
|
|
114
|
+
if (lastNewline < 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
const parsed = parseSessionEntries(content.slice(0, lastNewline + 1));
|
|
117
|
+
const header = parsed.find((entry) => entry.type === "session");
|
|
118
|
+
if (!header?.id || !header.cwd)
|
|
119
|
+
return undefined;
|
|
120
|
+
const allEntries = parsed.filter((entry) => entry.type !== "session");
|
|
121
|
+
const branch = buildContextEntries(allEntries);
|
|
122
|
+
if (branch.length === 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
const cursorIndex = cursor?.lastReviewedEntryId
|
|
125
|
+
? branch.findIndex((entry) => entry.id === cursor.lastReviewedEntryId)
|
|
126
|
+
: -1;
|
|
127
|
+
const pending = branch.slice(cursorIndex + 1);
|
|
128
|
+
if (pending.length === 0)
|
|
129
|
+
return undefined;
|
|
130
|
+
const delta = buildDelta(pending);
|
|
131
|
+
const firstEntryId = pending[0].id;
|
|
132
|
+
const lastEntryId = pending[pending.length - 1].id;
|
|
133
|
+
const deltaDigest = knowledgeDeltaDigest(delta || `${firstEntryId}\0${lastEntryId}`);
|
|
134
|
+
const sessionFileHash = hash(resolve(sessionFile), 16);
|
|
135
|
+
const sessionKey = hash(header.id, 24);
|
|
136
|
+
const reviewKey = hash(`${header.id}\0${firstEntryId}\0${lastEntryId}\0${deltaDigest}`);
|
|
137
|
+
const projectRoot = projectRootForSession(header, branch);
|
|
138
|
+
return {
|
|
139
|
+
requestId: randomUUID(), reviewKey, sessionId: header.id, sessionKey, sessionFileHash,
|
|
140
|
+
projectRoot, projectKey: projectKnowledgeKey(projectRoot),
|
|
141
|
+
firstEntryId, lastEntryId, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
145
|
+
const files = discoverSessionFiles(sessionsRoot).map((path) => {
|
|
146
|
+
try {
|
|
147
|
+
return { path, mtimeMs: statSync(path).mtimeMs };
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}).filter((item) => Boolean(item))
|
|
153
|
+
.sort((left, right) => left.mtimeMs - right.mtimeMs);
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
const fileHash = hash(resolve(file.path), 16);
|
|
156
|
+
const cursor = Object.values(reviews).find((item) => item.sessionFileHash === fileHash);
|
|
157
|
+
let task = readReviewTask(file.path, cursor, now);
|
|
158
|
+
const stableCursor = task ? reviews[task.sessionKey] : undefined;
|
|
159
|
+
if (task && stableCursor && stableCursor !== cursor)
|
|
160
|
+
task = readReviewTask(file.path, stableCursor, now);
|
|
161
|
+
if (task)
|
|
162
|
+
return task;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
6
|
+
import { userRuntimePaths } from "../runtime/paths.js";
|
|
7
|
+
import { compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.js";
|
|
8
|
+
const STORAGE = KNOWLEDGE_RUNTIME_DEFAULTS.storage;
|
|
9
|
+
const SAFE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
|
|
10
|
+
const SAFE_GENERATION_RE = /^[0-9TZ-]+-[a-f0-9]{12}$/u;
|
|
11
|
+
const PROJECT_SCOPE_RE = /^project:[a-f0-9]{16}$/u;
|
|
12
|
+
export class KnowledgeCommitBusyError extends Error {
|
|
13
|
+
}
|
|
14
|
+
function emptyCatalog() {
|
|
15
|
+
return { version: 3, updatedAt: new Date().toISOString(), items: [] };
|
|
16
|
+
}
|
|
17
|
+
function emptyManifest() {
|
|
18
|
+
return {
|
|
19
|
+
version: 3, generationId: "", createdAt: new Date().toISOString(), leaderToken: "",
|
|
20
|
+
catalog: emptyCatalog(), reviews: {},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function ensureDirectory(path) {
|
|
24
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
25
|
+
chmodSync(path, 0o700);
|
|
26
|
+
}
|
|
27
|
+
function atomicWrite(path, content) {
|
|
28
|
+
ensureDirectory(dirname(path));
|
|
29
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
30
|
+
writeFileSync(temporary, content, { encoding: "utf8", mode: 0o600 });
|
|
31
|
+
renameSync(temporary, path);
|
|
32
|
+
chmodSync(path, 0o600);
|
|
33
|
+
}
|
|
34
|
+
export function ensureKnowledgeDirectories(home = homedir()) {
|
|
35
|
+
const paths = userRuntimePaths(home);
|
|
36
|
+
for (const path of [paths.knowledge, paths.knowledgeGenerations, paths.knowledgeRuntime])
|
|
37
|
+
ensureDirectory(path);
|
|
38
|
+
}
|
|
39
|
+
export function projectKnowledgeKey(projectRoot) {
|
|
40
|
+
return createHash("sha256").update(resolve(projectRoot)).digest("hex").slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
function generationDirectory(generationId, home) {
|
|
43
|
+
if (!SAFE_GENERATION_RE.test(generationId))
|
|
44
|
+
return undefined;
|
|
45
|
+
const root = resolve(userRuntimePaths(home).knowledgeGenerations);
|
|
46
|
+
const directory = resolve(root, generationId);
|
|
47
|
+
return directory.startsWith(`${root}${sep}`) ? directory : undefined;
|
|
48
|
+
}
|
|
49
|
+
function parseManifest(path) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
52
|
+
if (parsed.version !== 3 || !SAFE_GENERATION_RE.test(parsed.generationId)
|
|
53
|
+
|| parsed.catalog?.version !== 3 || !Array.isArray(parsed.catalog.items)
|
|
54
|
+
|| !parsed.reviews || typeof parsed.reviews !== "object")
|
|
55
|
+
return undefined;
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function validManifestAt(manifest, directory) {
|
|
63
|
+
return manifest.catalog.items.every((entry) => {
|
|
64
|
+
if (!SAFE_ID_RE.test(entry.id) || basename(entry.file) !== `${entry.id}.md`)
|
|
65
|
+
return false;
|
|
66
|
+
if (entry.scope !== "global" && !PROJECT_SCOPE_RE.test(entry.scope))
|
|
67
|
+
return false;
|
|
68
|
+
const path = resolve(directory, entry.file);
|
|
69
|
+
return path.startsWith(`${directory}${sep}`) && existsSync(path);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function validManifest(manifest, home) {
|
|
73
|
+
const directory = generationDirectory(manifest.generationId, home);
|
|
74
|
+
return Boolean(directory && validManifestAt(manifest, directory));
|
|
75
|
+
}
|
|
76
|
+
export function loadCurrentManifest(home = homedir()) {
|
|
77
|
+
ensureKnowledgeDirectories(home);
|
|
78
|
+
const paths = userRuntimePaths(home);
|
|
79
|
+
const preferred = existsSync(paths.knowledgeCurrent) ? readFileSync(paths.knowledgeCurrent, "utf8").trim() : "";
|
|
80
|
+
const candidates = [preferred, ...readdirSync(paths.knowledgeGenerations, { withFileTypes: true })
|
|
81
|
+
.filter((entry) => entry.isDirectory() && SAFE_GENERATION_RE.test(entry.name))
|
|
82
|
+
.map((entry) => entry.name).sort().reverse()]
|
|
83
|
+
.filter((value, index, values) => value && values.indexOf(value) === index);
|
|
84
|
+
for (const generationId of candidates) {
|
|
85
|
+
const directory = generationDirectory(generationId, home);
|
|
86
|
+
if (!directory)
|
|
87
|
+
continue;
|
|
88
|
+
const manifest = parseManifest(join(directory, "manifest.json"));
|
|
89
|
+
if (manifest && manifest.generationId === generationId && validManifest(manifest, home))
|
|
90
|
+
return manifest;
|
|
91
|
+
}
|
|
92
|
+
return emptyManifest();
|
|
93
|
+
}
|
|
94
|
+
export function loadKnowledgeCatalog(home = homedir()) {
|
|
95
|
+
return loadCurrentManifest(home).catalog;
|
|
96
|
+
}
|
|
97
|
+
function normalizeSlug(value) {
|
|
98
|
+
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/gu, "-")
|
|
99
|
+
.replace(/^-+|-+$/gu, "").slice(0, STORAGE.slugMaxChars) || "knowledge";
|
|
100
|
+
}
|
|
101
|
+
function normalizedFingerprint(candidate) {
|
|
102
|
+
return createHash("sha256").update(`${candidate.scope}\0${candidate.key.trim().toLowerCase()}`).digest("hex");
|
|
103
|
+
}
|
|
104
|
+
function contentHash(candidate) {
|
|
105
|
+
return createHash("sha256").update(`${candidate.title}\0${candidate.summary}\0${candidate.body}`).digest("hex");
|
|
106
|
+
}
|
|
107
|
+
function asStringArray(value, maxItems) {
|
|
108
|
+
if (!Array.isArray(value))
|
|
109
|
+
return [];
|
|
110
|
+
return [...new Set(value.filter((item) => typeof item === "string")
|
|
111
|
+
.map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
|
|
112
|
+
}
|
|
113
|
+
function normalizeCandidate(value, projectKey) {
|
|
114
|
+
if (!value || typeof value !== "object")
|
|
115
|
+
return undefined;
|
|
116
|
+
const raw = value;
|
|
117
|
+
if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
|
|
118
|
+
|| typeof raw.body !== "string" || typeof raw.confidence !== "number")
|
|
119
|
+
return undefined;
|
|
120
|
+
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence)
|
|
121
|
+
return undefined;
|
|
122
|
+
const explicitUserDirective = raw.explicitUserDirective === true;
|
|
123
|
+
const scope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${projectKey}`;
|
|
124
|
+
const body = compactKnowledgeText(raw.body, 20_000);
|
|
125
|
+
const evidence = asStringArray(raw.evidence, 8);
|
|
126
|
+
if (!body || evidence.length === 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
const requestedTrack = raw.storageHint === "rule" ? "rule" : "topic";
|
|
129
|
+
const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
|
|
130
|
+
&& (explicitUserDirective || raw.confidence >= 0.9);
|
|
131
|
+
return {
|
|
132
|
+
key: compactKnowledgeText(raw.key, 160), title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
133
|
+
summary: compactKnowledgeText(raw.summary, STORAGE.maxSummaryChars),
|
|
134
|
+
keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
|
|
135
|
+
scope, body, evidence, confidence: Math.min(1, raw.confidence),
|
|
136
|
+
storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
|
|
137
|
+
action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
|
|
138
|
+
explicitUserDirective,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function renderKnowledgeFile(candidate) {
|
|
142
|
+
if (candidate.storageHint === "rule")
|
|
143
|
+
return `# ${candidate.title}\n\n${candidate.body}\n`;
|
|
144
|
+
return [
|
|
145
|
+
`# ${candidate.title}`, "", candidate.summary, "", `Keywords: ${candidate.keywords.join(", ")}`, "",
|
|
146
|
+
candidate.body, "", "## Evidence", "", ...candidate.evidence.map((item) => `- ${item}`), "",
|
|
147
|
+
].join("\n");
|
|
148
|
+
}
|
|
149
|
+
function applicable(entry, projectKey) {
|
|
150
|
+
return entry.scope === "global" || entry.scope === `project:${projectKey}`;
|
|
151
|
+
}
|
|
152
|
+
function generateMemory(catalog, projectKey) {
|
|
153
|
+
const lines = [
|
|
154
|
+
"# HWCode Knowledge Index", "",
|
|
155
|
+
"Detailed topics are loaded only through hwcode_knowledge_lookup. Search by ID or keywords.", "",
|
|
156
|
+
];
|
|
157
|
+
const topics = catalog.items.filter((item) => item.track === "topic" && (!projectKey || applicable(item, projectKey)))
|
|
158
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
159
|
+
for (const item of topics) {
|
|
160
|
+
const scope = item.scope === "global" ? "global" : "project";
|
|
161
|
+
const line = `- [${item.id}] (${scope}; ${item.keywords.join(", ")}) ${item.title}: ${item.summary}`;
|
|
162
|
+
if (lines.length + 1 >= STORAGE.maxMemoryLines || [...lines, line].join("\n").length > STORAGE.maxMemoryChars) {
|
|
163
|
+
lines.push("- Additional topics remain searchable through hwcode_knowledge_lookup(query: \"keywords\").");
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
lines.push(line);
|
|
167
|
+
}
|
|
168
|
+
return `${lines.join("\n")}\n`;
|
|
169
|
+
}
|
|
170
|
+
function processExists(pid) {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(pid, 0);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return error.code === "EPERM";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function acquireCommitLock(leaderToken, home) {
|
|
180
|
+
const paths = userRuntimePaths(home);
|
|
181
|
+
ensureDirectory(paths.knowledgeRuntime);
|
|
182
|
+
const attempt = () => {
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync(paths.knowledgeCommitLock, { mode: 0o700 });
|
|
185
|
+
writeFileSync(join(paths.knowledgeCommitLock, "owner.json"), JSON.stringify({ pid: process.pid, leaderToken }), { mode: 0o600 });
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (error.code !== "EEXIST")
|
|
190
|
+
throw error;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
if (!attempt()) {
|
|
195
|
+
try {
|
|
196
|
+
const owner = JSON.parse(readFileSync(join(paths.knowledgeCommitLock, "owner.json"), "utf8"));
|
|
197
|
+
if (owner.leaderToken !== leaderToken || (typeof owner.pid === "number" && !processExists(owner.pid))) {
|
|
198
|
+
rmSync(paths.knowledgeCommitLock, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
const age = Date.now() - statSync(paths.knowledgeCommitLock).mtimeMs;
|
|
203
|
+
if (age > KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs)
|
|
204
|
+
rmSync(paths.knowledgeCommitLock, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
if (!attempt())
|
|
207
|
+
throw new KnowledgeCommitBusyError("Knowledge repository is busy");
|
|
208
|
+
}
|
|
209
|
+
return () => rmSync(paths.knowledgeCommitLock, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
function newGenerationId() {
|
|
212
|
+
return `${new Date().toISOString().replace(/[:.]/gu, "-")}-${randomUUID().replace(/-/gu, "").slice(0, 12)}`;
|
|
213
|
+
}
|
|
214
|
+
function copyCurrentContent(manifest, target, home) {
|
|
215
|
+
for (const name of ["rules", "topics", "pending"])
|
|
216
|
+
ensureDirectory(join(target, name));
|
|
217
|
+
if (!manifest.generationId)
|
|
218
|
+
return;
|
|
219
|
+
const source = generationDirectory(manifest.generationId, home);
|
|
220
|
+
if (!source)
|
|
221
|
+
return;
|
|
222
|
+
for (const name of ["rules", "topics", "pending"]) {
|
|
223
|
+
const sourceDirectory = join(source, name);
|
|
224
|
+
if (!existsSync(sourceDirectory))
|
|
225
|
+
continue;
|
|
226
|
+
for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
227
|
+
if (!entry.isFile())
|
|
228
|
+
continue;
|
|
229
|
+
const sourceFile = join(sourceDirectory, entry.name);
|
|
230
|
+
const targetFile = join(target, name, entry.name);
|
|
231
|
+
try {
|
|
232
|
+
linkSync(sourceFile, targetFile);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
copyFileSync(sourceFile, targetFile);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function storedRuleCharacters(directory) {
|
|
241
|
+
return readdirSync(join(directory, "rules"), { withFileTypes: true })
|
|
242
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
243
|
+
.reduce((total, entry) => total + readFileSync(join(directory, "rules", entry.name), "utf8").length, 0);
|
|
244
|
+
}
|
|
245
|
+
function reviewCursor(task) {
|
|
246
|
+
return {
|
|
247
|
+
sessionId: task.sessionId, sessionKey: task.sessionKey, sessionFileHash: task.sessionFileHash,
|
|
248
|
+
projectRoot: task.projectRoot, projectKey: task.projectKey, lastReviewedEntryId: task.lastEntryId,
|
|
249
|
+
lastReviewedAt: new Date().toISOString(), lastDeltaDigest: task.deltaDigest,
|
|
250
|
+
lastReviewKey: task.reviewKey, fileSize: task.fileSize, fileMtimeMs: task.fileMtimeMs,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
export function commitKnowledgeReview(values, task, leaderToken, home = homedir()) {
|
|
254
|
+
ensureKnowledgeDirectories(home);
|
|
255
|
+
const release = acquireCommitLock(leaderToken, home);
|
|
256
|
+
const paths = userRuntimePaths(home);
|
|
257
|
+
let temporary = "";
|
|
258
|
+
try {
|
|
259
|
+
const current = loadCurrentManifest(home);
|
|
260
|
+
if (current.reviews[task.sessionKey]?.lastReviewKey === task.reviewKey) {
|
|
261
|
+
return { saved: 0, updated: 0, pending: 0, skipped: 0 };
|
|
262
|
+
}
|
|
263
|
+
const generationId = newGenerationId();
|
|
264
|
+
temporary = join(paths.knowledgeGenerations, `.tmp-${generationId}-${process.pid}`);
|
|
265
|
+
ensureDirectory(temporary);
|
|
266
|
+
copyCurrentContent(current, temporary, home);
|
|
267
|
+
const catalog = structuredClone(current.catalog);
|
|
268
|
+
const result = { saved: 0, updated: 0, pending: 0, skipped: 0 };
|
|
269
|
+
for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
|
|
270
|
+
const candidate = normalizeCandidate(value, task.projectKey);
|
|
271
|
+
if (!candidate) {
|
|
272
|
+
result.skipped++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const fingerprint = normalizedFingerprint(candidate);
|
|
276
|
+
const hash = contentHash(candidate);
|
|
277
|
+
const existingIndex = catalog.items.findIndex((item) => item.fingerprint === fingerprint);
|
|
278
|
+
const existing = existingIndex >= 0 ? catalog.items[existingIndex] : undefined;
|
|
279
|
+
if (existing?.contentHash === hash) {
|
|
280
|
+
existing.evidenceCount += candidate.evidence.length;
|
|
281
|
+
existing.updatedAt = new Date().toISOString();
|
|
282
|
+
result.updated++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (existing && !(candidate.action === "revise" && (candidate.explicitUserDirective || candidate.confidence >= 0.92))) {
|
|
286
|
+
const name = `${Date.now()}-${normalizeSlug(candidate.title)}-${randomUUID().slice(0, 8)}.json`;
|
|
287
|
+
atomicWrite(join(temporary, "pending", name), `${JSON.stringify({ reason: "conflicting-or-ambiguous-update", candidate }, null, 2)}\n`);
|
|
288
|
+
result.pending++;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!existing && candidate.storageHint === "rule"
|
|
292
|
+
&& storedRuleCharacters(temporary) + renderKnowledgeFile(candidate).length > STORAGE.maxRulesPromptChars) {
|
|
293
|
+
candidate.storageHint = "topic";
|
|
294
|
+
}
|
|
295
|
+
const now = new Date().toISOString();
|
|
296
|
+
const id = existing?.id ?? `${normalizeSlug(candidate.title)}-${fingerprint.slice(0, 8)}`;
|
|
297
|
+
const track = existing?.track ?? candidate.storageHint;
|
|
298
|
+
candidate.storageHint = track;
|
|
299
|
+
const relativeFile = `${track === "rule" ? "rules" : "topics"}/${id}.md`;
|
|
300
|
+
atomicWrite(join(temporary, relativeFile), renderKnowledgeFile(candidate));
|
|
301
|
+
const entry = {
|
|
302
|
+
id, fingerprint, contentHash: hash, title: candidate.title, summary: candidate.summary,
|
|
303
|
+
keywords: candidate.keywords, scope: candidate.scope, track, file: relativeFile,
|
|
304
|
+
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
305
|
+
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
306
|
+
};
|
|
307
|
+
if (existingIndex >= 0) {
|
|
308
|
+
catalog.items[existingIndex] = entry;
|
|
309
|
+
result.updated++;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
catalog.items.push(entry);
|
|
313
|
+
result.saved++;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catalog.updatedAt = new Date().toISOString();
|
|
317
|
+
const manifest = {
|
|
318
|
+
version: 3, generationId, createdAt: new Date().toISOString(), leaderToken, catalog,
|
|
319
|
+
reviews: { ...current.reviews, [task.sessionKey]: reviewCursor(task) },
|
|
320
|
+
};
|
|
321
|
+
atomicWrite(join(temporary, "MEMORY.md"), generateMemory(catalog));
|
|
322
|
+
atomicWrite(join(temporary, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
323
|
+
if (!validManifestAt(manifest, temporary))
|
|
324
|
+
throw new Error("Refusing to publish an incomplete knowledge generation");
|
|
325
|
+
const finalDirectory = join(paths.knowledgeGenerations, generationId);
|
|
326
|
+
renameSync(temporary, finalDirectory);
|
|
327
|
+
temporary = "";
|
|
328
|
+
atomicWrite(paths.knowledgeCurrent, `${generationId}\n`);
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
if (temporary)
|
|
333
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
334
|
+
release();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function readCatalogFile(entry, manifest, home) {
|
|
338
|
+
if (!SAFE_ID_RE.test(entry.id) || basename(entry.file) !== `${entry.id}.md`)
|
|
339
|
+
return undefined;
|
|
340
|
+
const root = generationDirectory(manifest.generationId, home);
|
|
341
|
+
if (!root)
|
|
342
|
+
return undefined;
|
|
343
|
+
const path = resolve(root, entry.file);
|
|
344
|
+
if (!path.startsWith(`${root}${sep}`) || !existsSync(path))
|
|
345
|
+
return undefined;
|
|
346
|
+
return sanitizeKnowledgeText(readFileSync(path, "utf8"));
|
|
347
|
+
}
|
|
348
|
+
export function loadKnowledgeSnapshot(projectKey, home = homedir()) {
|
|
349
|
+
const manifest = loadCurrentManifest(home);
|
|
350
|
+
if (!manifest.generationId) {
|
|
351
|
+
return { rulesPrompt: "", memoryPrompt: generateMemory(manifest.catalog, projectKey), catalog: manifest.catalog };
|
|
352
|
+
}
|
|
353
|
+
const rules = manifest.catalog.items.filter((entry) => entry.track === "rule" && applicable(entry, projectKey))
|
|
354
|
+
.sort((left, right) => left.file.localeCompare(right.file))
|
|
355
|
+
.map((entry) => readCatalogFile(entry, manifest, home)).filter((content) => Boolean(content));
|
|
356
|
+
return {
|
|
357
|
+
generationId: manifest.generationId, rulesPrompt: rules.join("\n\n"),
|
|
358
|
+
memoryPrompt: generateMemory(manifest.catalog, projectKey), catalog: manifest.catalog,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export function loadKnowledgeById(id, projectKey, home = homedir()) {
|
|
362
|
+
if (!SAFE_ID_RE.test(id))
|
|
363
|
+
return undefined;
|
|
364
|
+
const manifest = loadCurrentManifest(home);
|
|
365
|
+
const entry = manifest.catalog.items.find((item) => item.id === id && applicable(item, projectKey));
|
|
366
|
+
if (!entry)
|
|
367
|
+
return undefined;
|
|
368
|
+
const content = readCatalogFile(entry, manifest, home);
|
|
369
|
+
return content ? { entry, content } : undefined;
|
|
370
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Internal product defaults and safety limits. User-overridable settings stay in settings.json. */
|
|
2
|
+
export const CLOUD_RUNTIME_DEFAULTS = Object.freeze({
|
|
3
|
+
workflow: Object.freeze({ maxFailedApproaches: 3, maxSuccessfulSteps: 100 }),
|
|
4
|
+
process: Object.freeze({
|
|
5
|
+
commandTimeoutMs: 120_000,
|
|
6
|
+
providerValidationTimeoutMs: 30_000,
|
|
7
|
+
forceKillGraceMs: 2_000,
|
|
8
|
+
maxOutputBytes: 50_000,
|
|
9
|
+
}),
|
|
10
|
+
runner: Object.freeze({
|
|
11
|
+
defaultUser: "ubuntu",
|
|
12
|
+
defaultPort: 22,
|
|
13
|
+
keyCandidates: Object.freeze(["id_ed25519", "id_rsa"]),
|
|
14
|
+
hostKeyScanTimeoutMs: 15_000,
|
|
15
|
+
connectTimeoutSeconds: 15,
|
|
16
|
+
commandTimeoutMs: 120_000,
|
|
17
|
+
}),
|
|
18
|
+
templates: Object.freeze({ slugMaxLength: 48, nameMaxLength: 80 }),
|
|
19
|
+
terraform: Object.freeze({ executable: "terraform", planFile: "hwcode.tfplan" }),
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Defaults for the cross-workflow knowledge base (`~/.hwcode/knowledge-v3/`).
|
|
23
|
+
*
|
|
24
|
+
* A Worker checks settled sessions on a fixed interval. Short rules are loaded
|
|
25
|
+
* in full; detailed topics are routed through a bounded MEMORY.md index so the
|
|
26
|
+
* prompt cost stays stable as the complete machine catalog grows.
|
|
27
|
+
*/
|
|
28
|
+
export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
|
|
29
|
+
review: Object.freeze({
|
|
30
|
+
intervalMs: 60_000,
|
|
31
|
+
idleMs: 60_000,
|
|
32
|
+
capabilityPollMs: 5_000,
|
|
33
|
+
modelTimeoutMs: 120_000,
|
|
34
|
+
maxDeltaChars: 30_000,
|
|
35
|
+
maxCandidates: 8,
|
|
36
|
+
minimumConfidence: 0.72,
|
|
37
|
+
}),
|
|
38
|
+
storage: Object.freeze({
|
|
39
|
+
maxRuleChars: 600,
|
|
40
|
+
maxRuleFileLines: 40,
|
|
41
|
+
maxRulesPromptChars: 12_000,
|
|
42
|
+
maxMemoryChars: 25_000,
|
|
43
|
+
maxMemoryLines: 200,
|
|
44
|
+
maxSummaryChars: 180,
|
|
45
|
+
maxTitleChars: 100,
|
|
46
|
+
maxKeywordCount: 12,
|
|
47
|
+
maxLookupResults: 5,
|
|
48
|
+
slugMaxChars: 64,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, posix, resolve } from "node:path";
|
|
3
|
+
export const HWCODE_DATA_DIRECTORY = ".hwcode";
|
|
4
|
+
export const PROJECT_SDD_SPECS_RELATIVE = `${HWCODE_DATA_DIRECTORY}/specs`;
|
|
5
|
+
export const PROJECT_CLOUD_RUNS_RELATIVE = `${HWCODE_DATA_DIRECTORY}/cloud/runs`;
|
|
6
|
+
// Canonical subpath layout under ~/.hwcode. Kept together so filesystem topology
|
|
7
|
+
// stays in one place instead of scattered across cloud/knowledge modules.
|
|
8
|
+
const CLOUD_SUBDIR = "cloud";
|
|
9
|
+
const CLOUD_VAULT_FILE = "credentials.enc";
|
|
10
|
+
const CLOUD_KNOWN_HOSTS_FILE = "known_hosts";
|
|
11
|
+
const CLOUD_TEMPLATES_SUBDIR = "templates";
|
|
12
|
+
const CLOUD_TERRAFORM_SUBDIR = "terraform";
|
|
13
|
+
const KNOWLEDGE_SUBDIR = "knowledge-v3";
|
|
14
|
+
export function projectRuntimePaths(projectRoot) {
|
|
15
|
+
const root = resolve(projectRoot);
|
|
16
|
+
const hwcode = join(root, HWCODE_DATA_DIRECTORY);
|
|
17
|
+
const cloud = join(hwcode, CLOUD_SUBDIR);
|
|
18
|
+
return {
|
|
19
|
+
root,
|
|
20
|
+
hwcode,
|
|
21
|
+
specs: join(hwcode, "specs"),
|
|
22
|
+
cloud,
|
|
23
|
+
cloudRuns: join(cloud, "runs"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function userRuntimePaths(home = homedir()) {
|
|
27
|
+
const root = join(home, HWCODE_DATA_DIRECTORY);
|
|
28
|
+
const cloud = join(root, CLOUD_SUBDIR);
|
|
29
|
+
const knowledge = join(root, KNOWLEDGE_SUBDIR);
|
|
30
|
+
return {
|
|
31
|
+
root,
|
|
32
|
+
cloud,
|
|
33
|
+
cloudVault: join(cloud, CLOUD_VAULT_FILE),
|
|
34
|
+
cloudKnownHosts: join(cloud, CLOUD_KNOWN_HOSTS_FILE),
|
|
35
|
+
cloudTerraformTemplates: join(cloud, CLOUD_TEMPLATES_SUBDIR, CLOUD_TERRAFORM_SUBDIR),
|
|
36
|
+
knowledge,
|
|
37
|
+
knowledgeGenerations: join(knowledge, "generations"),
|
|
38
|
+
knowledgeCurrent: join(knowledge, "CURRENT"),
|
|
39
|
+
knowledgeRuntime: join(knowledge, "runtime"),
|
|
40
|
+
// Unix-domain socket paths are short on purpose (macOS caps them at roughly 104 bytes).
|
|
41
|
+
knowledgeCoordinatorSocket: join(root, "knowledge-v3.sock"),
|
|
42
|
+
knowledgeLeader: join(knowledge, "runtime", "leader.json"),
|
|
43
|
+
knowledgeCommitLock: join(knowledge, "runtime", "commit.lock"),
|
|
44
|
+
knowledgeElectionLock: join(knowledge, "runtime", "election.lock"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function remoteRunnerPaths(user) {
|
|
48
|
+
const root = posix.join("/home", user, HWCODE_DATA_DIRECTORY);
|
|
49
|
+
return { root, runs: posix.join(root, "runs") };
|
|
50
|
+
}
|
|
51
|
+
export function profileResourcePath(fileName) {
|
|
52
|
+
return process.env.HWCODE_PROFILE_DIR
|
|
53
|
+
? join(process.env.HWCODE_PROFILE_DIR, fileName)
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { Worker } from "node:worker_threads";
|
|
4
6
|
|
|
5
7
|
import { Type } from "@earendil-works/pi-ai";
|
|
@@ -27,11 +29,17 @@ interface ProcessKnowledgeRuntime {
|
|
|
27
29
|
modelAvailable?: boolean;
|
|
28
30
|
activeReview?: ActiveReview;
|
|
29
31
|
capabilityTimer?: NodeJS.Timeout;
|
|
32
|
+
workerRetryTimer?: NodeJS.Timeout;
|
|
33
|
+
workerRetryAttempt: number;
|
|
34
|
+
lastWorkerIssue?: { message: string; reportedAt: number };
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
const RUNTIME_SYMBOL = Symbol.for("hwcode.knowledge.runtime.v3");
|
|
33
38
|
const processGlobals = globalThis as unknown as Record<PropertyKey, unknown>;
|
|
34
|
-
const runtime = (processGlobals[RUNTIME_SYMBOL] ??= {}) as ProcessKnowledgeRuntime;
|
|
39
|
+
const runtime = (processGlobals[RUNTIME_SYMBOL] ??= { workerRetryAttempt: 0 }) as ProcessKnowledgeRuntime;
|
|
40
|
+
runtime.workerRetryAttempt ??= 0;
|
|
41
|
+
|
|
42
|
+
const WORKER_RETRY_DELAYS_MS = [1_000, 5_000, 30_000, 60_000] as const;
|
|
35
43
|
|
|
36
44
|
function post(message: KnowledgeWorkerInput): void {
|
|
37
45
|
runtime.worker?.postMessage(message);
|
|
@@ -54,6 +62,37 @@ function cancelActiveReview(): void {
|
|
|
54
62
|
runtime.activeReview?.controller.abort();
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
function reportWorkerIssue(error: unknown): void {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const prior = runtime.lastWorkerIssue;
|
|
69
|
+
if (prior?.message === message && now - prior.reportedAt < 60_000) return;
|
|
70
|
+
runtime.lastWorkerIssue = { message, reportedAt: now };
|
|
71
|
+
const rendered = `HWCode knowledge worker failed: ${message}`;
|
|
72
|
+
if (runtime.context?.hasUI) runtime.context.ui.notify(rendered, "error");
|
|
73
|
+
else console.error(rendered);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function scheduleWorkerRestart(): void {
|
|
77
|
+
if (!runtime.context || runtime.workerRetryTimer) return;
|
|
78
|
+
const index = Math.min(runtime.workerRetryAttempt, WORKER_RETRY_DELAYS_MS.length - 1);
|
|
79
|
+
const delay = WORKER_RETRY_DELAYS_MS[index];
|
|
80
|
+
runtime.workerRetryAttempt += 1;
|
|
81
|
+
runtime.workerRetryTimer = setTimeout(() => {
|
|
82
|
+
runtime.workerRetryTimer = undefined;
|
|
83
|
+
if (!runtime.context) return;
|
|
84
|
+
ensureWorker();
|
|
85
|
+
updateCapability();
|
|
86
|
+
}, delay);
|
|
87
|
+
runtime.workerRetryTimer.unref();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function workerEntryUrl(): URL {
|
|
91
|
+
const compiled = new URL("../dist/lib/knowledge/review-worker.js", import.meta.url);
|
|
92
|
+
if (existsSync(fileURLToPath(compiled))) return compiled;
|
|
93
|
+
return new URL("../lib/knowledge/review-worker.ts", import.meta.url);
|
|
94
|
+
}
|
|
95
|
+
|
|
57
96
|
async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review_request" }>): Promise<void> {
|
|
58
97
|
const ctx = runtime.context;
|
|
59
98
|
if (!ctx?.model || !ctx.modelRegistry.hasConfiguredAuth(ctx.model)) {
|
|
@@ -98,29 +137,41 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
98
137
|
}
|
|
99
138
|
|
|
100
139
|
function handleWorkerMessage(message: KnowledgeWorkerOutput): void {
|
|
140
|
+
if (message.type === "ready") {
|
|
141
|
+
runtime.workerRetryAttempt = 0;
|
|
142
|
+
runtime.lastWorkerIssue = undefined;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
101
145
|
if (message.type === "review_request") { void runReview(message); return; }
|
|
102
146
|
if (message.type === "review_cancel" && runtime.activeReview?.requestId === message.requestId) {
|
|
103
147
|
runtime.activeReview.controller.abort();
|
|
148
|
+
return;
|
|
104
149
|
}
|
|
150
|
+
if (message.type === "review_failed") reportWorkerIssue(message.error);
|
|
105
151
|
}
|
|
106
152
|
|
|
107
|
-
function ensureWorker(): Worker {
|
|
153
|
+
function ensureWorker(): Worker | undefined {
|
|
108
154
|
if (runtime.worker) return runtime.worker;
|
|
109
|
-
|
|
155
|
+
if (runtime.workerRetryTimer) return undefined;
|
|
156
|
+
const worker = new Worker(workerEntryUrl());
|
|
110
157
|
worker.unref();
|
|
111
158
|
runtime.worker = worker;
|
|
112
159
|
worker.on("message", (message: KnowledgeWorkerOutput) => handleWorkerMessage(message));
|
|
113
|
-
worker.on("error", () => {
|
|
160
|
+
worker.on("error", (error) => {
|
|
161
|
+
reportWorkerIssue(error);
|
|
114
162
|
cancelActiveReview();
|
|
115
163
|
if (runtime.worker === worker) {
|
|
116
164
|
runtime.worker = undefined;
|
|
117
165
|
runtime.modelAvailable = undefined;
|
|
166
|
+
scheduleWorkerRestart();
|
|
118
167
|
}
|
|
119
168
|
});
|
|
120
|
-
worker.on("exit", () => {
|
|
169
|
+
worker.on("exit", (code) => {
|
|
121
170
|
if (runtime.worker === worker) {
|
|
122
171
|
runtime.worker = undefined;
|
|
123
172
|
runtime.modelAvailable = undefined;
|
|
173
|
+
if (code !== 0) reportWorkerIssue(`worker exited with code ${code}`);
|
|
174
|
+
scheduleWorkerRestart();
|
|
124
175
|
}
|
|
125
176
|
});
|
|
126
177
|
runtime.capabilityTimer ??= setInterval(updateCapability, KNOWLEDGE_RUNTIME_DEFAULTS.review.capabilityPollMs);
|
|
@@ -7,6 +7,7 @@ export type KnowledgeWorkerInput =
|
|
|
7
7
|
| { type: "stop" };
|
|
8
8
|
|
|
9
9
|
export type KnowledgeWorkerOutput =
|
|
10
|
+
| { type: "ready" }
|
|
10
11
|
| { type: "leadership"; state: "leader" | "standby" | "ineligible"; leaderToken?: string }
|
|
11
12
|
| { type: "review_request"; leaderToken: string; requestId: string; task: KnowledgeReviewTask }
|
|
12
13
|
| { type: "review_cancel"; requestId: string; reason: string }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadooppei/hwcode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "A customizable terminal coding agent with local-model support and HWCode workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"bin/hwcode.js",
|
|
11
11
|
".env.example",
|
|
12
12
|
".pi/APPEND_SYSTEM.md",
|
|
13
|
+
".pi/dist",
|
|
13
14
|
".pi/settings.json",
|
|
14
15
|
".pi/extensions",
|
|
15
16
|
".pi/lib",
|
|
@@ -25,10 +26,12 @@
|
|
|
25
26
|
"scripts": {
|
|
26
27
|
"start": "node --env-file-if-exists=.env ./bin/hwcode.js",
|
|
27
28
|
"pi": "node --env-file-if-exists=.env ./bin/hwcode.js",
|
|
29
|
+
"build:knowledge-worker": "node scripts/build-knowledge-worker.mjs",
|
|
28
30
|
"test": "node scripts/run-tests.mjs",
|
|
31
|
+
"test:package": "node scripts/test-packed-worker.mjs",
|
|
29
32
|
"typecheck": "tsc -p tsconfig.json",
|
|
30
33
|
"test:workflows": "npm test",
|
|
31
|
-
"prepack": "npm run typecheck && npm test && node scripts/audit-package.mjs"
|
|
34
|
+
"prepack": "npm run typecheck && npm test && npm run build:knowledge-worker && npm run test:package && node scripts/audit-package.mjs"
|
|
32
35
|
},
|
|
33
36
|
"engines": {
|
|
34
37
|
"node": ">=22.19.0"
|