aidimag 1.0.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/LICENSE +102 -0
- package/README.md +113 -0
- package/dist/capture/bootstrap.d.ts +25 -0
- package/dist/capture/bootstrap.js +188 -0
- package/dist/capture/commit-miner.d.ts +59 -0
- package/dist/capture/commit-miner.js +381 -0
- package/dist/capture/harvest.d.ts +50 -0
- package/dist/capture/harvest.js +207 -0
- package/dist/capture/pr-miner.d.ts +57 -0
- package/dist/capture/pr-miner.js +185 -0
- package/dist/capture/session-briefing.d.ts +36 -0
- package/dist/capture/session-briefing.js +150 -0
- package/dist/capture/session-extraction.d.ts +23 -0
- package/dist/capture/session-extraction.js +54 -0
- package/dist/capture/triage.d.ts +30 -0
- package/dist/capture/triage.js +108 -0
- package/dist/cli/commands/capture.d.ts +5 -0
- package/dist/cli/commands/capture.js +357 -0
- package/dist/cli/commands/hosts.d.ts +5 -0
- package/dist/cli/commands/hosts.js +98 -0
- package/dist/cli/commands/knowledge.d.ts +5 -0
- package/dist/cli/commands/knowledge.js +121 -0
- package/dist/cli/commands/memory.d.ts +6 -0
- package/dist/cli/commands/memory.js +392 -0
- package/dist/cli/commands/sync.d.ts +5 -0
- package/dist/cli/commands/sync.js +307 -0
- package/dist/cli/commands/tickets.d.ts +6 -0
- package/dist/cli/commands/tickets.js +328 -0
- package/dist/cli/commands/verify.d.ts +5 -0
- package/dist/cli/commands/verify.js +136 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +46 -0
- package/dist/cli/shared.d.ts +37 -0
- package/dist/cli/shared.js +175 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.js +51 -0
- package/dist/context/generate.d.ts +24 -0
- package/dist/context/generate.js +115 -0
- package/dist/critique/critique.d.ts +40 -0
- package/dist/critique/critique.js +110 -0
- package/dist/db/schema.d.ts +28 -0
- package/dist/db/schema.js +269 -0
- package/dist/db/store.d.ts +182 -0
- package/dist/db/store.js +906 -0
- package/dist/debug.d.ts +14 -0
- package/dist/debug.js +25 -0
- package/dist/embeddings/provider.d.ts +16 -0
- package/dist/embeddings/provider.js +100 -0
- package/dist/embeddings/search.d.ts +22 -0
- package/dist/embeddings/search.js +95 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/knowledge/chunk.d.ts +9 -0
- package/dist/knowledge/chunk.js +78 -0
- package/dist/knowledge/extract.d.ts +34 -0
- package/dist/knowledge/extract.js +113 -0
- package/dist/knowledge/ingest.d.ts +79 -0
- package/dist/knowledge/ingest.js +305 -0
- package/dist/knowledge/llm.d.ts +21 -0
- package/dist/knowledge/llm.js +110 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +532 -0
- package/dist/security/evidence.d.ts +11 -0
- package/dist/security/evidence.js +15 -0
- package/dist/security/seal.d.ts +3 -0
- package/dist/security/seal.js +28 -0
- package/dist/security/sync-push.d.ts +2 -0
- package/dist/security/sync-push.js +37 -0
- package/dist/security/url.d.ts +6 -0
- package/dist/security/url.js +67 -0
- package/dist/sync/client.d.ts +84 -0
- package/dist/sync/client.js +391 -0
- package/dist/sync/server.d.ts +75 -0
- package/dist/sync/server.js +659 -0
- package/dist/tickets/provider.d.ts +80 -0
- package/dist/tickets/provider.js +375 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +5 -0
- package/dist/ui/page.d.ts +5 -0
- package/dist/ui/page.js +841 -0
- package/dist/ui/server.d.ts +10 -0
- package/dist/ui/server.js +437 -0
- package/dist/verify/check.d.ts +38 -0
- package/dist/verify/check.js +121 -0
- package/dist/verify/engine.d.ts +39 -0
- package/dist/verify/engine.js +178 -0
- package/dist/verify/hooks.d.ts +24 -0
- package/dist/verify/hooks.js +125 -0
- package/dist/verify/runners.d.ts +26 -0
- package/dist/verify/runners.js +193 -0
- package/package.json +78 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification engine (Phase 3) — runs evidence and applies the
|
|
3
|
+
* status lifecycle: UNVERIFIED/VERIFIED ↔ STALE. REFUTED stays a
|
|
4
|
+
* deliberate human/agent action, never automatic.
|
|
5
|
+
*
|
|
6
|
+
* Transition rules per memory:
|
|
7
|
+
* - any runnable evidence FAILs → STALE (confidence floored)
|
|
8
|
+
* - all runnable evidence PASSes (≥1) → VERIFIED (confidence boosted)
|
|
9
|
+
* - only skipped/unknown evidence, or none → status unchanged
|
|
10
|
+
* - REFUTED memories are never re-verified (negative knowledge is final
|
|
11
|
+
* until explicitly superseded)
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { runEvidence } from "./runners.js";
|
|
15
|
+
import { debugLog } from "../debug.js";
|
|
16
|
+
const CONFIDENCE_BOOST = 0.1;
|
|
17
|
+
const CONFIDENCE_CAP = 0.95;
|
|
18
|
+
const CONFIDENCE_FLOOR_ON_FAIL = 0.2;
|
|
19
|
+
// ---- confidence decay (Phase 5) -------------------------------------------
|
|
20
|
+
// Memories that can't be re-verified this run decay exponentially with age.
|
|
21
|
+
// HUMAN_ATTESTED-only memories decay fastest (weakest evidence per DESIGN.md).
|
|
22
|
+
const DECAY_HALF_LIFE_DAYS = 45;
|
|
23
|
+
const DECAY_HALF_LIFE_HUMAN_DAYS = 14;
|
|
24
|
+
/** VERIFIED memories whose confidence decays below this demote to UNVERIFIED. */
|
|
25
|
+
const DEMOTION_THRESHOLD = 0.35;
|
|
26
|
+
const MIN_CONFIDENCE = 0.05;
|
|
27
|
+
/** Ignore decay smaller than this — avoids noisy sub-percent updates on every run. */
|
|
28
|
+
const DECAY_EPSILON = 0.01;
|
|
29
|
+
export function decayedConfidence(confidence, lastAnchorIso, halfLifeDays, now = new Date()) {
|
|
30
|
+
const ageDays = (now.getTime() - new Date(lastAnchorIso).getTime()) / 86_400_000;
|
|
31
|
+
if (ageDays <= 0)
|
|
32
|
+
return confidence;
|
|
33
|
+
return Math.max(MIN_CONFIDENCE, confidence * Math.pow(0.5, ageDays / halfLifeDays));
|
|
34
|
+
}
|
|
35
|
+
function isHumanOnly(memory) {
|
|
36
|
+
return memory.grounding.length > 0 && memory.grounding.every((e) => e.type === "HUMAN_ATTESTED");
|
|
37
|
+
}
|
|
38
|
+
export function verifyMemory(store, memory, repoRoot, opts = {}) {
|
|
39
|
+
const outcomes = [];
|
|
40
|
+
const now = new Date().toISOString();
|
|
41
|
+
const runOpts = {
|
|
42
|
+
isTrusted: (payload) => store.isEvidencePayloadTrusted(payload),
|
|
43
|
+
...opts,
|
|
44
|
+
};
|
|
45
|
+
for (const ev of memory.grounding) {
|
|
46
|
+
const outcome = runEvidence(ev, repoRoot, runOpts);
|
|
47
|
+
outcomes.push(outcome);
|
|
48
|
+
if (outcome.result !== "SKIPPED") {
|
|
49
|
+
store.updateEvidenceResult(ev.id, outcome.result, now);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// HUMAN_ATTESTED passes trivially; it re-anchors status but must NOT block
|
|
53
|
+
// decay, otherwise human-only memories would never age.
|
|
54
|
+
const machineRunnable = outcomes.filter((o) => (o.result === "PASS" || o.result === "FAIL") && o.type !== "HUMAN_ATTESTED");
|
|
55
|
+
const runnable = outcomes.filter((o) => o.result === "PASS" || o.result === "FAIL");
|
|
56
|
+
const anyFail = runnable.some((o) => o.result === "FAIL");
|
|
57
|
+
const allPass = runnable.length > 0 && runnable.every((o) => o.result === "PASS");
|
|
58
|
+
debugLog("verify", `${memory.id.slice(0, 8)} outcomes=${outcomes.length} runnable=${runnable.length} allPass=${allPass} status=${memory.status} verifiedAt=${memory.verifiedAt}`);
|
|
59
|
+
let after = memory.status;
|
|
60
|
+
let confidenceAfter = memory.confidence;
|
|
61
|
+
let decayed = false;
|
|
62
|
+
if (anyFail) {
|
|
63
|
+
after = "STALE";
|
|
64
|
+
confidenceAfter = Math.min(memory.confidence, CONFIDENCE_FLOOR_ON_FAIL);
|
|
65
|
+
}
|
|
66
|
+
else if (allPass && machineRunnable.length > 0) {
|
|
67
|
+
after = "VERIFIED";
|
|
68
|
+
confidenceAfter = Math.min(CONFIDENCE_CAP, memory.confidence + CONFIDENCE_BOOST);
|
|
69
|
+
}
|
|
70
|
+
else if (allPass && runnable.length > 0) {
|
|
71
|
+
// human-attested only: VERIFIED on first attestation, then decays from
|
|
72
|
+
// verified_at — re-running verify must not refresh human trust.
|
|
73
|
+
if (memory.verifiedAt === null && memory.status === "UNVERIFIED") {
|
|
74
|
+
after = "VERIFIED";
|
|
75
|
+
debugLog("verify", `${memory.id.slice(0, 8)} UNVERIFIED→VERIFIED (first human attestation)`);
|
|
76
|
+
}
|
|
77
|
+
else if (!memory.pinned) {
|
|
78
|
+
const next = decayedConfidence(memory.confidence, memory.verifiedAt ?? memory.createdAt, DECAY_HALF_LIFE_HUMAN_DAYS);
|
|
79
|
+
if (memory.confidence - next >= DECAY_EPSILON) {
|
|
80
|
+
confidenceAfter = next;
|
|
81
|
+
decayed = true;
|
|
82
|
+
if (memory.status === "VERIFIED" && next < DEMOTION_THRESHOLD)
|
|
83
|
+
after = "UNVERIFIED";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (!memory.pinned) {
|
|
88
|
+
// nothing machine-checkable ran this round → decay with age.
|
|
89
|
+
// Pinned memories are exempt: the clock can't erode them — only a
|
|
90
|
+
// failing evidence run (handled above) can mark them STALE.
|
|
91
|
+
const halfLife = isHumanOnly(memory) ? DECAY_HALF_LIFE_HUMAN_DAYS : DECAY_HALF_LIFE_DAYS;
|
|
92
|
+
const anchor = memory.verifiedAt ?? memory.createdAt;
|
|
93
|
+
const next = decayedConfidence(memory.confidence, anchor, halfLife);
|
|
94
|
+
if (memory.confidence - next >= DECAY_EPSILON) {
|
|
95
|
+
confidenceAfter = next;
|
|
96
|
+
decayed = true;
|
|
97
|
+
if (memory.status === "VERIFIED" && next < DEMOTION_THRESHOLD) {
|
|
98
|
+
after = "UNVERIFIED"; // trust expired without re-confirmation
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (after !== memory.status)
|
|
103
|
+
store.setStatus(memory.id, after);
|
|
104
|
+
if (confidenceAfter !== memory.confidence)
|
|
105
|
+
store.setConfidence(memory.id, confidenceAfter);
|
|
106
|
+
// Touch verifiedAt when becoming VERIFIED (machine-runnable OR human-attested first verification)
|
|
107
|
+
if (after === "VERIFIED" && allPass) {
|
|
108
|
+
if (machineRunnable.length > 0 || memory.verifiedAt === null) {
|
|
109
|
+
store.touchVerified(memory.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
memoryId: memory.id,
|
|
114
|
+
claim: memory.claim,
|
|
115
|
+
before: memory.status,
|
|
116
|
+
after,
|
|
117
|
+
confidenceBefore: memory.confidence,
|
|
118
|
+
confidenceAfter,
|
|
119
|
+
outcomes,
|
|
120
|
+
decayed,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function verifyAll(store, repoRoot, opts = {}) {
|
|
124
|
+
let memories = store.list(10_000).filter((m) => m.status !== "REFUTED");
|
|
125
|
+
if (opts.ids?.length) {
|
|
126
|
+
memories = memories.filter((m) => opts.ids.some((id) => m.id === id || m.id.startsWith(id)));
|
|
127
|
+
}
|
|
128
|
+
const results = memories.map((m) => verifyMemory(store, m, repoRoot, { deep: opts.deep }));
|
|
129
|
+
// Staleness is a capture trigger, not a dead end: when a memory NEWLY flips
|
|
130
|
+
// to STALE, draft a review-queue proposal so the human decides whether the
|
|
131
|
+
// code drifted (fix + re-verify) or the belief is outdated (approve the
|
|
132
|
+
// replacement / refute the original). Dedupe absorbs repeat runs.
|
|
133
|
+
for (const r of results) {
|
|
134
|
+
if (r.after !== "STALE" || r.before === "STALE")
|
|
135
|
+
continue;
|
|
136
|
+
const failed = r.outcomes.filter((o) => o.result === "FAIL");
|
|
137
|
+
store.propose({
|
|
138
|
+
kind: "TODO_CONTEXT",
|
|
139
|
+
claim: `Stale belief needs revisiting: "${r.claim.slice(0, 200)}" — its evidence now fails ` +
|
|
140
|
+
`(${failed.map((f) => `${f.type}: ${f.detail}`).join("; ").slice(0, 200)}). ` +
|
|
141
|
+
`Either the code drifted (fix and re-verify) or the claim is outdated (update or refute it).`,
|
|
142
|
+
source: "verify:stale",
|
|
143
|
+
sourceRef: r.memoryId,
|
|
144
|
+
rationale: `Memory ${r.memoryId.slice(0, 8)} went ${r.before} → STALE during verification.`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// CLOUD_DESIGN consensus input: one verification_report event per run,
|
|
148
|
+
// anchored to the repo HEAD so the server can aggregate "N machines
|
|
149
|
+
// confirm memory X PASSes at sha Y".
|
|
150
|
+
let head = null;
|
|
151
|
+
try {
|
|
152
|
+
head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// not a git repo (or no commits yet) — report without an anchor
|
|
156
|
+
}
|
|
157
|
+
for (const r of results) {
|
|
158
|
+
const ran = r.outcomes.filter((o) => o.result === "PASS" || o.result === "FAIL");
|
|
159
|
+
if (!ran.length)
|
|
160
|
+
continue; // nothing machine-checkable ran; no report
|
|
161
|
+
store.recordEvent("verification_report", r.memoryId, {
|
|
162
|
+
head,
|
|
163
|
+
status: r.after,
|
|
164
|
+
confidence: r.confidenceAfter,
|
|
165
|
+
pass: ran.every((o) => o.result === "PASS"),
|
|
166
|
+
deep: Boolean(opts.deep),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
checked: results.length,
|
|
171
|
+
verified: results.filter((r) => r.after === "VERIFIED").length,
|
|
172
|
+
stale: results.filter((r) => r.after === "STALE").length,
|
|
173
|
+
unchanged: results.filter((r) => r.after === r.before).length,
|
|
174
|
+
decayed: results.filter((r) => r.decayed).length,
|
|
175
|
+
results,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git hook installer (Phase 3) — re-verify memory on every pull/checkout/merge
|
|
3
|
+
* so agents never see a stale green checkmark.
|
|
4
|
+
*
|
|
5
|
+
* Also installs a post-commit CAPTURE hook: each new commit is mined for
|
|
6
|
+
* memory-worthy signals (decisions, gotchas, failed approaches). Candidates go
|
|
7
|
+
* to the proposal queue and the committer gets a gentle nudge to review —
|
|
8
|
+
* capture stays human-gated, it just stops depending on humans remembering
|
|
9
|
+
* to run `dim mine`.
|
|
10
|
+
*
|
|
11
|
+
* Hooks are additive: if a hook already exists without our marker, we append;
|
|
12
|
+
* existing logic is never clobbered.
|
|
13
|
+
*/
|
|
14
|
+
export declare const HOOK_MARKER = "# >>> aidimag verify hook >>>";
|
|
15
|
+
export declare const CAPTURE_HOOK_MARKER = "# >>> aidimag capture hook >>>";
|
|
16
|
+
export declare const BRANCH_HOOK_MARKER = "# >>> aidimag branch hook >>>";
|
|
17
|
+
export declare const PRECOMMIT_HOOK_MARKER = "# >>> aidimag check hook >>>";
|
|
18
|
+
export declare const KNOWLEDGE_HOOK_MARKER = "# >>> aidimag knowledge hook >>>";
|
|
19
|
+
export interface HookInstallResult {
|
|
20
|
+
installed: string[];
|
|
21
|
+
alreadyPresent: string[];
|
|
22
|
+
skipped: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function installGitHooks(repoRoot: string): HookInstallResult;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git hook installer (Phase 3) — re-verify memory on every pull/checkout/merge
|
|
3
|
+
* so agents never see a stale green checkmark.
|
|
4
|
+
*
|
|
5
|
+
* Also installs a post-commit CAPTURE hook: each new commit is mined for
|
|
6
|
+
* memory-worthy signals (decisions, gotchas, failed approaches). Candidates go
|
|
7
|
+
* to the proposal queue and the committer gets a gentle nudge to review —
|
|
8
|
+
* capture stays human-gated, it just stops depending on humans remembering
|
|
9
|
+
* to run `dim mine`.
|
|
10
|
+
*
|
|
11
|
+
* Hooks are additive: if a hook already exists without our marker, we append;
|
|
12
|
+
* existing logic is never clobbered.
|
|
13
|
+
*/
|
|
14
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
export const HOOK_MARKER = "# >>> aidimag verify hook >>>";
|
|
17
|
+
export const CAPTURE_HOOK_MARKER = "# >>> aidimag capture hook >>>";
|
|
18
|
+
export const BRANCH_HOOK_MARKER = "# >>> aidimag branch hook >>>";
|
|
19
|
+
export const PRECOMMIT_HOOK_MARKER = "# >>> aidimag check hook >>>";
|
|
20
|
+
export const KNOWLEDGE_HOOK_MARKER = "# >>> aidimag knowledge hook >>>";
|
|
21
|
+
const VERIFY_HOOK_NAMES = ["post-merge", "post-checkout", "post-rewrite"];
|
|
22
|
+
const CAPTURE_HOOK_NAME = "post-commit";
|
|
23
|
+
const BRANCH_PUSH_HOOK_NAME = "pre-push";
|
|
24
|
+
const PRECOMMIT_HOOK_NAME = "pre-commit";
|
|
25
|
+
const KNOWLEDGE_HOOK_NAME = "post-merge";
|
|
26
|
+
function hookBlock() {
|
|
27
|
+
return [
|
|
28
|
+
HOOK_MARKER,
|
|
29
|
+
"# Re-verifies aidimag memories against the new repo state (cheap tier only).",
|
|
30
|
+
"command -v dim >/dev/null 2>&1 && dim verify --quiet || true",
|
|
31
|
+
"# <<< aidimag verify hook <<<",
|
|
32
|
+
"",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
function captureHookBlock() {
|
|
36
|
+
return [
|
|
37
|
+
CAPTURE_HOOK_MARKER,
|
|
38
|
+
"# Mines the new commit for memory candidates (queued for review, never auto-saved).",
|
|
39
|
+
"command -v dim >/dev/null 2>&1 && dim mine --quiet || true",
|
|
40
|
+
"# <<< aidimag capture hook <<<",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
/** post-checkout: warn (never block) when a new branch breaks the team convention. */
|
|
45
|
+
function branchWarnHookBlock() {
|
|
46
|
+
return [
|
|
47
|
+
BRANCH_HOOK_MARKER,
|
|
48
|
+
"# Warns when the branch name breaks the team convention (tickets.branch in .aidimag/config.json).",
|
|
49
|
+
'if [ "$3" = "1" ]; then command -v dim >/dev/null 2>&1 && dim branch-check --warn || true; fi',
|
|
50
|
+
"# <<< aidimag branch hook <<<",
|
|
51
|
+
"",
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
54
|
+
/** pre-push: block pushes of non-conforming branches when enforce mode is "push". */
|
|
55
|
+
function branchPushHookBlock() {
|
|
56
|
+
return [
|
|
57
|
+
BRANCH_HOOK_MARKER,
|
|
58
|
+
"# Blocks pushing branches that break the team convention (tickets.branch.enforce = push).",
|
|
59
|
+
"command -v dim >/dev/null 2>&1 && { dim branch-check --push || exit 1; } || true",
|
|
60
|
+
"# <<< aidimag branch hook <<<",
|
|
61
|
+
"",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
/** pre-commit: scan the staged diff against memory/guardrails. No-op unless
|
|
65
|
+
* preCommitCheck is enabled in .aidimag/config.json (warn or block). */
|
|
66
|
+
function preCommitHookBlock() {
|
|
67
|
+
return [
|
|
68
|
+
PRECOMMIT_HOOK_MARKER,
|
|
69
|
+
"# Scans the staged diff against active memories + guardrails (shift-left verifier).",
|
|
70
|
+
"# Behavior follows preCommitCheck in .aidimag/config.json (unset = no-op).",
|
|
71
|
+
"command -v dim >/dev/null 2>&1 && { dim check --pre-commit || exit 1; } || true",
|
|
72
|
+
"# <<< aidimag check hook <<<",
|
|
73
|
+
"",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
/** post-merge: catch-up summarization of any docs sitting in the knowledge inbox. */
|
|
77
|
+
function knowledgeHookBlock() {
|
|
78
|
+
return [
|
|
79
|
+
KNOWLEDGE_HOOK_MARKER,
|
|
80
|
+
"# Summarizes newly-pulled knowledge-inbox docs into review proposals (best-effort, never blocks).",
|
|
81
|
+
"command -v dim >/dev/null 2>&1 && dim knowledge sync >/dev/null 2>&1 || true",
|
|
82
|
+
"# <<< aidimag knowledge hook <<<",
|
|
83
|
+
"",
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
export function installGitHooks(repoRoot) {
|
|
87
|
+
const gitDir = path.join(repoRoot, ".git");
|
|
88
|
+
const result = { installed: [], alreadyPresent: [], skipped: [] };
|
|
89
|
+
if (!existsSync(gitDir)) {
|
|
90
|
+
result.skipped = [...VERIFY_HOOK_NAMES, CAPTURE_HOOK_NAME];
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
const hooksDir = path.join(gitDir, "hooks");
|
|
94
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
95
|
+
const install = (name, marker, block) => {
|
|
96
|
+
const file = path.join(hooksDir, name);
|
|
97
|
+
if (existsSync(file)) {
|
|
98
|
+
const current = readFileSync(file, "utf8");
|
|
99
|
+
if (current.includes(marker)) {
|
|
100
|
+
result.alreadyPresent.push(name);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
appendFileSync(file, `\n${block}`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
writeFileSync(file, `#!/bin/sh\n${block}`);
|
|
107
|
+
}
|
|
108
|
+
chmodSync(file, 0o755);
|
|
109
|
+
result.installed.push(name);
|
|
110
|
+
};
|
|
111
|
+
for (const name of VERIFY_HOOK_NAMES)
|
|
112
|
+
install(name, HOOK_MARKER, hookBlock());
|
|
113
|
+
install(CAPTURE_HOOK_NAME, CAPTURE_HOOK_MARKER, captureHookBlock());
|
|
114
|
+
// branch convention (T1.5): warn at creation (post-checkout), gate at push
|
|
115
|
+
install("post-checkout", BRANCH_HOOK_MARKER, branchWarnHookBlock());
|
|
116
|
+
install(BRANCH_PUSH_HOOK_NAME, BRANCH_HOOK_MARKER, branchPushHookBlock());
|
|
117
|
+
// shift-left verifier: scan staged diff before the commit lands (opt-in via config)
|
|
118
|
+
install(PRECOMMIT_HOOK_NAME, PRECOMMIT_HOOK_MARKER, preCommitHookBlock());
|
|
119
|
+
// knowledge catch-up: summarize freshly-pulled inbox docs after a merge/pull
|
|
120
|
+
install(KNOWLEDGE_HOOK_NAME, KNOWLEDGE_HOOK_MARKER, knowledgeHookBlock());
|
|
121
|
+
result.installed = [...new Set(result.installed)];
|
|
122
|
+
result.alreadyPresent = [...new Set(result.alreadyPresent)].filter((n) => !result.installed.includes(n));
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence runners.
|
|
3
|
+
*
|
|
4
|
+
* Cheap tier (STATIC_CHECK, COMMIT_REF) runs on every `git pull` via hooks.
|
|
5
|
+
* Expensive tier (TEST_RESULT, EXEC_TRACE) runs only with deep=true
|
|
6
|
+
* (`dim verify --deep`) so hook-triggered verification stays fast.
|
|
7
|
+
*/
|
|
8
|
+
import type { Evidence, EvidenceResult } from "../types.js";
|
|
9
|
+
export interface RunOutcome {
|
|
10
|
+
evidenceId: string;
|
|
11
|
+
type: Evidence["type"];
|
|
12
|
+
result: EvidenceResult | "SKIPPED";
|
|
13
|
+
detail: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RunOptions {
|
|
16
|
+
/** Run the expensive tier (TEST_RESULT, EXEC_TRACE). Default false — cheap tier only. */
|
|
17
|
+
deep?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Trust gate for executable evidence (STATIC_CHECK/TEST_RESULT/EXEC_TRACE):
|
|
20
|
+
* payloads run shell commands, so only locally-approved ones may execute.
|
|
21
|
+
* Returns true if the payload is approved on this machine. Omitted = trust
|
|
22
|
+
* everything (unit tests / explicit --trust runs).
|
|
23
|
+
*/
|
|
24
|
+
isTrusted?: (payload: string) => boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function runEvidence(ev: Evidence, repoRoot: string, opts?: RunOptions): RunOutcome;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence runners.
|
|
3
|
+
*
|
|
4
|
+
* Cheap tier (STATIC_CHECK, COMMIT_REF) runs on every `git pull` via hooks.
|
|
5
|
+
* Expensive tier (TEST_RESULT, EXEC_TRACE) runs only with deep=true
|
|
6
|
+
* (`dim verify --deep`) so hook-triggered verification stays fast.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
9
|
+
const STATIC_CHECK_TIMEOUT_MS = 15_000;
|
|
10
|
+
const DEEP_TIMEOUT_MS = 120_000; // TEST_RESULT / EXEC_TRACE
|
|
11
|
+
/** STATIC_CHECK: payload is a shell command; exit 0 = claim holds. */
|
|
12
|
+
function runStaticCheck(ev, repoRoot) {
|
|
13
|
+
try {
|
|
14
|
+
execSync(ev.payload, {
|
|
15
|
+
cwd: repoRoot,
|
|
16
|
+
timeout: STATIC_CHECK_TIMEOUT_MS,
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
});
|
|
19
|
+
return { evidenceId: ev.id, type: ev.type, result: "PASS", detail: "exit 0" };
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const e = err;
|
|
23
|
+
if (e.killed) {
|
|
24
|
+
return {
|
|
25
|
+
evidenceId: ev.id,
|
|
26
|
+
type: ev.type,
|
|
27
|
+
result: "UNKNOWN",
|
|
28
|
+
detail: `timed out after ${STATIC_CHECK_TIMEOUT_MS}ms`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
evidenceId: ev.id,
|
|
33
|
+
type: ev.type,
|
|
34
|
+
result: "FAIL",
|
|
35
|
+
detail: `exit ${e.status ?? "?"}`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* COMMIT_REF: payload is a commit sha (optionally "sha:path,path" to also
|
|
41
|
+
* check the anchored files haven't changed since that commit).
|
|
42
|
+
* - sha missing from history → FAIL (rebased away / rewritten)
|
|
43
|
+
* - anchored files changed since sha → FAIL (claim may have drifted)
|
|
44
|
+
* - otherwise → PASS
|
|
45
|
+
*/
|
|
46
|
+
function runCommitRef(ev, repoRoot) {
|
|
47
|
+
const [sha, pathSpec] = ev.payload.split(":", 2);
|
|
48
|
+
const git = (args) => execFileSync("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
49
|
+
try {
|
|
50
|
+
git(["cat-file", "-e", `${sha}^{commit}`]);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {
|
|
54
|
+
evidenceId: ev.id,
|
|
55
|
+
type: ev.type,
|
|
56
|
+
result: "FAIL",
|
|
57
|
+
detail: `commit ${sha.slice(0, 8)} no longer exists in history`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// is the commit still reachable from HEAD? (revert/branch-switch detection)
|
|
61
|
+
try {
|
|
62
|
+
git(["merge-base", "--is-ancestor", sha, "HEAD"]);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return {
|
|
66
|
+
evidenceId: ev.id,
|
|
67
|
+
type: ev.type,
|
|
68
|
+
result: "FAIL",
|
|
69
|
+
detail: `commit ${sha.slice(0, 8)} is not an ancestor of HEAD`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (pathSpec) {
|
|
73
|
+
const paths = pathSpec.split(",").map((p) => p.trim()).filter(Boolean);
|
|
74
|
+
for (const p of paths) {
|
|
75
|
+
if (p.startsWith("/") || p.includes("..")) {
|
|
76
|
+
return {
|
|
77
|
+
evidenceId: ev.id,
|
|
78
|
+
type: ev.type,
|
|
79
|
+
result: "FAIL",
|
|
80
|
+
detail: "invalid path in COMMIT_REF evidence",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const changed = git(["diff", "--name-only", sha, "HEAD", "--", ...paths]).trim();
|
|
86
|
+
if (changed) {
|
|
87
|
+
return {
|
|
88
|
+
evidenceId: ev.id,
|
|
89
|
+
type: ev.type,
|
|
90
|
+
result: "FAIL",
|
|
91
|
+
detail: `anchored file(s) changed since ${sha.slice(0, 8)}: ${changed.split("\n").slice(0, 3).join(", ")}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { evidenceId: ev.id, type: ev.type, result: "UNKNOWN", detail: "diff failed" };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { evidenceId: ev.id, type: ev.type, result: "PASS", detail: `${sha.slice(0, 8)} reachable from HEAD` };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* TEST_RESULT: payload is a test command (e.g. "npm test -- --run auth").
|
|
103
|
+
* Expensive tier — only runs when deep=true. Exit 0 = the claim holds.
|
|
104
|
+
*/
|
|
105
|
+
function runTestResult(ev, repoRoot) {
|
|
106
|
+
try {
|
|
107
|
+
execSync(ev.payload, {
|
|
108
|
+
cwd: repoRoot,
|
|
109
|
+
timeout: DEEP_TIMEOUT_MS,
|
|
110
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
111
|
+
env: { ...process.env, CI: "1" }, // non-interactive, no watch mode
|
|
112
|
+
});
|
|
113
|
+
return { evidenceId: ev.id, type: ev.type, result: "PASS", detail: "test command exit 0" };
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const e = err;
|
|
117
|
+
if (e.killed) {
|
|
118
|
+
return { evidenceId: ev.id, type: ev.type, result: "UNKNOWN", detail: `timed out after ${DEEP_TIMEOUT_MS}ms` };
|
|
119
|
+
}
|
|
120
|
+
return { evidenceId: ev.id, type: ev.type, result: "FAIL", detail: `test command exit ${e.status ?? "?"}` };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* EXEC_TRACE: payload is "command :: expected-output-regex".
|
|
125
|
+
* Runs the command and matches stdout against the regex — the claim holds iff
|
|
126
|
+
* the observed behavior matches. Without " :: ", exit 0 = PASS.
|
|
127
|
+
* Sandboxing note: v1 confines execution to the repo root with a hard timeout;
|
|
128
|
+
* container/VM isolation is a later hardening step.
|
|
129
|
+
*/
|
|
130
|
+
function runExecTrace(ev, repoRoot) {
|
|
131
|
+
const sep = " :: ";
|
|
132
|
+
const idx = ev.payload.indexOf(sep);
|
|
133
|
+
const cmd = idx >= 0 ? ev.payload.slice(0, idx) : ev.payload;
|
|
134
|
+
const expect = idx >= 0 ? ev.payload.slice(idx + sep.length) : null;
|
|
135
|
+
try {
|
|
136
|
+
const stdout = execSync(cmd, {
|
|
137
|
+
cwd: repoRoot,
|
|
138
|
+
timeout: DEEP_TIMEOUT_MS,
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
141
|
+
});
|
|
142
|
+
if (expect === null) {
|
|
143
|
+
return { evidenceId: ev.id, type: ev.type, result: "PASS", detail: "exec exit 0" };
|
|
144
|
+
}
|
|
145
|
+
const re = new RegExp(expect, "m");
|
|
146
|
+
return re.test(stdout)
|
|
147
|
+
? { evidenceId: ev.id, type: ev.type, result: "PASS", detail: `output matched /${expect}/` }
|
|
148
|
+
: { evidenceId: ev.id, type: ev.type, result: "FAIL", detail: `output did not match /${expect}/` };
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const e = err;
|
|
152
|
+
if (e.killed) {
|
|
153
|
+
return { evidenceId: ev.id, type: ev.type, result: "UNKNOWN", detail: `timed out after ${DEEP_TIMEOUT_MS}ms` };
|
|
154
|
+
}
|
|
155
|
+
return { evidenceId: ev.id, type: ev.type, result: "FAIL", detail: `exec exit ${e.status ?? "?"}` };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const EXECUTABLE_TYPES = new Set(["STATIC_CHECK", "TEST_RESULT", "EXEC_TRACE"]);
|
|
159
|
+
export function runEvidence(ev, repoRoot, opts = {}) {
|
|
160
|
+
// Supply-chain guard: evidence that arrived via team sync is shell code
|
|
161
|
+
// someone else wrote. It never executes until approved on this machine
|
|
162
|
+
// (`dim verify --trust` to review & approve).
|
|
163
|
+
if (EXECUTABLE_TYPES.has(ev.type) && opts.isTrusted && !opts.isTrusted(ev.payload)) {
|
|
164
|
+
return {
|
|
165
|
+
evidenceId: ev.id,
|
|
166
|
+
type: ev.type,
|
|
167
|
+
result: "SKIPPED",
|
|
168
|
+
detail: "untrusted (synced) evidence — inspect & approve with `dim verify --trust`",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
switch (ev.type) {
|
|
172
|
+
case "STATIC_CHECK":
|
|
173
|
+
return runStaticCheck(ev, repoRoot);
|
|
174
|
+
case "COMMIT_REF":
|
|
175
|
+
return runCommitRef(ev, repoRoot);
|
|
176
|
+
case "HUMAN_ATTESTED":
|
|
177
|
+
// human word is taken as-is; confidence decay handles its aging
|
|
178
|
+
return { evidenceId: ev.id, type: ev.type, result: "PASS", detail: "human attested" };
|
|
179
|
+
case "TEST_RESULT":
|
|
180
|
+
return opts.deep
|
|
181
|
+
? runTestResult(ev, repoRoot)
|
|
182
|
+
: { evidenceId: ev.id, type: ev.type, result: "SKIPPED", detail: "expensive tier — use --deep" };
|
|
183
|
+
case "EXEC_TRACE":
|
|
184
|
+
return opts.deep
|
|
185
|
+
? runExecTrace(ev, repoRoot)
|
|
186
|
+
: { evidenceId: ev.id, type: ev.type, result: "SKIPPED", detail: "expensive tier — use --deep" };
|
|
187
|
+
case "TICKET_REF":
|
|
188
|
+
// annotation-only provenance (TICKETS_DESIGN open question #2: ticket
|
|
189
|
+
// lifecycle is a weaker signal than code evidence — never flips status)
|
|
190
|
+
return { evidenceId: ev.id, type: ev.type, result: "SKIPPED", detail: `ticket ${ev.payload} (provenance annotation)` };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=runners.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aidimag",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Persistent, verified memory for AI coding agents. CLI: dim.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"author": "Anup Khanal",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/anupkhanal/aidimag.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/anupkhanal/aidimag#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/anupkhanal/aidimag/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"dim": "./dist/cli/index.js",
|
|
18
|
+
"aidimag": "./dist/cli/index.js"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"!dist/test",
|
|
30
|
+
"!dist/**/*.map",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"dev": "tsc --watch",
|
|
40
|
+
"mcp": "node dist/mcp/server.js",
|
|
41
|
+
"pretest": "tsc",
|
|
42
|
+
"test": "node --test dist/test/*.test.js",
|
|
43
|
+
"prepublishOnly": "npm run build && npm test",
|
|
44
|
+
"docs:dev": "vitepress dev docs",
|
|
45
|
+
"docs:build": "vitepress build docs",
|
|
46
|
+
"docs:preview": "vitepress preview docs"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"mcp",
|
|
50
|
+
"ai-agents",
|
|
51
|
+
"memory",
|
|
52
|
+
"codebase",
|
|
53
|
+
"claude",
|
|
54
|
+
"cursor",
|
|
55
|
+
"copilot",
|
|
56
|
+
"knowledge-base",
|
|
57
|
+
"cli"
|
|
58
|
+
],
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
61
|
+
"better-sqlite3": "^11.0.0",
|
|
62
|
+
"commander": "^12.0.0",
|
|
63
|
+
"mammoth": "^1.11.0",
|
|
64
|
+
"pdf-parse": "^1.1.1",
|
|
65
|
+
"sqlite-vec": "^0.1.9",
|
|
66
|
+
"uuid": "^12.0.1",
|
|
67
|
+
"zod": "^3.23.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
71
|
+
"@types/node": "^20.0.0",
|
|
72
|
+
"typescript": "^5.5.0",
|
|
73
|
+
"vitepress": "2.0.0-alpha.17"
|
|
74
|
+
},
|
|
75
|
+
"overrides": {
|
|
76
|
+
"esbuild": "^0.28.1"
|
|
77
|
+
}
|
|
78
|
+
}
|