@hadooppei/hwcode 1.0.10 → 1.0.11

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.
@@ -7,10 +7,12 @@ Persist only knowledge that is likely to be useful in future sessions:
7
7
  - verified engineering rules, successful procedures, or expensive failed approaches;
8
8
  - reusable architecture, testing, debugging, deployment, or operational knowledge.
9
9
  Do not persist task summaries, guesses, temporary IDs, credentials, secrets, private data, or facts recoverable by simply reading the repository.
10
+ Do not persist mutable inventory such as current cloud resources, names, availability, status, timestamps, or account snapshots. Preserve the discovery method or selection rule instead. If useful procedure is mixed with volatile facts, remove the volatile facts.
10
11
  Use storageHint "rule" only for short, precise, high-value instructions. Use "topic" for multi-step SOPs and detailed experience.
11
12
  Use action "revise" only when the delta explicitly corrects prior knowledge; otherwise use "add" or "reinforce".
12
13
  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
+ Each summary must be one complete sentence of at most 160 characters. Keep each body under 3000 characters. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered.
15
+ Return: {"candidates":[{"key":"stable semantic key","title":"...","summary":"one complete sentence","keywords":["..."],"scope":"global|project","body":"markdown body","evidence":["verified evidence"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|revise","explicitUserDirective":false,"durability":"stable"}]}
14
16
  Return {"candidates":[]} when nothing meets the threshold.`;
15
17
  export function buildKnowledgeExtractionPrompt(projectRoot, delta) {
16
18
  return `Project root: ${projectRoot}\n\n<conversation_delta>\n${delta}\n</conversation_delta>`;
@@ -22,10 +24,78 @@ export function parseCandidateEnvelope(text) {
22
24
  const end = unfenced.lastIndexOf("}");
23
25
  if (start < 0 || end <= start)
24
26
  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);
27
+ let strictError;
28
+ try {
29
+ const parsed = JSON.parse(unfenced.slice(start, end + 1));
30
+ if (!Array.isArray(parsed.candidates))
31
+ throw new Error("Knowledge reviewer response is missing candidates[]");
32
+ return parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates);
33
+ }
34
+ catch (error) {
35
+ strictError = error;
36
+ }
37
+ const arrayKey = /["']candidates["']\s*:/gu.exec(unfenced);
38
+ const arrayStart = arrayKey ? unfenced.indexOf("[", arrayKey.index + arrayKey[0].length) : -1;
39
+ if (arrayStart >= 0) {
40
+ const candidates = [];
41
+ let inString = false;
42
+ let escaped = false;
43
+ for (let index = arrayStart + 1; index < unfenced.length && candidates.length < KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates; index++) {
44
+ const character = unfenced[index];
45
+ if (inString) {
46
+ if (escaped)
47
+ escaped = false;
48
+ else if (character === "\\")
49
+ escaped = true;
50
+ else if (character === '"')
51
+ inString = false;
52
+ continue;
53
+ }
54
+ if (character === '"') {
55
+ inString = true;
56
+ continue;
57
+ }
58
+ if (character !== "{")
59
+ continue;
60
+ const objectEnd = balancedObjectEnd(unfenced, index);
61
+ if (objectEnd < 0)
62
+ break;
63
+ try {
64
+ candidates.push(JSON.parse(unfenced.slice(index, objectEnd + 1)));
65
+ }
66
+ catch { /* Salvage other complete candidates. */ }
67
+ index = objectEnd;
68
+ }
69
+ if (candidates.length > 0)
70
+ return candidates;
71
+ }
72
+ throw strictError;
73
+ }
74
+ function balancedObjectEnd(value, start) {
75
+ let depth = 0;
76
+ let inString = false;
77
+ let escaped = false;
78
+ for (let index = start; index < value.length; index++) {
79
+ const character = value[index];
80
+ if (inString) {
81
+ if (escaped)
82
+ escaped = false;
83
+ else if (character === "\\")
84
+ escaped = true;
85
+ else if (character === '"')
86
+ inString = false;
87
+ continue;
88
+ }
89
+ if (character === '"') {
90
+ inString = true;
91
+ continue;
92
+ }
93
+ if (character === "{")
94
+ depth++;
95
+ else if (character === "}" && --depth === 0)
96
+ return index;
97
+ }
98
+ return -1;
29
99
  }
30
100
  export function knowledgeDeltaDigest(delta) {
31
101
  return createHash("sha256").update(delta).digest("hex");
@@ -0,0 +1,20 @@
1
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { userRuntimePaths } from "../runtime/paths.js";
4
+ export function writeKnowledgeReviewStatus(status, home) {
5
+ const path = userRuntimePaths(home).knowledgeStatus;
6
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
7
+ const temporary = `${path}.${process.pid}.tmp`;
8
+ writeFileSync(temporary, `${JSON.stringify(status, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
9
+ renameSync(temporary, path);
10
+ chmodSync(path, 0o600);
11
+ }
12
+ export function readKnowledgeReviewStatus(home) {
13
+ try {
14
+ const parsed = JSON.parse(readFileSync(userRuntimePaths(home).knowledgeStatus, "utf8"));
15
+ return parsed.version === 1 && parsed.protocolVersion === 3 ? parsed : undefined;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
@@ -6,6 +6,8 @@ import { parentPort, workerData } from "node:worker_threads";
6
6
  import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
7
7
  import { userRuntimePaths } from "../runtime/paths.js";
8
8
  import { parseCandidateEnvelope } from "./extractor.js";
9
+ import { writeKnowledgeReviewStatus } from "./review-status.js";
10
+ import { sanitizeKnowledgeText } from "./sanitize.js";
9
11
  import { findNextReviewTask } from "./session-scanner.js";
10
12
  import { commitKnowledgeReview, ensureKnowledgeDirectories, loadCurrentManifest } from "./store.js";
11
13
  if (!parentPort)
@@ -21,6 +23,11 @@ let leaderToken = "";
21
23
  let standbySocket;
22
24
  let electionPending = false;
23
25
  let activeTask;
26
+ let activeAttempt = 0;
27
+ let activeStartedAt = 0;
28
+ let activeDeadlineTimer;
29
+ let reviewStatus;
30
+ const reviewAttempts = new Map();
24
31
  function send(message) {
25
32
  parentPort.postMessage(message);
26
33
  }
@@ -29,12 +36,74 @@ function coordinatorPath() {
29
36
  ? `\\\\.\\pipe\\hwcode-knowledge-v3-${Buffer.from(home).toString("hex").slice(0, 16)}`
30
37
  : paths.knowledgeCoordinatorSocket;
31
38
  }
32
- function writeLeaderMetadata() {
39
+ function writeLeaderMetadata(electedAt) {
33
40
  writeFileSync(paths.knowledgeLeader, `${JSON.stringify({
34
- protocolVersion: 3, leaderToken, pid: process.pid, electedAt: new Date().toISOString(),
41
+ protocolVersion: 3, leaderToken, pid: process.pid, electedAt,
35
42
  }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
36
43
  chmodSync(paths.knowledgeLeader, 0o600);
37
44
  }
45
+ function publishReviewStatus() {
46
+ if (!reviewStatus)
47
+ return;
48
+ reviewStatus.updatedAt = new Date().toISOString();
49
+ try {
50
+ writeKnowledgeReviewStatus(reviewStatus, home);
51
+ }
52
+ catch (error) {
53
+ send({ type: "review_failed", error: `Unable to write knowledge status: ${error instanceof Error ? error.message : String(error)}` });
54
+ }
55
+ }
56
+ function boundedError(error) {
57
+ return sanitizeKnowledgeText(error instanceof Error ? error.message : String(error)).slice(0, 1_000);
58
+ }
59
+ function beginReview(task) {
60
+ activeAttempt = (reviewAttempts.get(task.reviewKey) ?? 0) + 1;
61
+ reviewAttempts.set(task.reviewKey, activeAttempt);
62
+ activeStartedAt = Date.now();
63
+ clearTimeout(activeDeadlineTimer);
64
+ activeDeadlineTimer = setTimeout(() => {
65
+ if (activeTask?.requestId !== task.requestId)
66
+ return;
67
+ const error = "knowledge-review-result-timeout";
68
+ finishReview(task, "failed", { error });
69
+ activeTask = undefined;
70
+ send({ type: "review_cancel", requestId: task.requestId, reason: error });
71
+ send({ type: "review_failed", requestId: task.requestId, error });
72
+ }, KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs + 5_000);
73
+ activeDeadlineTimer.unref();
74
+ if (reviewStatus) {
75
+ reviewStatus.activeReview = {
76
+ reviewKey: task.reviewKey,
77
+ sessionKey: task.sessionKey,
78
+ sessionFileHash: task.sessionFileHash,
79
+ projectKey: task.projectKey,
80
+ attempt: activeAttempt,
81
+ startedAt: new Date(activeStartedAt).toISOString(),
82
+ };
83
+ publishReviewStatus();
84
+ }
85
+ }
86
+ function finishReview(task, outcome, detail) {
87
+ clearTimeout(activeDeadlineTimer);
88
+ activeDeadlineTimer = undefined;
89
+ if (!reviewStatus)
90
+ return;
91
+ reviewStatus.activeReview = undefined;
92
+ reviewStatus.lastReview = {
93
+ reviewKey: task.reviewKey,
94
+ sessionKey: task.sessionKey,
95
+ attempt: activeAttempt,
96
+ outcome,
97
+ completedAt: new Date().toISOString(),
98
+ durationMs: Math.max(0, Date.now() - activeStartedAt),
99
+ generationId: detail.generationId,
100
+ result: detail.result,
101
+ error: detail.error === undefined ? undefined : boundedError(detail.error),
102
+ };
103
+ if (outcome === "saved")
104
+ reviewAttempts.delete(task.reviewKey);
105
+ publishReviewStatus();
106
+ }
38
107
  function removeLeaderMetadata(token) {
39
108
  try {
40
109
  const current = JSON.parse(readFileSync(paths.knowledgeLeader, "utf8"));
@@ -63,7 +132,17 @@ function scheduleElection(delay = Math.floor(Math.random() * 500)) {
63
132
  function stepDown(reason) {
64
133
  if (activeTask)
65
134
  send({ type: "review_cancel", requestId: activeTask.requestId, reason });
135
+ if (reviewStatus) {
136
+ reviewStatus.state = "stepped-down";
137
+ reviewStatus.stepDownReason = reason;
138
+ reviewStatus.activeReview = undefined;
139
+ publishReviewStatus();
140
+ }
66
141
  activeTask = undefined;
142
+ clearTimeout(activeDeadlineTimer);
143
+ activeDeadlineTimer = undefined;
144
+ activeAttempt = 0;
145
+ activeStartedAt = 0;
67
146
  const token = leaderToken;
68
147
  leaderToken = "";
69
148
  for (const socket of standbySockets)
@@ -85,6 +164,7 @@ function stepDown(reason) {
85
164
  }
86
165
  if (token)
87
166
  removeLeaderMetadata(token);
167
+ reviewStatus = undefined;
88
168
  }
89
169
  function becomeStandby(socket) {
90
170
  standbySocket = socket;
@@ -162,6 +242,7 @@ function connectToLeader(probe = 0) {
162
242
  function becomeLeader(server) {
163
243
  leaderServer = server;
164
244
  leaderToken = randomUUID();
245
+ const electedAt = new Date().toISOString();
165
246
  server.unref();
166
247
  server.on("connection", (socket) => {
167
248
  standbySockets.add(socket);
@@ -176,7 +257,11 @@ function becomeLeader(server) {
176
257
  stepDown("coordinator-server-failed");
177
258
  scheduleElection();
178
259
  });
179
- writeLeaderMetadata();
260
+ writeLeaderMetadata(electedAt);
261
+ reviewStatus = {
262
+ version: 1, protocolVersion: 3, state: "leader", pid: process.pid, leaderToken, electedAt, updatedAt: electedAt,
263
+ };
264
+ publishReviewStatus();
180
265
  send({ type: "leadership", state: "leader", leaderToken });
181
266
  void scanForReview();
182
267
  }
@@ -204,15 +289,22 @@ function attemptElection() {
204
289
  async function scanForReview() {
205
290
  if (!leaderServer || !leaderToken || activeTask || !sessionsRoot || !eligible)
206
291
  return;
292
+ if (reviewStatus) {
293
+ reviewStatus.lastScanAt = new Date().toISOString();
294
+ reviewStatus.lastScanError = undefined;
295
+ publishReviewStatus();
296
+ }
207
297
  try {
208
298
  const manifest = loadCurrentManifest(home);
209
299
  const task = findNextReviewTask(sessionsRoot, manifest.reviews);
210
300
  if (!task)
211
301
  return;
212
302
  activeTask = task;
303
+ beginReview(task);
213
304
  if (!task.delta) {
214
305
  const result = commitKnowledgeReview([], task, leaderToken, home);
215
306
  const generationId = loadCurrentManifest(home).generationId;
307
+ finishReview(task, "saved", { generationId, result });
216
308
  send({ type: "review_saved", requestId: task.requestId, generationId, result });
217
309
  if (generationId) {
218
310
  send({ type: "generation_changed", generationId });
@@ -225,8 +317,14 @@ async function scanForReview() {
225
317
  send({ type: "review_request", leaderToken, requestId: task.requestId, task });
226
318
  }
227
319
  catch (error) {
320
+ if (activeTask)
321
+ finishReview(activeTask, "failed", { error });
322
+ else if (reviewStatus) {
323
+ reviewStatus.lastScanError = { at: new Date().toISOString(), error: boundedError(error) };
324
+ publishReviewStatus();
325
+ }
228
326
  activeTask = undefined;
229
- send({ type: "review_failed", error: error instanceof Error ? error.message : String(error) });
327
+ send({ type: "review_failed", error: boundedError(error) });
230
328
  }
231
329
  }
232
330
  function handleReviewResult(message) {
@@ -239,6 +337,7 @@ function handleReviewResult(message) {
239
337
  throw new Error(message.error || "Knowledge review returned no content");
240
338
  const result = commitKnowledgeReview(parseCandidateEnvelope(message.raw), task, leaderToken, home);
241
339
  const generationId = loadCurrentManifest(home).generationId;
340
+ finishReview(task, "saved", { generationId, result });
242
341
  send({ type: "review_saved", requestId: task.requestId, generationId, result });
243
342
  if (generationId) {
244
343
  send({ type: "generation_changed", generationId });
@@ -247,7 +346,8 @@ function handleReviewResult(message) {
247
346
  committed = true;
248
347
  }
249
348
  catch (error) {
250
- send({ type: "review_failed", requestId: task.requestId, error: error instanceof Error ? error.message : String(error) });
349
+ finishReview(task, "failed", { error });
350
+ send({ type: "review_failed", requestId: task.requestId, error: boundedError(error) });
251
351
  }
252
352
  finally {
253
353
  activeTask = undefined;
@@ -22,3 +22,23 @@ export function compactKnowledgeText(value, maxChars) {
22
22
  .trim()
23
23
  .slice(0, maxChars);
24
24
  }
25
+ export function compactKnowledgeSummary(value, maxChars) {
26
+ const normalized = sanitizeKnowledgeText(value).replace(/\s+/gu, " ").trim();
27
+ if (normalized.length <= maxChars || maxChars <= 0)
28
+ return normalized.slice(0, Math.max(0, maxChars));
29
+ if (maxChars === 1)
30
+ return "…";
31
+ const limit = maxChars - 1;
32
+ const prefix = normalized.slice(0, limit);
33
+ const minimumBoundary = Math.floor(limit * 0.6);
34
+ const sentenceEnd = Math.max(prefix.lastIndexOf("。"), prefix.lastIndexOf("!"), prefix.lastIndexOf("?"), prefix.lastIndexOf("."), prefix.lastIndexOf("!"), prefix.lastIndexOf("?"));
35
+ if (sentenceEnd >= minimumBoundary)
36
+ return prefix.slice(0, sentenceEnd + 1).trimEnd();
37
+ const next = normalized.charAt(limit);
38
+ if (/[A-Za-z0-9]/u.test(prefix.at(-1) ?? "") && /[A-Za-z0-9]/u.test(next)) {
39
+ const wordBoundary = Math.max(prefix.lastIndexOf(" "), prefix.lastIndexOf("-"), prefix.lastIndexOf("/"));
40
+ if (wordBoundary >= minimumBoundary)
41
+ return `${prefix.slice(0, wordBoundary).trimEnd()}…`;
42
+ }
43
+ return `${prefix.trimEnd()}…`;
44
+ }
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { basename, dirname, join, resolve, sep } from "node:path";
5
5
  import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
6
6
  import { userRuntimePaths } from "../runtime/paths.js";
7
- import { compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.js";
7
+ import { compactKnowledgeSummary, compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.js";
8
8
  const STORAGE = KNOWLEDGE_RUNTIME_DEFAULTS.storage;
9
9
  const SAFE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
10
10
  const SAFE_GENERATION_RE = /^[0-9TZ-]+-[a-f0-9]{12}$/u;
@@ -117,6 +117,8 @@ function normalizeCandidate(value, projectKey) {
117
117
  if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
118
118
  || typeof raw.body !== "string" || typeof raw.confidence !== "number")
119
119
  return undefined;
120
+ if (raw.durability !== "stable")
121
+ return undefined;
120
122
  if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence)
121
123
  return undefined;
122
124
  const explicitUserDirective = raw.explicitUserDirective === true;
@@ -130,12 +132,12 @@ function normalizeCandidate(value, projectKey) {
130
132
  && (explicitUserDirective || raw.confidence >= 0.9);
131
133
  return {
132
134
  key: compactKnowledgeText(raw.key, 160), title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
133
- summary: compactKnowledgeText(raw.summary, STORAGE.maxSummaryChars),
135
+ summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
134
136
  keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
135
137
  scope, body, evidence, confidence: Math.min(1, raw.confidence),
136
138
  storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
137
139
  action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
138
- explicitUserDirective,
140
+ explicitUserDirective, durability: "stable",
139
141
  };
140
142
  }
141
143
  function renderKnowledgeFile(candidate) {
@@ -30,9 +30,9 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
30
30
  intervalMs: 60_000,
31
31
  idleMs: 60_000,
32
32
  capabilityPollMs: 5_000,
33
- modelTimeoutMs: 120_000,
34
- maxDeltaChars: 30_000,
35
- maxCandidates: 8,
33
+ modelTimeoutMs: 180_000,
34
+ maxDeltaChars: 20_000,
35
+ maxCandidates: 5,
36
36
  minimumConfidence: 0.72,
37
37
  }),
38
38
  storage: Object.freeze({
@@ -40,6 +40,7 @@ export function userRuntimePaths(home = homedir()) {
40
40
  // Unix-domain socket paths are short on purpose (macOS caps them at roughly 104 bytes).
41
41
  knowledgeCoordinatorSocket: join(root, "knowledge-v3.sock"),
42
42
  knowledgeLeader: join(knowledge, "runtime", "leader.json"),
43
+ knowledgeStatus: join(knowledge, "runtime", "status.json"),
43
44
  knowledgeCommitLock: join(knowledge, "runtime", "commit.lock"),
44
45
  knowledgeElectionLock: join(knowledge, "runtime", "election.lock"),
45
46
  };
@@ -94,21 +94,15 @@ function workerEntryUrl(): URL {
94
94
  }
95
95
 
96
96
  async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review_request" }>): Promise<void> {
97
- const ctx = runtime.context;
98
- if (!ctx?.model || !ctx.modelRegistry.hasConfiguredAuth(ctx.model)) {
99
- post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, error: "no-configured-model" });
100
- updateCapability();
101
- return;
102
- }
103
- if (!ctx.isIdle() || ctx.hasPendingMessages()) {
104
- post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, error: "model-executor-is-busy" });
105
- return;
106
- }
107
- const controller = new AbortController();
108
- runtime.activeReview = { requestId: message.requestId, leaderToken: message.leaderToken, controller };
109
- const timeout = setTimeout(() => controller.abort(), KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs);
110
- timeout.unref();
97
+ let timeout: NodeJS.Timeout | undefined;
111
98
  try {
99
+ const ctx = runtime.context;
100
+ if (!ctx?.model || !ctx.modelRegistry.hasConfiguredAuth(ctx.model)) throw new Error("no-configured-model");
101
+ if (!ctx.isIdle() || ctx.hasPendingMessages()) throw new Error("model-executor-is-busy");
102
+ const controller = new AbortController();
103
+ runtime.activeReview = { requestId: message.requestId, leaderToken: message.leaderToken, controller };
104
+ timeout = setTimeout(() => controller.abort(), KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs);
105
+ timeout.unref();
112
106
  const response = await ctx.modelRegistry.complete(
113
107
  ctx.model,
114
108
  {
@@ -130,6 +124,7 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
130
124
  type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId,
131
125
  error: error instanceof Error ? error.message : String(error),
132
126
  });
127
+ try { updateCapability(); } catch { /* The worker lease will recover even if the extension context changed. */ }
133
128
  } finally {
134
129
  clearTimeout(timeout);
135
130
  if (runtime.activeReview?.requestId === message.requestId) runtime.activeReview = undefined;
@@ -9,10 +9,12 @@ Persist only knowledge that is likely to be useful in future sessions:
9
9
  - verified engineering rules, successful procedures, or expensive failed approaches;
10
10
  - reusable architecture, testing, debugging, deployment, or operational knowledge.
11
11
  Do not persist task summaries, guesses, temporary IDs, credentials, secrets, private data, or facts recoverable by simply reading the repository.
12
+ Do not persist mutable inventory such as current cloud resources, names, availability, status, timestamps, or account snapshots. Preserve the discovery method or selection rule instead. If useful procedure is mixed with volatile facts, remove the volatile facts.
12
13
  Use storageHint "rule" only for short, precise, high-value instructions. Use "topic" for multi-step SOPs and detailed experience.
13
14
  Use action "revise" only when the delta explicitly corrects prior knowledge; otherwise use "add" or "reinforce".
14
15
  Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
15
- 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}]}
16
+ Each summary must be one complete sentence of at most 160 characters. Keep each body under 3000 characters. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered.
17
+ Return: {"candidates":[{"key":"stable semantic key","title":"...","summary":"one complete sentence","keywords":["..."],"scope":"global|project","body":"markdown body","evidence":["verified evidence"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|revise","explicitUserDirective":false,"durability":"stable"}]}
16
18
  Return {"candidates":[]} when nothing meets the threshold.`;
17
19
 
18
20
  export function buildKnowledgeExtractionPrompt(projectRoot: string, delta: string): string {
@@ -25,9 +27,58 @@ export function parseCandidateEnvelope(text: string): unknown[] {
25
27
  const start = unfenced.indexOf("{");
26
28
  const end = unfenced.lastIndexOf("}");
27
29
  if (start < 0 || end <= start) throw new Error("Knowledge reviewer did not return a JSON object");
28
- const parsed = JSON.parse(unfenced.slice(start, end + 1)) as { candidates?: unknown };
29
- if (!Array.isArray(parsed.candidates)) throw new Error("Knowledge reviewer response is missing candidates[]");
30
- return parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates);
30
+ let strictError: unknown;
31
+ try {
32
+ const parsed = JSON.parse(unfenced.slice(start, end + 1)) as { candidates?: unknown };
33
+ if (!Array.isArray(parsed.candidates)) throw new Error("Knowledge reviewer response is missing candidates[]");
34
+ return parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates);
35
+ } catch (error) {
36
+ strictError = error;
37
+ }
38
+
39
+ const arrayKey = /["']candidates["']\s*:/gu.exec(unfenced);
40
+ const arrayStart = arrayKey ? unfenced.indexOf("[", arrayKey.index + arrayKey[0].length) : -1;
41
+ if (arrayStart >= 0) {
42
+ const candidates: unknown[] = [];
43
+ let inString = false;
44
+ let escaped = false;
45
+ for (let index = arrayStart + 1; index < unfenced.length && candidates.length < KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates; index++) {
46
+ const character = unfenced[index];
47
+ if (inString) {
48
+ if (escaped) escaped = false;
49
+ else if (character === "\\") escaped = true;
50
+ else if (character === '"') inString = false;
51
+ continue;
52
+ }
53
+ if (character === '"') { inString = true; continue; }
54
+ if (character !== "{") continue;
55
+ const objectEnd = balancedObjectEnd(unfenced, index);
56
+ if (objectEnd < 0) break;
57
+ try { candidates.push(JSON.parse(unfenced.slice(index, objectEnd + 1))); } catch { /* Salvage other complete candidates. */ }
58
+ index = objectEnd;
59
+ }
60
+ if (candidates.length > 0) return candidates;
61
+ }
62
+ throw strictError;
63
+ }
64
+
65
+ function balancedObjectEnd(value: string, start: number): number {
66
+ let depth = 0;
67
+ let inString = false;
68
+ let escaped = false;
69
+ for (let index = start; index < value.length; index++) {
70
+ const character = value[index];
71
+ if (inString) {
72
+ if (escaped) escaped = false;
73
+ else if (character === "\\") escaped = true;
74
+ else if (character === '"') inString = false;
75
+ continue;
76
+ }
77
+ if (character === '"') { inString = true; continue; }
78
+ if (character === "{") depth++;
79
+ else if (character === "}" && --depth === 0) return index;
80
+ }
81
+ return -1;
31
82
  }
32
83
 
33
84
  export function knowledgeDeltaDigest(delta: string): string {
@@ -0,0 +1,55 @@
1
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ import { userRuntimePaths } from "../runtime/paths.ts";
5
+ import type { PersistKnowledgeResult } from "./types.ts";
6
+
7
+ export interface KnowledgeReviewStatus {
8
+ version: 1;
9
+ protocolVersion: 3;
10
+ state: "leader" | "stepped-down";
11
+ pid: number;
12
+ leaderToken: string;
13
+ electedAt: string;
14
+ updatedAt: string;
15
+ stepDownReason?: string;
16
+ lastScanAt?: string;
17
+ lastScanError?: { at: string; error: string };
18
+ activeReview?: {
19
+ reviewKey: string;
20
+ sessionKey: string;
21
+ sessionFileHash: string;
22
+ projectKey: string;
23
+ attempt: number;
24
+ startedAt: string;
25
+ };
26
+ lastReview?: {
27
+ reviewKey: string;
28
+ sessionKey: string;
29
+ attempt: number;
30
+ outcome: "saved" | "failed";
31
+ completedAt: string;
32
+ durationMs: number;
33
+ generationId?: string;
34
+ result?: PersistKnowledgeResult;
35
+ error?: string;
36
+ };
37
+ }
38
+
39
+ export function writeKnowledgeReviewStatus(status: KnowledgeReviewStatus, home: string): void {
40
+ const path = userRuntimePaths(home).knowledgeStatus;
41
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
42
+ const temporary = `${path}.${process.pid}.tmp`;
43
+ writeFileSync(temporary, `${JSON.stringify(status, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
44
+ renameSync(temporary, path);
45
+ chmodSync(path, 0o600);
46
+ }
47
+
48
+ export function readKnowledgeReviewStatus(home: string): KnowledgeReviewStatus | undefined {
49
+ try {
50
+ const parsed = JSON.parse(readFileSync(userRuntimePaths(home).knowledgeStatus, "utf8")) as KnowledgeReviewStatus;
51
+ return parsed.version === 1 && parsed.protocolVersion === 3 ? parsed : undefined;
52
+ } catch {
53
+ return undefined;
54
+ }
55
+ }
@@ -7,9 +7,11 @@ import { parentPort, workerData } from "node:worker_threads";
7
7
  import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
8
8
  import { userRuntimePaths } from "../runtime/paths.ts";
9
9
  import { parseCandidateEnvelope } from "./extractor.ts";
10
+ import { type KnowledgeReviewStatus, writeKnowledgeReviewStatus } from "./review-status.ts";
11
+ import { sanitizeKnowledgeText } from "./sanitize.ts";
10
12
  import { findNextReviewTask } from "./session-scanner.ts";
11
13
  import { commitKnowledgeReview, ensureKnowledgeDirectories, loadCurrentManifest } from "./store.ts";
12
- import type { KnowledgeReviewTask } from "./types.ts";
14
+ import type { KnowledgeReviewTask, PersistKnowledgeResult } from "./types.ts";
13
15
  import type { CoordinatorBroadcast, KnowledgeWorkerInput, KnowledgeWorkerOutput } from "./worker-protocol.ts";
14
16
 
15
17
  if (!parentPort) throw new Error("Knowledge review worker requires a parent port");
@@ -25,6 +27,11 @@ let leaderToken = "";
25
27
  let standbySocket: Socket | undefined;
26
28
  let electionPending = false;
27
29
  let activeTask: KnowledgeReviewTask | undefined;
30
+ let activeAttempt = 0;
31
+ let activeStartedAt = 0;
32
+ let activeDeadlineTimer: NodeJS.Timeout | undefined;
33
+ let reviewStatus: KnowledgeReviewStatus | undefined;
34
+ const reviewAttempts = new Map<string, number>();
28
35
 
29
36
  function send(message: KnowledgeWorkerOutput): void {
30
37
  parentPort!.postMessage(message);
@@ -36,13 +43,76 @@ function coordinatorPath(): string {
36
43
  : paths.knowledgeCoordinatorSocket;
37
44
  }
38
45
 
39
- function writeLeaderMetadata(): void {
46
+ function writeLeaderMetadata(electedAt: string): void {
40
47
  writeFileSync(paths.knowledgeLeader, `${JSON.stringify({
41
- protocolVersion: 3, leaderToken, pid: process.pid, electedAt: new Date().toISOString(),
48
+ protocolVersion: 3, leaderToken, pid: process.pid, electedAt,
42
49
  }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
43
50
  chmodSync(paths.knowledgeLeader, 0o600);
44
51
  }
45
52
 
53
+ function publishReviewStatus(): void {
54
+ if (!reviewStatus) return;
55
+ reviewStatus.updatedAt = new Date().toISOString();
56
+ try { writeKnowledgeReviewStatus(reviewStatus, home); } catch (error) {
57
+ send({ type: "review_failed", error: `Unable to write knowledge status: ${error instanceof Error ? error.message : String(error)}` });
58
+ }
59
+ }
60
+
61
+ function boundedError(error: unknown): string {
62
+ return sanitizeKnowledgeText(error instanceof Error ? error.message : String(error)).slice(0, 1_000);
63
+ }
64
+
65
+ function beginReview(task: KnowledgeReviewTask): void {
66
+ activeAttempt = (reviewAttempts.get(task.reviewKey) ?? 0) + 1;
67
+ reviewAttempts.set(task.reviewKey, activeAttempt);
68
+ activeStartedAt = Date.now();
69
+ clearTimeout(activeDeadlineTimer);
70
+ activeDeadlineTimer = setTimeout(() => {
71
+ if (activeTask?.requestId !== task.requestId) return;
72
+ const error = "knowledge-review-result-timeout";
73
+ finishReview(task, "failed", { error });
74
+ activeTask = undefined;
75
+ send({ type: "review_cancel", requestId: task.requestId, reason: error });
76
+ send({ type: "review_failed", requestId: task.requestId, error });
77
+ }, KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs + 5_000);
78
+ activeDeadlineTimer.unref();
79
+ if (reviewStatus) {
80
+ reviewStatus.activeReview = {
81
+ reviewKey: task.reviewKey,
82
+ sessionKey: task.sessionKey,
83
+ sessionFileHash: task.sessionFileHash,
84
+ projectKey: task.projectKey,
85
+ attempt: activeAttempt,
86
+ startedAt: new Date(activeStartedAt).toISOString(),
87
+ };
88
+ publishReviewStatus();
89
+ }
90
+ }
91
+
92
+ function finishReview(
93
+ task: KnowledgeReviewTask,
94
+ outcome: "saved" | "failed",
95
+ detail: { generationId?: string; result?: PersistKnowledgeResult; error?: unknown },
96
+ ): void {
97
+ clearTimeout(activeDeadlineTimer);
98
+ activeDeadlineTimer = undefined;
99
+ if (!reviewStatus) return;
100
+ reviewStatus.activeReview = undefined;
101
+ reviewStatus.lastReview = {
102
+ reviewKey: task.reviewKey,
103
+ sessionKey: task.sessionKey,
104
+ attempt: activeAttempt,
105
+ outcome,
106
+ completedAt: new Date().toISOString(),
107
+ durationMs: Math.max(0, Date.now() - activeStartedAt),
108
+ generationId: detail.generationId,
109
+ result: detail.result,
110
+ error: detail.error === undefined ? undefined : boundedError(detail.error),
111
+ };
112
+ if (outcome === "saved") reviewAttempts.delete(task.reviewKey);
113
+ publishReviewStatus();
114
+ }
115
+
46
116
  function removeLeaderMetadata(token: string): void {
47
117
  try {
48
118
  const current = JSON.parse(readFileSync(paths.knowledgeLeader, "utf8")) as { leaderToken?: string };
@@ -69,7 +139,17 @@ function scheduleElection(delay = Math.floor(Math.random() * 500)): void {
69
139
 
70
140
  function stepDown(reason: string): void {
71
141
  if (activeTask) send({ type: "review_cancel", requestId: activeTask.requestId, reason });
142
+ if (reviewStatus) {
143
+ reviewStatus.state = "stepped-down";
144
+ reviewStatus.stepDownReason = reason;
145
+ reviewStatus.activeReview = undefined;
146
+ publishReviewStatus();
147
+ }
72
148
  activeTask = undefined;
149
+ clearTimeout(activeDeadlineTimer);
150
+ activeDeadlineTimer = undefined;
151
+ activeAttempt = 0;
152
+ activeStartedAt = 0;
73
153
  const token = leaderToken;
74
154
  leaderToken = "";
75
155
  for (const socket of standbySockets) socket.destroy();
@@ -83,6 +163,7 @@ function stepDown(reason: string): void {
83
163
  }
84
164
  }
85
165
  if (token) removeLeaderMetadata(token);
166
+ reviewStatus = undefined;
86
167
  }
87
168
 
88
169
  function becomeStandby(socket: Socket): void {
@@ -147,6 +228,7 @@ function connectToLeader(probe = 0): void {
147
228
  function becomeLeader(server: Server): void {
148
229
  leaderServer = server;
149
230
  leaderToken = randomUUID();
231
+ const electedAt = new Date().toISOString();
150
232
  server.unref();
151
233
  server.on("connection", (socket) => {
152
234
  standbySockets.add(socket);
@@ -160,7 +242,11 @@ function becomeLeader(server: Server): void {
160
242
  stepDown("coordinator-server-failed");
161
243
  scheduleElection();
162
244
  });
163
- writeLeaderMetadata();
245
+ writeLeaderMetadata(electedAt);
246
+ reviewStatus = {
247
+ version: 1, protocolVersion: 3, state: "leader", pid: process.pid, leaderToken, electedAt, updatedAt: electedAt,
248
+ };
249
+ publishReviewStatus();
164
250
  send({ type: "leadership", state: "leader", leaderToken });
165
251
  void scanForReview();
166
252
  }
@@ -186,14 +272,21 @@ function attemptElection(): void {
186
272
 
187
273
  async function scanForReview(): Promise<void> {
188
274
  if (!leaderServer || !leaderToken || activeTask || !sessionsRoot || !eligible) return;
275
+ if (reviewStatus) {
276
+ reviewStatus.lastScanAt = new Date().toISOString();
277
+ reviewStatus.lastScanError = undefined;
278
+ publishReviewStatus();
279
+ }
189
280
  try {
190
281
  const manifest = loadCurrentManifest(home);
191
282
  const task = findNextReviewTask(sessionsRoot, manifest.reviews);
192
283
  if (!task) return;
193
284
  activeTask = task;
285
+ beginReview(task);
194
286
  if (!task.delta) {
195
287
  const result = commitKnowledgeReview([], task, leaderToken, home);
196
288
  const generationId = loadCurrentManifest(home).generationId;
289
+ finishReview(task, "saved", { generationId, result });
197
290
  send({ type: "review_saved", requestId: task.requestId, generationId, result });
198
291
  if (generationId) {
199
292
  send({ type: "generation_changed", generationId });
@@ -205,8 +298,13 @@ async function scanForReview(): Promise<void> {
205
298
  }
206
299
  send({ type: "review_request", leaderToken, requestId: task.requestId, task });
207
300
  } catch (error) {
301
+ if (activeTask) finishReview(activeTask, "failed", { error });
302
+ else if (reviewStatus) {
303
+ reviewStatus.lastScanError = { at: new Date().toISOString(), error: boundedError(error) };
304
+ publishReviewStatus();
305
+ }
208
306
  activeTask = undefined;
209
- send({ type: "review_failed", error: error instanceof Error ? error.message : String(error) });
307
+ send({ type: "review_failed", error: boundedError(error) });
210
308
  }
211
309
  }
212
310
 
@@ -218,6 +316,7 @@ function handleReviewResult(message: Extract<KnowledgeWorkerInput, { type: "revi
218
316
  if (message.error || message.raw === undefined) throw new Error(message.error || "Knowledge review returned no content");
219
317
  const result = commitKnowledgeReview(parseCandidateEnvelope(message.raw), task, leaderToken, home);
220
318
  const generationId = loadCurrentManifest(home).generationId;
319
+ finishReview(task, "saved", { generationId, result });
221
320
  send({ type: "review_saved", requestId: task.requestId, generationId, result });
222
321
  if (generationId) {
223
322
  send({ type: "generation_changed", generationId });
@@ -225,7 +324,8 @@ function handleReviewResult(message: Extract<KnowledgeWorkerInput, { type: "revi
225
324
  }
226
325
  committed = true;
227
326
  } catch (error) {
228
- send({ type: "review_failed", requestId: task.requestId, error: error instanceof Error ? error.message : String(error) });
327
+ finishReview(task, "failed", { error });
328
+ send({ type: "review_failed", requestId: task.requestId, error: boundedError(error) });
229
329
  } finally {
230
330
  activeTask = undefined;
231
331
  if (committed) queueMicrotask(() => void scanForReview());
@@ -24,3 +24,23 @@ export function compactKnowledgeText(value: string, maxChars: number): string {
24
24
  .trim()
25
25
  .slice(0, maxChars);
26
26
  }
27
+
28
+ export function compactKnowledgeSummary(value: string, maxChars: number): string {
29
+ const normalized = sanitizeKnowledgeText(value).replace(/\s+/gu, " ").trim();
30
+ if (normalized.length <= maxChars || maxChars <= 0) return normalized.slice(0, Math.max(0, maxChars));
31
+ if (maxChars === 1) return "…";
32
+ const limit = maxChars - 1;
33
+ const prefix = normalized.slice(0, limit);
34
+ const minimumBoundary = Math.floor(limit * 0.6);
35
+ const sentenceEnd = Math.max(
36
+ prefix.lastIndexOf("。"), prefix.lastIndexOf("!"), prefix.lastIndexOf("?"),
37
+ prefix.lastIndexOf("."), prefix.lastIndexOf("!"), prefix.lastIndexOf("?"),
38
+ );
39
+ if (sentenceEnd >= minimumBoundary) return prefix.slice(0, sentenceEnd + 1).trimEnd();
40
+ const next = normalized.charAt(limit);
41
+ if (/[A-Za-z0-9]/u.test(prefix.at(-1) ?? "") && /[A-Za-z0-9]/u.test(next)) {
42
+ const wordBoundary = Math.max(prefix.lastIndexOf(" "), prefix.lastIndexOf("-"), prefix.lastIndexOf("/"));
43
+ if (wordBoundary >= minimumBoundary) return `${prefix.slice(0, wordBoundary).trimEnd()}…`;
44
+ }
45
+ return `${prefix.trimEnd()}…`;
46
+ }
@@ -8,7 +8,7 @@ import { basename, dirname, join, resolve, sep } from "node:path";
8
8
 
9
9
  import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
10
10
  import { userRuntimePaths } from "../runtime/paths.ts";
11
- import { compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.ts";
11
+ import { compactKnowledgeSummary, compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.ts";
12
12
  import type {
13
13
  KnowledgeCandidate, KnowledgeCatalog, KnowledgeCatalogEntry, KnowledgeManifest, KnowledgeReviewCursor,
14
14
  KnowledgeReviewTask, KnowledgeScope, KnowledgeSnapshot, KnowledgeTrack, PersistKnowledgeResult,
@@ -132,6 +132,7 @@ function normalizeCandidate(value: unknown, projectKey: string): KnowledgeCandid
132
132
  const raw = value as Record<string, unknown>;
133
133
  if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
134
134
  || typeof raw.body !== "string" || typeof raw.confidence !== "number") return undefined;
135
+ if (raw.durability !== "stable") return undefined;
135
136
  if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence) return undefined;
136
137
  const explicitUserDirective = raw.explicitUserDirective === true;
137
138
  const scope: KnowledgeScope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${projectKey}`;
@@ -143,12 +144,12 @@ function normalizeCandidate(value: unknown, projectKey: string): KnowledgeCandid
143
144
  && (explicitUserDirective || raw.confidence >= 0.9);
144
145
  return {
145
146
  key: compactKnowledgeText(raw.key, 160), title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
146
- summary: compactKnowledgeText(raw.summary, STORAGE.maxSummaryChars),
147
+ summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
147
148
  keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
148
149
  scope, body, evidence, confidence: Math.min(1, raw.confidence),
149
150
  storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
150
151
  action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
151
- explicitUserDirective,
152
+ explicitUserDirective, durability: "stable",
152
153
  };
153
154
  }
154
155
 
@@ -1,6 +1,7 @@
1
1
  export type KnowledgeTrack = "rule" | "topic";
2
2
  export type KnowledgeScope = "global" | `project:${string}`;
3
3
  export type KnowledgeAction = "add" | "reinforce" | "revise";
4
+ export type KnowledgeDurability = "stable";
4
5
 
5
6
  export interface KnowledgeCandidate {
6
7
  key: string;
@@ -14,6 +15,7 @@ export interface KnowledgeCandidate {
14
15
  storageHint: KnowledgeTrack;
15
16
  action: KnowledgeAction;
16
17
  explicitUserDirective: boolean;
18
+ durability: KnowledgeDurability;
17
19
  }
18
20
 
19
21
  export interface KnowledgeCatalogEntry {
@@ -31,9 +31,9 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
31
31
  intervalMs: 60_000,
32
32
  idleMs: 60_000,
33
33
  capabilityPollMs: 5_000,
34
- modelTimeoutMs: 120_000,
35
- maxDeltaChars: 30_000,
36
- maxCandidates: 8,
34
+ modelTimeoutMs: 180_000,
35
+ maxDeltaChars: 20_000,
36
+ maxCandidates: 5,
37
37
  minimumConfidence: 0.72,
38
38
  }),
39
39
  storage: Object.freeze({
@@ -34,6 +34,7 @@ export interface UserRuntimePaths {
34
34
  knowledgeRuntime: string;
35
35
  knowledgeCoordinatorSocket: string;
36
36
  knowledgeLeader: string;
37
+ knowledgeStatus: string;
37
38
  knowledgeCommitLock: string;
38
39
  knowledgeElectionLock: string;
39
40
  }
@@ -68,6 +69,7 @@ export function userRuntimePaths(home = homedir()): UserRuntimePaths {
68
69
  // Unix-domain socket paths are short on purpose (macOS caps them at roughly 104 bytes).
69
70
  knowledgeCoordinatorSocket: join(root, "knowledge-v3.sock"),
70
71
  knowledgeLeader: join(knowledge, "runtime", "leader.json"),
72
+ knowledgeStatus: join(knowledge, "runtime", "status.json"),
71
73
  knowledgeCommitLock: join(knowledge, "runtime", "commit.lock"),
72
74
  knowledgeElectionLock: join(knowledge, "runtime", "election.lock"),
73
75
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadooppei/hwcode",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "A customizable terminal coding agent with local-model support and HWCode workflows.",
5
5
  "type": "module",
6
6
  "bin": {