@hadooppei/hwcode 1.0.11 → 1.0.13
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 +17 -6
- package/.pi/dist/lib/knowledge/review-worker.js +30 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +62 -13
- package/.pi/dist/lib/knowledge/store.js +128 -22
- package/.pi/dist/lib/runtime/defaults.js +8 -4
- package/.pi/dist/lib/runtime/session-state.js +9 -0
- package/.pi/dist/lib/workflows/state.js +159 -0
- package/.pi/dist/lib/working-directory.js +170 -0
- package/.pi/extensions/knowledge.ts +58 -10
- package/.pi/lib/knowledge/extractor.ts +31 -6
- package/.pi/lib/knowledge/review-status.ts +6 -0
- package/.pi/lib/knowledge/review-worker.ts +25 -0
- package/.pi/lib/knowledge/session-scanner.ts +57 -12
- package/.pi/lib/knowledge/store.ts +126 -16
- package/.pi/lib/knowledge/types.ts +11 -0
- package/.pi/lib/knowledge/worker-protocol.ts +1 -0
- package/.pi/lib/runtime/defaults.ts +8 -4
- package/.pi/lib/working-directory.ts +10 -0
- package/package.json +1 -1
|
@@ -8,14 +8,25 @@ Persist only knowledge that is likely to be useful in future sessions:
|
|
|
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
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.
|
|
11
|
-
|
|
12
|
-
Use
|
|
11
|
+
Treat failed, unsupported, invalid, or corrected operations as negative evidence. Never present an operation as valid when the supplied evidence says it failed; use only a verified alternative or record an explicit warning. Never invent commands or parameters absent from successful evidence.
|
|
12
|
+
Use storageHint "rule" only for one short imperative instruction of at most 240 characters. A rule must not contain incident narration, example resource names, IDs, timestamps, or evidence details. Use "topic" for multi-step SOPs and detailed experience.
|
|
13
|
+
Relevant existing knowledge may be supplied. If a candidate has the same intent as an existing item, do not add a translated, renamed, or reformatted duplicate. Set targetId to that exact existing ID and action to "reinforce" when the existing content remains correct, or "revise" only when the delta explicitly proves a correction. Use targetId null only for genuinely new knowledge.
|
|
13
14
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
The prior validation context is only for checking facts and contradictions; do not persist it again unless the new delta independently reinforces it. Prefer accumulated workflow successfulSteps and failedApproaches over assistant narration.
|
|
16
|
+
Each summary must be one complete sentence of at most 160 characters. Keep each topic body under 2200 characters. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered. Return at most 3 candidates.
|
|
17
|
+
Return: {"candidates":[{"key":"stable semantic key","targetId":"existing-id or null","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
|
-
export function buildKnowledgeExtractionPrompt(projectRoot, delta) {
|
|
18
|
-
|
|
19
|
+
export function buildKnowledgeExtractionPrompt(projectRoot, delta, context = "", existing = []) {
|
|
20
|
+
const related = existing.length > 0 ? JSON.stringify(existing) : "[]";
|
|
21
|
+
return [
|
|
22
|
+
`Project root: ${projectRoot}`,
|
|
23
|
+
"",
|
|
24
|
+
"<relevant_existing_knowledge>", related, "</relevant_existing_knowledge>",
|
|
25
|
+
"",
|
|
26
|
+
"<prior_validation_context>", context, "</prior_validation_context>",
|
|
27
|
+
"",
|
|
28
|
+
"<conversation_delta>", delta, "</conversation_delta>",
|
|
29
|
+
].join("\n");
|
|
19
30
|
}
|
|
20
31
|
export function parseCandidateEnvelope(text) {
|
|
21
32
|
const trimmed = text.trim();
|
|
@@ -104,6 +104,25 @@ function finishReview(task, outcome, detail) {
|
|
|
104
104
|
reviewAttempts.delete(task.reviewKey);
|
|
105
105
|
publishReviewStatus();
|
|
106
106
|
}
|
|
107
|
+
function deferReview(task, reason) {
|
|
108
|
+
clearTimeout(activeDeadlineTimer);
|
|
109
|
+
activeDeadlineTimer = undefined;
|
|
110
|
+
const attempts = reviewAttempts.get(task.reviewKey) ?? 1;
|
|
111
|
+
if (attempts <= 1)
|
|
112
|
+
reviewAttempts.delete(task.reviewKey);
|
|
113
|
+
else
|
|
114
|
+
reviewAttempts.set(task.reviewKey, attempts - 1);
|
|
115
|
+
if (!reviewStatus)
|
|
116
|
+
return;
|
|
117
|
+
reviewStatus.activeReview = undefined;
|
|
118
|
+
reviewStatus.lastDeferred = {
|
|
119
|
+
reviewKey: task.reviewKey,
|
|
120
|
+
sessionKey: task.sessionKey,
|
|
121
|
+
deferredAt: new Date().toISOString(),
|
|
122
|
+
reason: boundedError(reason),
|
|
123
|
+
};
|
|
124
|
+
publishReviewStatus();
|
|
125
|
+
}
|
|
107
126
|
function removeLeaderMetadata(token) {
|
|
108
127
|
try {
|
|
109
128
|
const current = JSON.parse(readFileSync(paths.knowledgeLeader, "utf8"));
|
|
@@ -355,6 +374,13 @@ function handleReviewResult(message) {
|
|
|
355
374
|
queueMicrotask(() => void scanForReview());
|
|
356
375
|
}
|
|
357
376
|
}
|
|
377
|
+
function handleReviewDeferred(message) {
|
|
378
|
+
const task = activeTask;
|
|
379
|
+
if (!task || message.requestId !== task.requestId || message.leaderToken !== leaderToken || !leaderServer)
|
|
380
|
+
return;
|
|
381
|
+
deferReview(task, message.reason);
|
|
382
|
+
activeTask = undefined;
|
|
383
|
+
}
|
|
358
384
|
const scanTimer = setInterval(() => { void scanForReview(); }, KNOWLEDGE_RUNTIME_DEFAULTS.review.intervalMs);
|
|
359
385
|
scanTimer.unref();
|
|
360
386
|
parentPort.on("message", (message) => {
|
|
@@ -373,6 +399,10 @@ parentPort.on("message", (message) => {
|
|
|
373
399
|
scheduleElection(0);
|
|
374
400
|
return;
|
|
375
401
|
}
|
|
402
|
+
if (message.type === "review_deferred") {
|
|
403
|
+
handleReviewDeferred(message);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
376
406
|
if (message.type === "review_result") {
|
|
377
407
|
handleReviewResult(message);
|
|
378
408
|
return;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { resolve, sep } from "node:path";
|
|
4
4
|
import { buildContextEntries, parseSessionEntries, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
6
|
+
import { findLatestWorkflowRoot } from "../working-directory.js";
|
|
6
7
|
import { knowledgeDeltaDigest } from "./extractor.js";
|
|
7
8
|
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
8
9
|
import { projectKnowledgeKey } from "./store.js";
|
|
10
|
+
const FAILURE_EVIDENCE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|mistyp(?:e|ed)|typo)\b|not supported|失败|错误|不支持|无效)/iu;
|
|
11
|
+
const INJECTED_SKILL_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
9
12
|
function hash(value, length = 64) {
|
|
10
13
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
11
14
|
}
|
|
@@ -23,11 +26,11 @@ function textContent(content) {
|
|
|
23
26
|
}
|
|
24
27
|
function reviewPiece(entry, index) {
|
|
25
28
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
26
|
-
return { index, priority:
|
|
29
|
+
return { index, priority: 2, category: "summary", text: `[summary]\n${sanitizeKnowledgeText(entry.summary).slice(0, 6_000)}` };
|
|
27
30
|
}
|
|
28
31
|
if (entry.type === "custom" && entry.customType.startsWith("hwcode-workflow")) {
|
|
29
32
|
return {
|
|
30
|
-
index, priority:
|
|
33
|
+
index, priority: 1, category: "workflow",
|
|
31
34
|
text: `[workflow_state:${entry.customType}]\n${sanitizeKnowledgeText(JSON.stringify(entry.data ?? {})).slice(0, 6_000)}`,
|
|
32
35
|
};
|
|
33
36
|
}
|
|
@@ -35,10 +38,11 @@ function reviewPiece(entry, index) {
|
|
|
35
38
|
return undefined;
|
|
36
39
|
const message = entry.message;
|
|
37
40
|
const role = typeof message.role === "string" ? message.role : "message";
|
|
38
|
-
const content = sanitizeKnowledgeText(textContent(message.content));
|
|
41
|
+
const content = sanitizeKnowledgeText(textContent(message.content).replace(INJECTED_SKILL_RE, "[loaded skill omitted]"));
|
|
39
42
|
if (!content)
|
|
40
43
|
return undefined;
|
|
41
|
-
const priority = role === "user" ? 0 : role === "toolResult"
|
|
44
|
+
const priority = role === "user" ? 0 : role === "toolResult" && FAILURE_EVIDENCE_RE.test(content) ? 2
|
|
45
|
+
: role === "toolResult" ? 4 : 3;
|
|
42
46
|
const cap = role === "user" ? 8_000 : role === "toolResult" ? 1_500 : 4_000;
|
|
43
47
|
const category = role === "user" ? "user" : role === "toolResult" ? "tool" : "assistant";
|
|
44
48
|
return { index, priority, category, text: `[${role}]\n${content.slice(0, cap)}` };
|
|
@@ -50,27 +54,71 @@ function buildDelta(entries) {
|
|
|
50
54
|
user: 14_000, workflow: 6_000, summary: 4_000, assistant: 8_000, tool: 4_000,
|
|
51
55
|
};
|
|
52
56
|
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars;
|
|
53
|
-
for (const piece of pieces.slice().sort((left, right) =>
|
|
57
|
+
for (const piece of pieces.slice().sort((left, right) => {
|
|
58
|
+
if (left.priority !== right.priority)
|
|
59
|
+
return left.priority - right.priority;
|
|
60
|
+
if (left.category === right.category && left.category !== "user")
|
|
61
|
+
return right.index - left.index;
|
|
62
|
+
return left.index - right.index;
|
|
63
|
+
})) {
|
|
54
64
|
if (remaining <= 0)
|
|
55
65
|
break;
|
|
56
|
-
const
|
|
66
|
+
const separatorCharacters = selected.length > 0 ? 2 : 0;
|
|
67
|
+
if (remaining <= separatorCharacters)
|
|
68
|
+
break;
|
|
69
|
+
const text = piece.text.slice(0, Math.min(remaining - separatorCharacters, categoryRemaining[piece.category]));
|
|
57
70
|
if (text)
|
|
58
71
|
selected.push({ ...piece, text });
|
|
59
|
-
remaining -= text.length;
|
|
72
|
+
remaining -= text.length + separatorCharacters;
|
|
60
73
|
categoryRemaining[piece.category] -= text.length;
|
|
61
74
|
}
|
|
62
|
-
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
75
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
76
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars);
|
|
77
|
+
}
|
|
78
|
+
function buildValidationContext(entries) {
|
|
79
|
+
const pieces = entries.map(reviewPiece).filter((piece) => Boolean(piece));
|
|
80
|
+
const latestWorkflow = pieces.filter((piece) => piece.category === "workflow").at(-1);
|
|
81
|
+
const evidence = pieces.filter((piece) => ((piece.category === "tool" || piece.category === "assistant" || piece.category === "summary")
|
|
82
|
+
&& FAILURE_EVIDENCE_RE.test(piece.text)));
|
|
83
|
+
if (latestWorkflow && !evidence.includes(latestWorkflow))
|
|
84
|
+
evidence.push(latestWorkflow);
|
|
85
|
+
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars;
|
|
86
|
+
const selected = [];
|
|
87
|
+
for (const piece of evidence.slice().sort((left, right) => right.index - left.index)) {
|
|
88
|
+
if (remaining <= 0)
|
|
89
|
+
break;
|
|
90
|
+
const separatorCharacters = selected.length > 0 ? 2 : 0;
|
|
91
|
+
if (remaining <= separatorCharacters)
|
|
92
|
+
break;
|
|
93
|
+
const text = piece.text.slice(0, remaining - separatorCharacters);
|
|
94
|
+
if (text)
|
|
95
|
+
selected.push({ ...piece, text });
|
|
96
|
+
remaining -= text.length + separatorCharacters;
|
|
97
|
+
}
|
|
98
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
99
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars);
|
|
63
100
|
}
|
|
64
101
|
function projectRootForSession(header, branch) {
|
|
102
|
+
const sessionRoot = resolve(header.cwd);
|
|
103
|
+
const workflowRootValue = findLatestWorkflowRoot(branch);
|
|
104
|
+
const workflowRoot = workflowRootValue ? resolve(workflowRootValue) : undefined;
|
|
105
|
+
let workingDirectory = sessionRoot;
|
|
65
106
|
for (const entry of branch.slice().reverse()) {
|
|
66
107
|
if (entry.type !== "custom" || entry.customType !== "hwcode-working-directory"
|
|
67
108
|
|| !entry.data || typeof entry.data !== "object")
|
|
68
109
|
continue;
|
|
69
110
|
const cwd = entry.data.cwd;
|
|
70
|
-
if (typeof cwd
|
|
71
|
-
|
|
111
|
+
if (typeof cwd !== "string" || !cwd)
|
|
112
|
+
continue;
|
|
113
|
+
workingDirectory = resolve(cwd);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (workflowRoot && (workingDirectory === workflowRoot || workingDirectory.startsWith(`${workflowRoot}${sep}`))) {
|
|
117
|
+
return workflowRoot;
|
|
72
118
|
}
|
|
73
|
-
|
|
119
|
+
if (workingDirectory === sessionRoot || workingDirectory.startsWith(`${sessionRoot}${sep}`))
|
|
120
|
+
return sessionRoot;
|
|
121
|
+
return workingDirectory;
|
|
74
122
|
}
|
|
75
123
|
export function discoverSessionFiles(root) {
|
|
76
124
|
const files = [];
|
|
@@ -128,6 +176,7 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
128
176
|
if (pending.length === 0)
|
|
129
177
|
return undefined;
|
|
130
178
|
const delta = buildDelta(pending);
|
|
179
|
+
const context = cursorIndex >= 0 ? buildValidationContext(branch.slice(0, cursorIndex + 1)) : "";
|
|
131
180
|
const firstEntryId = pending[0].id;
|
|
132
181
|
const lastEntryId = pending[pending.length - 1].id;
|
|
133
182
|
const deltaDigest = knowledgeDeltaDigest(delta || `${firstEntryId}\0${lastEntryId}`);
|
|
@@ -138,7 +187,7 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
138
187
|
return {
|
|
139
188
|
requestId: randomUUID(), reviewKey, sessionId: header.id, sessionKey, sessionFileHash,
|
|
140
189
|
projectRoot, projectKey: projectKnowledgeKey(projectRoot),
|
|
141
|
-
firstEntryId, lastEntryId, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
190
|
+
firstEntryId, lastEntryId, context, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
142
191
|
};
|
|
143
192
|
}
|
|
144
193
|
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
@@ -101,6 +101,63 @@ function normalizeSlug(value) {
|
|
|
101
101
|
function normalizedFingerprint(candidate) {
|
|
102
102
|
return createHash("sha256").update(`${candidate.scope}\0${candidate.key.trim().toLowerCase()}`).digest("hex");
|
|
103
103
|
}
|
|
104
|
+
function comparisonTerms(value) {
|
|
105
|
+
return new Set((value.normalize("NFKC").toLowerCase().match(/\p{Script=Han}+|[\p{L}\p{N}]+/gu) ?? [])
|
|
106
|
+
.filter((term) => term.length >= 2));
|
|
107
|
+
}
|
|
108
|
+
function containment(left, right) {
|
|
109
|
+
const minimum = Math.min(left.size, right.size);
|
|
110
|
+
if (minimum === 0)
|
|
111
|
+
return 0;
|
|
112
|
+
let shared = 0;
|
|
113
|
+
for (const term of left)
|
|
114
|
+
if (right.has(term))
|
|
115
|
+
shared++;
|
|
116
|
+
return shared / minimum;
|
|
117
|
+
}
|
|
118
|
+
function semanticDuplicateScore(candidate, entry) {
|
|
119
|
+
if (candidate.scope !== entry.scope || candidate.storageHint !== entry.track)
|
|
120
|
+
return 0;
|
|
121
|
+
const titleScore = containment(comparisonTerms(candidate.title), comparisonTerms(entry.title));
|
|
122
|
+
const keywordScore = containment(comparisonTerms(candidate.keywords.join(" ")), comparisonTerms(entry.keywords.join(" ")));
|
|
123
|
+
if (titleScore < 0.8 || keywordScore < 0.5)
|
|
124
|
+
return 0;
|
|
125
|
+
return titleScore * 0.7 + keywordScore * 0.3;
|
|
126
|
+
}
|
|
127
|
+
function semanticDuplicateIndex(candidate, catalog) {
|
|
128
|
+
let bestIndex = -1;
|
|
129
|
+
let bestScore = 0;
|
|
130
|
+
for (const [index, entry] of catalog.items.entries()) {
|
|
131
|
+
const score = semanticDuplicateScore(candidate, entry);
|
|
132
|
+
if (score > bestScore) {
|
|
133
|
+
bestIndex = index;
|
|
134
|
+
bestScore = score;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return bestIndex;
|
|
138
|
+
}
|
|
139
|
+
function unsupportedOperations(value) {
|
|
140
|
+
return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)].map((match) => {
|
|
141
|
+
const suffix = value.slice(match.index + match[0].length, match.index + match[0].length + 500);
|
|
142
|
+
const service = /\bhcloud\s+([A-Za-z][A-Za-z0-9-]*)\s+--help\b/iu.exec(suffix)?.[1]?.toLowerCase();
|
|
143
|
+
return { operation: match[1].toLowerCase(), service };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function contradictsVerifiedFailures(body, validationText) {
|
|
147
|
+
const normalizedBody = body.toLowerCase();
|
|
148
|
+
for (const unsupported of unsupportedOperations(validationText)) {
|
|
149
|
+
const operationExpression = unsupported.service
|
|
150
|
+
? new RegExp(`\\b(?:hcloud\\s+)?${unsupported.service}\\b[^\\n]{0,120}\\b${unsupported.operation}\\b`, "u")
|
|
151
|
+
: new RegExp(`\\b${unsupported.operation}\\b`, "u");
|
|
152
|
+
const index = normalizedBody.search(operationExpression);
|
|
153
|
+
if (index < 0)
|
|
154
|
+
continue;
|
|
155
|
+
const context = normalizedBody.slice(Math.max(0, index - 80), index + 240);
|
|
156
|
+
if (!/(?:not supported|unsupported|do not|don't|avoid|不支持|不要|避免)/u.test(context))
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
104
161
|
function contentHash(candidate) {
|
|
105
162
|
return createHash("sha256").update(`${candidate.title}\0${candidate.summary}\0${candidate.body}`).digest("hex");
|
|
106
163
|
}
|
|
@@ -110,35 +167,60 @@ function asStringArray(value, maxItems) {
|
|
|
110
167
|
return [...new Set(value.filter((item) => typeof item === "string")
|
|
111
168
|
.map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
|
|
112
169
|
}
|
|
113
|
-
function normalizeCandidate(value, projectKey) {
|
|
170
|
+
function normalizeCandidate(value, projectKey, validationText) {
|
|
114
171
|
if (!value || typeof value !== "object")
|
|
115
|
-
return
|
|
172
|
+
return { reason: "invalid-schema" };
|
|
116
173
|
const raw = value;
|
|
117
174
|
if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
|
|
118
175
|
|| typeof raw.body !== "string" || typeof raw.confidence !== "number")
|
|
119
|
-
return
|
|
176
|
+
return { reason: "invalid-schema" };
|
|
120
177
|
if (raw.durability !== "stable")
|
|
121
|
-
return
|
|
178
|
+
return { reason: "unstable" };
|
|
122
179
|
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence)
|
|
123
|
-
return
|
|
180
|
+
return { reason: "low-confidence" };
|
|
181
|
+
if (raw.targetId !== undefined && raw.targetId !== null
|
|
182
|
+
&& (typeof raw.targetId !== "string" || !SAFE_ID_RE.test(raw.targetId)))
|
|
183
|
+
return { reason: "unknown-target" };
|
|
124
184
|
const explicitUserDirective = raw.explicitUserDirective === true;
|
|
125
185
|
const scope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${projectKey}`;
|
|
126
186
|
const body = compactKnowledgeText(raw.body, 20_000);
|
|
127
187
|
const evidence = asStringArray(raw.evidence, 8);
|
|
128
188
|
if (!body || evidence.length === 0)
|
|
129
|
-
return
|
|
189
|
+
return { reason: "missing-body-or-evidence" };
|
|
190
|
+
if (contradictsVerifiedFailures(body, validationText))
|
|
191
|
+
return { reason: "contradicts-verified-failure" };
|
|
130
192
|
const requestedTrack = raw.storageHint === "rule" ? "rule" : "topic";
|
|
131
193
|
const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
|
|
194
|
+
&& body.split(/\n\s*\n/gu).length === 1
|
|
132
195
|
&& (explicitUserDirective || raw.confidence >= 0.9);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
196
|
+
const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
|
|
197
|
+
return { candidate: {
|
|
198
|
+
key: compactKnowledgeText(raw.key, 160), targetId,
|
|
199
|
+
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
200
|
+
summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
|
|
201
|
+
keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
|
|
202
|
+
scope, body, evidence, confidence: Math.min(1, raw.confidence),
|
|
203
|
+
storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
|
|
204
|
+
action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
|
|
205
|
+
explicitUserDirective, durability: "stable",
|
|
206
|
+
} };
|
|
207
|
+
}
|
|
208
|
+
function recordSkip(result, reason) {
|
|
209
|
+
result.skipped++;
|
|
210
|
+
result.skippedReasons ??= {};
|
|
211
|
+
result.skippedReasons[reason] = (result.skippedReasons[reason] ?? 0) + 1;
|
|
212
|
+
}
|
|
213
|
+
function writeRejectedPending(directory, reason, value) {
|
|
214
|
+
let serialized;
|
|
215
|
+
try {
|
|
216
|
+
serialized = JSON.stringify(value);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
serialized = String(value);
|
|
220
|
+
}
|
|
221
|
+
const candidate = sanitizeKnowledgeText(serialized).slice(0, 10_000);
|
|
222
|
+
const name = `${Date.now()}-rejected-${reason}-${randomUUID().slice(0, 8)}.json`;
|
|
223
|
+
atomicWrite(join(directory, "pending", name), `${JSON.stringify({ reason, candidate }, null, 2)}\n`);
|
|
142
224
|
}
|
|
143
225
|
function renderKnowledgeFile(candidate) {
|
|
144
226
|
if (candidate.storageHint === "rule")
|
|
@@ -268,17 +350,35 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
268
350
|
copyCurrentContent(current, temporary, home);
|
|
269
351
|
const catalog = structuredClone(current.catalog);
|
|
270
352
|
const result = { saved: 0, updated: 0, pending: 0, skipped: 0 };
|
|
353
|
+
const rejected = [];
|
|
271
354
|
for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
|
|
272
|
-
const
|
|
273
|
-
if (!candidate) {
|
|
274
|
-
result.
|
|
355
|
+
const normalized = normalizeCandidate(value, task.projectKey, `${task.context ?? ""}\n${task.delta}`);
|
|
356
|
+
if (!("candidate" in normalized)) {
|
|
357
|
+
recordSkip(result, normalized.reason);
|
|
358
|
+
rejected.push({ reason: normalized.reason, value });
|
|
275
359
|
continue;
|
|
276
360
|
}
|
|
361
|
+
const candidate = normalized.candidate;
|
|
277
362
|
const fingerprint = normalizedFingerprint(candidate);
|
|
278
363
|
const hash = contentHash(candidate);
|
|
279
|
-
const
|
|
364
|
+
const targetIndex = candidate.targetId
|
|
365
|
+
? catalog.items.findIndex((item) => item.id === candidate.targetId && applicable(item, task.projectKey))
|
|
366
|
+
: -1;
|
|
367
|
+
if (candidate.targetId && targetIndex < 0) {
|
|
368
|
+
recordSkip(result, "unknown-target");
|
|
369
|
+
rejected.push({ reason: "unknown-target", value });
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
let existingIndex = targetIndex >= 0
|
|
373
|
+
? targetIndex
|
|
374
|
+
: catalog.items.findIndex((item) => item.fingerprint === fingerprint);
|
|
375
|
+
let semanticReinforcement = false;
|
|
376
|
+
if (existingIndex < 0) {
|
|
377
|
+
existingIndex = semanticDuplicateIndex(candidate, catalog);
|
|
378
|
+
semanticReinforcement = existingIndex >= 0;
|
|
379
|
+
}
|
|
280
380
|
const existing = existingIndex >= 0 ? catalog.items[existingIndex] : undefined;
|
|
281
|
-
if (existing
|
|
381
|
+
if (existing && (existing.contentHash === hash || candidate.action === "reinforce" || semanticReinforcement)) {
|
|
282
382
|
existing.evidenceCount += candidate.evidence.length;
|
|
283
383
|
existing.updatedAt = new Date().toISOString();
|
|
284
384
|
result.updated++;
|
|
@@ -301,8 +401,9 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
301
401
|
const relativeFile = `${track === "rule" ? "rules" : "topics"}/${id}.md`;
|
|
302
402
|
atomicWrite(join(temporary, relativeFile), renderKnowledgeFile(candidate));
|
|
303
403
|
const entry = {
|
|
304
|
-
id, fingerprint
|
|
305
|
-
|
|
404
|
+
id, fingerprint: existing?.fingerprint ?? fingerprint, contentHash: hash,
|
|
405
|
+
title: candidate.title, summary: candidate.summary,
|
|
406
|
+
keywords: candidate.keywords, scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
|
|
306
407
|
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
307
408
|
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
308
409
|
};
|
|
@@ -315,6 +416,11 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
315
416
|
result.saved++;
|
|
316
417
|
}
|
|
317
418
|
}
|
|
419
|
+
if (result.skipped > 0 && result.saved === 0 && result.updated === 0 && result.pending === 0) {
|
|
420
|
+
for (const item of rejected)
|
|
421
|
+
writeRejectedPending(temporary, item.reason, item.value);
|
|
422
|
+
result.pending += rejected.length;
|
|
423
|
+
}
|
|
318
424
|
catalog.updatedAt = new Date().toISOString();
|
|
319
425
|
const manifest = {
|
|
320
426
|
version: 3, generationId, createdAt: new Date().toISOString(), leaderToken, catalog,
|
|
@@ -31,13 +31,17 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
|
|
|
31
31
|
idleMs: 60_000,
|
|
32
32
|
capabilityPollMs: 5_000,
|
|
33
33
|
modelTimeoutMs: 180_000,
|
|
34
|
-
maxDeltaChars:
|
|
35
|
-
|
|
34
|
+
maxDeltaChars: 12_000,
|
|
35
|
+
maxPriorContextChars: 4_000,
|
|
36
|
+
maxExistingContextItems: 5,
|
|
37
|
+
maxExistingBodyItems: 2,
|
|
38
|
+
maxExistingBodyChars: 600,
|
|
39
|
+
maxCandidates: 3,
|
|
36
40
|
minimumConfidence: 0.72,
|
|
37
41
|
}),
|
|
38
42
|
storage: Object.freeze({
|
|
39
|
-
maxRuleChars:
|
|
40
|
-
maxRuleFileLines:
|
|
43
|
+
maxRuleChars: 320,
|
|
44
|
+
maxRuleFileLines: 6,
|
|
41
45
|
maxRulesPromptChars: 12_000,
|
|
42
46
|
maxMemoryChars: 25_000,
|
|
43
47
|
maxMemoryLines: 200,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function findLatestState(entries, codec) {
|
|
2
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
3
|
+
const entry = entries[index];
|
|
4
|
+
if (entry.type !== "custom" || entry.customType !== codec.customType)
|
|
5
|
+
continue;
|
|
6
|
+
return { found: true, value: codec.decode(entry.data) };
|
|
7
|
+
}
|
|
8
|
+
return { found: false };
|
|
9
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { findLatestState } from "../runtime/session-state.js";
|
|
2
|
+
export const WORKFLOW_STATE_TYPE = "hwcode-workflow-state";
|
|
3
|
+
export const WORKFLOW_EXTERNAL_AUDIT_TYPE = "hwcode-workflow-external-approval";
|
|
4
|
+
function isCloudTemplateSource(value) {
|
|
5
|
+
if (!value || typeof value !== "object")
|
|
6
|
+
return false;
|
|
7
|
+
const data = value;
|
|
8
|
+
return typeof data.id === "string"
|
|
9
|
+
&& typeof data.name === "string"
|
|
10
|
+
&& typeof data.createdAt === "string"
|
|
11
|
+
&& typeof data.updatedAt === "string";
|
|
12
|
+
}
|
|
13
|
+
function isCloudRunnerSummary(value) {
|
|
14
|
+
if (!value || typeof value !== "object")
|
|
15
|
+
return false;
|
|
16
|
+
const data = value;
|
|
17
|
+
return typeof data.id === "string"
|
|
18
|
+
&& typeof data.name === "string"
|
|
19
|
+
&& typeof data.vendor === "string"
|
|
20
|
+
&& typeof data.region === "string"
|
|
21
|
+
&& typeof data.host === "string"
|
|
22
|
+
&& Number.isInteger(data.port)
|
|
23
|
+
&& typeof data.user === "string"
|
|
24
|
+
&& typeof data.remoteRoot === "string"
|
|
25
|
+
&& typeof data.hostKeyFingerprint === "string"
|
|
26
|
+
&& ["instance-role", "agency", "ssh-only"].includes(data.identityType);
|
|
27
|
+
}
|
|
28
|
+
function isTerraformRunState(value) {
|
|
29
|
+
if (!value || typeof value !== "object")
|
|
30
|
+
return false;
|
|
31
|
+
const data = value;
|
|
32
|
+
const summary = data.planSummary;
|
|
33
|
+
return typeof data.runId === "string"
|
|
34
|
+
&& typeof data.runnerId === "string"
|
|
35
|
+
&& ["synced", "validated", "planned", "applied", "failed"].includes(data.phase)
|
|
36
|
+
&& typeof data.sourceDigest === "string"
|
|
37
|
+
&& typeof data.remoteWorkspace === "string"
|
|
38
|
+
&& typeof data.startedAt === "string"
|
|
39
|
+
&& typeof data.updatedAt === "string"
|
|
40
|
+
&& (data.planDigest === undefined || typeof data.planDigest === "string")
|
|
41
|
+
&& (summary === undefined || (summary !== null && typeof summary === "object"
|
|
42
|
+
&& Object.values(summary).every((entry) => Number.isInteger(entry))));
|
|
43
|
+
}
|
|
44
|
+
function isCloudDetails(value) {
|
|
45
|
+
if (!value || typeof value !== "object")
|
|
46
|
+
return false;
|
|
47
|
+
const data = value;
|
|
48
|
+
return typeof data.vendor === "string"
|
|
49
|
+
&& typeof data.deployCurrentProject === "boolean"
|
|
50
|
+
&& typeof data.request === "string"
|
|
51
|
+
&& typeof data.allowNonDeleteChanges === "boolean"
|
|
52
|
+
&& Array.isArray(data.failedApproaches)
|
|
53
|
+
&& Array.isArray(data.successfulSteps)
|
|
54
|
+
&& (data.resources === undefined || (Array.isArray(data.resources) && data.resources.every((resource) => {
|
|
55
|
+
if (!resource || typeof resource !== "object")
|
|
56
|
+
return false;
|
|
57
|
+
const entry = resource;
|
|
58
|
+
return typeof entry.id === "string" && typeof entry.type === "string" && typeof entry.region === "string"
|
|
59
|
+
&& ["existing", "workflow-created"].includes(entry.ownership)
|
|
60
|
+
&& ["active", "deleted"].includes(entry.status)
|
|
61
|
+
&& typeof entry.updatedAt === "string";
|
|
62
|
+
})))
|
|
63
|
+
&& typeof data.terminalFailure === "boolean"
|
|
64
|
+
&& (data.artifactDirectory === undefined || typeof data.artifactDirectory === "string")
|
|
65
|
+
&& (data.templateGuidance === undefined || typeof data.templateGuidance === "string")
|
|
66
|
+
&& (data.terraformSourcePath === undefined || typeof data.terraformSourcePath === "string")
|
|
67
|
+
&& (data.sourceTemplate === undefined || isCloudTemplateSource(data.sourceTemplate))
|
|
68
|
+
&& (data.runner === undefined || isCloudRunnerSummary(data.runner))
|
|
69
|
+
&& (data.runnerPreference === undefined || ["automatic", "deferred", "existing"].includes(data.runnerPreference))
|
|
70
|
+
&& (data.terraformRun === undefined || isTerraformRunState(data.terraformRun));
|
|
71
|
+
}
|
|
72
|
+
export function decodeWorkflowState(value) {
|
|
73
|
+
if (!value || typeof value !== "object")
|
|
74
|
+
return undefined;
|
|
75
|
+
const data = value;
|
|
76
|
+
if (data.version === 2) {
|
|
77
|
+
if (!["active", "completed", "cancelled", "failed"].includes(data.status)
|
|
78
|
+
|| !["vibe", "sdd", "cloud"].includes(data.mode)
|
|
79
|
+
|| typeof data.root !== "string"
|
|
80
|
+
|| typeof data.phase !== "string"
|
|
81
|
+
|| typeof data.activatedAt !== "string"
|
|
82
|
+
|| typeof data.updatedAt !== "string")
|
|
83
|
+
return undefined;
|
|
84
|
+
if (data.mode === "cloud" && !isCloudDetails(data.details))
|
|
85
|
+
return undefined;
|
|
86
|
+
if (data.mode === "sdd" && data.sdd !== undefined) {
|
|
87
|
+
const sdd = data.sdd;
|
|
88
|
+
if (!["discovery", "requirements", "design", "test-plan", "tasks", "tests", "implementation", "verification"].includes(sdd.phase)
|
|
89
|
+
|| !Array.isArray(sdd.approvals))
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
export const WORKFLOW_STATE_CODEC = {
|
|
97
|
+
customType: WORKFLOW_STATE_TYPE,
|
|
98
|
+
decode: decodeWorkflowState,
|
|
99
|
+
};
|
|
100
|
+
export function restoreWorkflowState(entries) {
|
|
101
|
+
return findLatestState(entries, WORKFLOW_STATE_CODEC).value;
|
|
102
|
+
}
|
|
103
|
+
export function activeWorkflow(entries) {
|
|
104
|
+
const state = restoreWorkflowState(entries);
|
|
105
|
+
return state?.status === "active" ? state : undefined;
|
|
106
|
+
}
|
|
107
|
+
export function cloudDetails(state) {
|
|
108
|
+
return state.mode === "cloud" ? state.details : undefined;
|
|
109
|
+
}
|
|
110
|
+
export function createWorkflowState(mode, root, phase = "activated") {
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
return {
|
|
113
|
+
version: 2,
|
|
114
|
+
status: "active",
|
|
115
|
+
mode,
|
|
116
|
+
root,
|
|
117
|
+
phase,
|
|
118
|
+
activatedAt: now,
|
|
119
|
+
updatedAt: now,
|
|
120
|
+
...(mode === "sdd" ? { sdd: { phase: "discovery", approvals: [] } } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function createCloudWorkflowState(root, vendor, deployCurrentProject, request, template, artifactDirectory) {
|
|
124
|
+
const now = new Date().toISOString();
|
|
125
|
+
return {
|
|
126
|
+
version: 2,
|
|
127
|
+
status: "active",
|
|
128
|
+
mode: "cloud",
|
|
129
|
+
root,
|
|
130
|
+
phase: "connected",
|
|
131
|
+
activatedAt: now,
|
|
132
|
+
updatedAt: now,
|
|
133
|
+
details: {
|
|
134
|
+
vendor,
|
|
135
|
+
deployCurrentProject,
|
|
136
|
+
request,
|
|
137
|
+
allowNonDeleteChanges: false,
|
|
138
|
+
failedApproaches: [],
|
|
139
|
+
successfulSteps: [],
|
|
140
|
+
resources: [],
|
|
141
|
+
terminalFailure: false,
|
|
142
|
+
...(artifactDirectory ? { artifactDirectory } : {}),
|
|
143
|
+
...(template ? {
|
|
144
|
+
templateGuidance: template.guidance,
|
|
145
|
+
sourceTemplate: template.source,
|
|
146
|
+
} : {}),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function updateWorkflowState(state, changes) {
|
|
151
|
+
return { ...state, ...changes, version: 2, updatedAt: new Date().toISOString() };
|
|
152
|
+
}
|
|
153
|
+
export function workflowLabel(mode) {
|
|
154
|
+
if (mode === "vibe")
|
|
155
|
+
return "HWCode Vibe";
|
|
156
|
+
if (mode === "sdd")
|
|
157
|
+
return "HWCode SDD";
|
|
158
|
+
return "HWCode Cloud";
|
|
159
|
+
}
|