@bartolli/kmd 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/kmd.mjs +92 -13
- package/dist/kmd.mjs.map +3 -3
- package/package.json +1 -1
package/dist/kmd.mjs
CHANGED
|
@@ -150,7 +150,7 @@ ${issues}`);
|
|
|
150
150
|
}
|
|
151
151
|
return parsed.data;
|
|
152
152
|
}
|
|
153
|
-
var ScopeSchema, KindEntrySchema, WhenSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
153
|
+
var ScopeSchema, KindEntrySchema, WhenSchema, DedupSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
154
154
|
var init_vault_config = __esm({
|
|
155
155
|
"../db/src/vault-config.ts"() {
|
|
156
156
|
"use strict";
|
|
@@ -177,6 +177,10 @@ var init_vault_config = __esm({
|
|
|
177
177
|
than: z.array(z.string().min(1)).min(1)
|
|
178
178
|
})
|
|
179
179
|
]);
|
|
180
|
+
DedupSchema = z.union([
|
|
181
|
+
z.enum(["session", "never"]),
|
|
182
|
+
z.strictObject({ minutes: z.number().int().positive() })
|
|
183
|
+
]);
|
|
180
184
|
TriggerSchema = z.strictObject({
|
|
181
185
|
id: z.string().min(1).describe("Unique per scope list; duplicates keep the first occurrence."),
|
|
182
186
|
on: z.enum(["prompt", "pretool"]),
|
|
@@ -190,7 +194,10 @@ var init_vault_config = __esm({
|
|
|
190
194
|
"Precondition \u2014 the gate fires only when it is UNMET. newer-than: the newest page matching fresh must carry frontmatter updated at or after the newest matching than."
|
|
191
195
|
),
|
|
192
196
|
text: z.string().optional().describe("Required for inject and warn \u2014 the line emitted."),
|
|
193
|
-
reason: z.string().optional().describe("Required for block \u2014 the denial the agent reads.")
|
|
197
|
+
reason: z.string().optional().describe("Required for block \u2014 the denial the agent reads."),
|
|
198
|
+
dedup: DedupSchema.optional().describe(
|
|
199
|
+
"Re-fire policy: session (default, once per session), never, or {minutes: N} for at most once per bucket. Rejected on block triggers \u2014 blocks are dedup-exempt."
|
|
200
|
+
)
|
|
194
201
|
}).superRefine((trigger, ctx) => {
|
|
195
202
|
if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
|
|
196
203
|
ctx.addIssue({
|
|
@@ -210,6 +217,12 @@ var init_vault_config = __esm({
|
|
|
210
217
|
message: `trigger "${trigger.id}": files applies to pretool triggers only`
|
|
211
218
|
});
|
|
212
219
|
}
|
|
220
|
+
if (trigger.enforce === "block" && trigger.dedup !== void 0) {
|
|
221
|
+
ctx.addIssue({
|
|
222
|
+
code: "custom",
|
|
223
|
+
message: `block trigger "${trigger.id}" may not set dedup \u2014 blocks fire on every matching event`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
213
226
|
if (trigger.enforce === "block" ? trigger.reason === void 0 : trigger.text === void 0) {
|
|
214
227
|
ctx.addIssue({
|
|
215
228
|
code: "custom",
|
|
@@ -1737,7 +1750,7 @@ var init_authoring = __esm({
|
|
|
1737
1750
|
].join("\n");
|
|
1738
1751
|
DEFAULT_SYNC_PROTOCOL = [
|
|
1739
1752
|
"Edit the smallest set of files that reflects the change. A milestone tick is plan-only; don't cascade to index.md unless phase or status changed. Controlled-vocabulary edits (`vault.yaml`) need explicit user approval.",
|
|
1740
|
-
"
|
|
1753
|
+
"Harnesses with the posttool hook validate and sync automatically on every vault write. Check `kmd config`: if the `synced` line did not advance past your edits, the hook is not wired \u2014 run `kmd validate`, fix findings, then `kmd sync`."
|
|
1741
1754
|
].join("\n");
|
|
1742
1755
|
CANONICAL_STATUS_FLOW = ["draft", "active", "superseded", "archived"];
|
|
1743
1756
|
}
|
|
@@ -2343,12 +2356,15 @@ __export(hook_exports, {
|
|
|
2343
2356
|
matchPromptTriggers: () => matchPromptTriggers,
|
|
2344
2357
|
parsePretoolEvent: () => parsePretoolEvent,
|
|
2345
2358
|
parsePromptEvent: () => parsePromptEvent,
|
|
2359
|
+
parseStopEvent: () => parseStopEvent,
|
|
2346
2360
|
renderPosttool: () => renderPosttool,
|
|
2347
2361
|
renderPretool: () => renderPretool,
|
|
2362
|
+
renderStop: () => renderStop,
|
|
2348
2363
|
resolveScope: () => resolveScope,
|
|
2349
2364
|
runHookPosttool: () => runHookPosttool,
|
|
2350
2365
|
runHookPretool: () => runHookPretool,
|
|
2351
2366
|
runHookPrompt: () => runHookPrompt,
|
|
2367
|
+
runHookStop: () => runHookStop,
|
|
2352
2368
|
vaultPathTouched: () => vaultPathTouched
|
|
2353
2369
|
});
|
|
2354
2370
|
import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
|
|
@@ -2455,7 +2471,11 @@ function matchPromptTriggers(prompt, triggers) {
|
|
|
2455
2471
|
hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
|
|
2456
2472
|
}
|
|
2457
2473
|
if (hit) {
|
|
2458
|
-
matches.push({
|
|
2474
|
+
matches.push({
|
|
2475
|
+
id: trigger.id,
|
|
2476
|
+
text: trigger.text,
|
|
2477
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2478
|
+
});
|
|
2459
2479
|
}
|
|
2460
2480
|
}
|
|
2461
2481
|
} finally {
|
|
@@ -2534,7 +2554,8 @@ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
|
2534
2554
|
id: trigger.id,
|
|
2535
2555
|
enforce: trigger.enforce,
|
|
2536
2556
|
text,
|
|
2537
|
-
...trigger.when !== void 0 && { when: trigger.when }
|
|
2557
|
+
...trigger.when !== void 0 && { when: trigger.when },
|
|
2558
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2538
2559
|
});
|
|
2539
2560
|
}
|
|
2540
2561
|
return matches;
|
|
@@ -2657,6 +2678,27 @@ ${lines.join("\n")}`
|
|
|
2657
2678
|
}
|
|
2658
2679
|
return JSON.stringify({ findings, synced });
|
|
2659
2680
|
}
|
|
2681
|
+
function parseStopEvent(raw) {
|
|
2682
|
+
const fields = eventFields(raw);
|
|
2683
|
+
if (fields === null) return null;
|
|
2684
|
+
const { session_id, cwd, stop_hook_active } = fields;
|
|
2685
|
+
if (typeof session_id !== "string") return null;
|
|
2686
|
+
return {
|
|
2687
|
+
session_id,
|
|
2688
|
+
...typeof cwd === "string" && { cwd },
|
|
2689
|
+
...typeof stop_hook_active === "boolean" && { stop_hook_active }
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
function renderStop(findings) {
|
|
2693
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
2694
|
+
if (errors.length === 0) return null;
|
|
2695
|
+
const lines = errors.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
|
|
2696
|
+
return JSON.stringify({
|
|
2697
|
+
decision: "block",
|
|
2698
|
+
reason: `kmd validate: ${errors.length} error(s) outstanding \u2014 the index sync is held. Fix them, let the posttool hook sync, then finish:
|
|
2699
|
+
${lines.join("\n")}`
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2660
2702
|
function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
2661
2703
|
const blocks = matches.filter((match) => match.enforce === "block");
|
|
2662
2704
|
const rest = matches.filter((match) => match.enforce !== "block");
|
|
@@ -2666,15 +2708,26 @@ function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
|
2666
2708
|
function hookStateDir() {
|
|
2667
2709
|
return join10(kmdHome(), "state", "hook");
|
|
2668
2710
|
}
|
|
2669
|
-
function dedupeMatches(stateDir, sessionId, matches) {
|
|
2711
|
+
function dedupeMatches(stateDir, sessionId, matches, now = Date.now()) {
|
|
2670
2712
|
if (matches.length === 0) return [];
|
|
2671
2713
|
const file = join10(stateDir, `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
|
|
2672
2714
|
const fired = readFired(file);
|
|
2673
|
-
const fresh =
|
|
2674
|
-
|
|
2715
|
+
const fresh = [];
|
|
2716
|
+
const record = [];
|
|
2717
|
+
for (const match of matches) {
|
|
2718
|
+
if (match.dedup === "never") {
|
|
2719
|
+
fresh.push(match);
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
const key = typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id;
|
|
2723
|
+
if (fired.has(key)) continue;
|
|
2724
|
+
fresh.push(match);
|
|
2725
|
+
record.push(key);
|
|
2726
|
+
}
|
|
2727
|
+
if (record.length > 0) {
|
|
2675
2728
|
mkdirSync4(stateDir, { recursive: true });
|
|
2676
|
-
for (const
|
|
2677
|
-
fired.add(
|
|
2729
|
+
for (const key of record) {
|
|
2730
|
+
fired.add(key);
|
|
2678
2731
|
}
|
|
2679
2732
|
writeFileSync(file, JSON.stringify([...fired]));
|
|
2680
2733
|
pruneStale(stateDir, file);
|
|
@@ -2848,6 +2901,28 @@ async function runHookPosttool() {
|
|
|
2848
2901
|
diag2(err instanceof Error ? err.message : String(err));
|
|
2849
2902
|
}
|
|
2850
2903
|
}
|
|
2904
|
+
async function runHookStop() {
|
|
2905
|
+
try {
|
|
2906
|
+
const invocation = hookInvocation();
|
|
2907
|
+
if (invocation === null) return;
|
|
2908
|
+
const event = parseStopEvent(await readStdin());
|
|
2909
|
+
if (event === null) {
|
|
2910
|
+
diag2("stdin is not a stop event ({session_id})");
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
if (event.stop_hook_active === true) return;
|
|
2914
|
+
const config = await loadVaultConfig(invocation.vaultRoot);
|
|
2915
|
+
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
2916
|
+
if (scope === void 0) return;
|
|
2917
|
+
const rendered = renderStop(await validateVault(invocation.vaultRoot));
|
|
2918
|
+
if (rendered === null) return;
|
|
2919
|
+
const fired = dedupeMatches(hookStateDir(), event.session_id, [{ id: "stop-validate-gate" }]);
|
|
2920
|
+
if (fired.length === 0) return;
|
|
2921
|
+
console.log(rendered);
|
|
2922
|
+
} catch (err) {
|
|
2923
|
+
diag2(err instanceof Error ? err.message : String(err));
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2851
2926
|
async function readStdin() {
|
|
2852
2927
|
process.stdin.setEncoding("utf8");
|
|
2853
2928
|
let input = "";
|
|
@@ -2893,9 +2968,10 @@ commands:
|
|
|
2893
2968
|
mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
|
|
2894
2969
|
config [<vault-root>] print vault + index resolution; with no vault, list known vaults
|
|
2895
2970
|
db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
|
|
2896
|
-
hook <prompt|pretool|posttool> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
2971
|
+
hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
2897
2972
|
harness gate engine: JSON event on stdin, decision/context on stdout;
|
|
2898
|
-
posttool auto-runs validate + sync after a vault write
|
|
2973
|
+
posttool auto-runs validate + sync after a vault write;
|
|
2974
|
+
stop blocks the handoff once while validate errors hold the sync
|
|
2899
2975
|
|
|
2900
2976
|
options:
|
|
2901
2977
|
--version print version
|
|
@@ -2970,11 +3046,14 @@ async function run() {
|
|
|
2970
3046
|
} else if (sub === "posttool") {
|
|
2971
3047
|
const { runHookPosttool: runHookPosttool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
2972
3048
|
await runHookPosttool2();
|
|
3049
|
+
} else if (sub === "stop") {
|
|
3050
|
+
const { runHookStop: runHookStop2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
3051
|
+
await runHookStop2();
|
|
2973
3052
|
} else if (sub) {
|
|
2974
3053
|
console.error(`kmd hook: unknown event: ${sub}`);
|
|
2975
3054
|
} else {
|
|
2976
3055
|
console.error(
|
|
2977
|
-
"usage: kmd hook <prompt|pretool|posttool> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
|
|
3056
|
+
"usage: kmd hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
|
|
2978
3057
|
);
|
|
2979
3058
|
process.exit(2);
|
|
2980
3059
|
}
|