open-multi-agent-kit 0.78.0 → 0.78.2
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/CHANGELOG.md +56 -15
- package/MATURITY.md +6 -2
- package/README.md +125 -26
- package/ROADMAP.md +36 -28
- package/dist/cli/register-basic-commands.js +3 -2
- package/dist/cli/register-mcp-dag-cron-screenshot-commands.js +2 -0
- package/dist/cli/register-spec-agent-goal-commands.js +45 -0
- package/dist/cli/register-tool-commands.js +11 -0
- package/dist/cli/register-workflow-commands.js +1 -0
- package/dist/cli/registry/tooling.js +3 -2
- package/dist/cli/release-promotion-gate.d.ts +14 -0
- package/dist/cli/release-promotion-gate.js +71 -0
- package/dist/cli/v2/release-commands.d.ts +29 -0
- package/dist/cli/v2/release-commands.js +95 -0
- package/dist/commands/chat/core.js +5 -0
- package/dist/commands/chat/native-root-loop.js +74 -1
- package/dist/commands/chat/slash/commands/session.js +19 -1
- package/dist/commands/dag-from-spec.d.ts +1 -0
- package/dist/commands/dag-from-spec.js +61 -1
- package/dist/commands/goal-interview.d.ts +18 -0
- package/dist/commands/goal-interview.js +396 -0
- package/dist/commands/graph.d.ts +62 -0
- package/dist/commands/graph.js +182 -0
- package/dist/commands/merge.d.ts +1 -0
- package/dist/commands/merge.js +88 -0
- package/dist/commands/parallel/core.js +3 -3
- package/dist/commands/provider.js +5 -3
- package/dist/commands/star.js +6 -1
- package/dist/commands/summary.d.ts +4 -1
- package/dist/commands/summary.js +103 -1
- package/dist/commands/team.d.ts +1 -0
- package/dist/commands/team.js +38 -0
- package/dist/contracts/interview.d.ts +106 -0
- package/dist/contracts/interview.js +9 -0
- package/dist/contracts/provider-health.d.ts +42 -0
- package/dist/contracts/provider-health.js +9 -0
- package/dist/evidence/index.d.ts +4 -0
- package/dist/evidence/index.js +2 -0
- package/dist/evidence/proof-trust-cli.d.ts +8 -0
- package/dist/evidence/proof-trust-cli.js +27 -0
- package/dist/evidence/proof-trust.d.ts +14 -0
- package/dist/evidence/proof-trust.js +381 -0
- package/dist/evidence/regression-proof-matrix.d.ts +42 -0
- package/dist/evidence/regression-proof-matrix.js +72 -0
- package/dist/goal/intent-frame.d.ts +30 -0
- package/dist/goal/intent-frame.js +39 -9
- package/dist/goal/interview-assimilation.d.ts +13 -0
- package/dist/goal/interview-assimilation.js +383 -0
- package/dist/goal/interview-question-bank.d.ts +11 -0
- package/dist/goal/interview-question-bank.js +225 -0
- package/dist/goal/interview-scoring.d.ts +31 -0
- package/dist/goal/interview-scoring.js +187 -0
- package/dist/goal/interview-session.d.ts +25 -0
- package/dist/goal/interview-session.js +116 -0
- package/dist/input/input-envelope.d.ts +22 -0
- package/dist/input/input-envelope.js +1 -0
- package/dist/memory/local-graph-memory-store.d.ts +15 -0
- package/dist/memory/local-graph-memory-store.js +176 -0
- package/dist/memory/memory-store.d.ts +18 -0
- package/dist/memory/memory-store.js +18 -0
- package/dist/orchestration/adaptorch-topology.d.ts +59 -0
- package/dist/orchestration/adaptorch-topology.js +194 -0
- package/dist/orchestration/capability-routing.d.ts +23 -0
- package/dist/orchestration/capability-routing.js +56 -0
- package/dist/orchestration/dag-compiler-types.d.ts +3 -0
- package/dist/orchestration/dag-compiler.js +14 -1
- package/dist/orchestration/parallel-orchestrator.d.ts +6 -0
- package/dist/orchestration/parallel-orchestrator.js +31 -0
- package/dist/providers/provider-health.d.ts +39 -0
- package/dist/providers/provider-health.js +161 -0
- package/dist/runtime/advanced-control-loop.d.ts +60 -0
- package/dist/runtime/advanced-control-loop.js +136 -0
- package/dist/runtime/agent-runtime.d.ts +10 -0
- package/dist/runtime/blast-radius.d.ts +10 -0
- package/dist/runtime/blast-radius.js +14 -0
- package/dist/runtime/context-broker.d.ts +13 -4
- package/dist/runtime/context-broker.js +14 -1
- package/dist/runtime/contracts/evidence.d.ts +87 -0
- package/dist/runtime/contracts/evidence.js +7 -0
- package/dist/runtime/contracts/router-v2.d.ts +44 -0
- package/dist/runtime/contracts/router-v2.js +4 -0
- package/dist/runtime/contracts/weakness-remediation.d.ts +67 -0
- package/dist/runtime/contracts/weakness-remediation.js +36 -0
- package/dist/runtime/headroom-policy.d.ts +37 -0
- package/dist/runtime/headroom-policy.js +122 -0
- package/dist/runtime/kimi-api-runtime.js +59 -1
- package/dist/runtime/ouroboros-policy.d.ts +57 -0
- package/dist/runtime/ouroboros-policy.js +134 -0
- package/dist/runtime/proof-bundle-trust.d.ts +74 -0
- package/dist/runtime/proof-bundle-trust.js +100 -0
- package/dist/runtime/provider-maturity-gate.d.ts +41 -0
- package/dist/runtime/provider-maturity-gate.js +101 -0
- package/dist/runtime/public-surface.d.ts +93 -0
- package/dist/runtime/public-surface.js +146 -0
- package/dist/runtime/router-v2-scoring.d.ts +11 -0
- package/dist/runtime/router-v2-scoring.js +151 -0
- package/dist/runtime/runtime-backed-task-runner.js +9 -1
- package/dist/runtime/tool-dispatch-contracts.d.ts +57 -1
- package/dist/runtime/tool-dispatch-contracts.js +79 -3
- package/dist/runtime/weakness-remediation-index.d.ts +27 -0
- package/dist/runtime/weakness-remediation-index.js +37 -0
- package/dist/safety/tool-authority-gate.d.ts +62 -0
- package/dist/safety/tool-authority-gate.js +108 -0
- package/dist/schema/proof-bundle.schema.d.ts +26 -26
- package/dist/schema/provider.schema.d.ts +4 -4
- package/dist/util/clipboard-image.d.ts +49 -0
- package/dist/util/clipboard-image.js +263 -0
- package/dist/util/first-run-star.d.ts +9 -0
- package/dist/util/first-run-star.js +42 -1
- package/dist/util/terminal-input.d.ts +20 -0
- package/dist/util/terminal-input.js +32 -0
- package/dist/util/update-check.d.ts +6 -1
- package/dist/util/update-check.js +35 -1
- package/docs/2026-06-08/critical-issues.md +20 -0
- package/docs/2026-06-08/improvements.md +14 -0
- package/docs/2026-06-08/init-checklist.md +25 -0
- package/docs/2026-06-08/plan.md +20 -0
- package/docs/2026-06-09/critical-issues.md +20 -0
- package/docs/2026-06-09/improvements.md +14 -0
- package/docs/2026-06-09/init-checklist.md +25 -0
- package/docs/2026-06-09/plan.md +20 -0
- package/docs/getting-started.md +31 -3
- package/docs/github-organic-promotion.md +127 -0
- package/docs/integrations/ouroboros.md +96 -0
- package/docs/native-root-runtime-algorithms.md +301 -0
- package/docs/provider-maturity.md +1 -1
- package/docs/versioning.md +3 -3
- package/package.json +4 -3
- package/readmeasset/ASSET_INDEX.md +1 -0
- package/templates/skills/agents/omk-agent-reach-websearch/SKILL.md +55 -0
- package/templates/skills/kimi/omk-agent-reach-websearch/SKILL.md +55 -0
- package/dist/native/linux-x64/omk-safety +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Module: src/goal/interview-scoring.ts
|
|
2
|
+
// OMK Deep Interview — pure deterministic scoring/ranking.
|
|
3
|
+
//
|
|
4
|
+
// 100% deterministic: no I/O, no Date.now, no randomness, no network/LLM calls.
|
|
5
|
+
// All 0..1 outputs are clamped to [0,1] and scores rounded to 2 decimals.
|
|
6
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
7
|
+
const round2 = (x) => Math.round(x * 100) / 100;
|
|
8
|
+
/** Max questions surfaced per interview depth. */
|
|
9
|
+
export const DEPTH_LIMITS = {
|
|
10
|
+
light: 5,
|
|
11
|
+
standard: 10,
|
|
12
|
+
deep: 18,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Derive the ranking score for a candidate question.
|
|
16
|
+
* score = informationGain*0.35 + riskReduction*0.25 + dagImpact*0.20
|
|
17
|
+
* + evidenceImpact*0.15 - userCost*0.05, clamped to [0,1].
|
|
18
|
+
*/
|
|
19
|
+
export function scoreInterviewQuestion(input) {
|
|
20
|
+
const raw = input.informationGain * 0.35 +
|
|
21
|
+
input.riskReduction * 0.25 +
|
|
22
|
+
input.dagImpact * 0.2 +
|
|
23
|
+
input.evidenceImpact * 0.15 -
|
|
24
|
+
input.userCost * 0.05;
|
|
25
|
+
const score = round2(clamp01(raw));
|
|
26
|
+
return { ...input, score };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Select the highest-value questions for a depth, dropping questions whose
|
|
30
|
+
* target field is already answered by the goal (unless marked required).
|
|
31
|
+
*/
|
|
32
|
+
export function selectInterviewQuestions(goal, candidates, depth, maxQuestions) {
|
|
33
|
+
const limit = maxQuestions != null ? Math.min(maxQuestions, DEPTH_LIMITS[depth]) : DEPTH_LIMITS[depth];
|
|
34
|
+
const answeredTargets = new Set();
|
|
35
|
+
if (goal) {
|
|
36
|
+
if (goal.objective?.trim())
|
|
37
|
+
answeredTargets.add("objective");
|
|
38
|
+
if (goal.successCriteria.length > 0)
|
|
39
|
+
answeredTargets.add("successCriteria");
|
|
40
|
+
if (goal.expectedArtifacts.length > 0)
|
|
41
|
+
answeredTargets.add("expectedArtifacts");
|
|
42
|
+
if (goal.constraints.length > 0)
|
|
43
|
+
answeredTargets.add("constraints");
|
|
44
|
+
if (goal.nonGoals.length > 0)
|
|
45
|
+
answeredTargets.add("nonGoals");
|
|
46
|
+
if (goal.risks.length > 0)
|
|
47
|
+
answeredTargets.add("risks");
|
|
48
|
+
}
|
|
49
|
+
const filtered = candidates.filter((q) => q.required || !answeredTargets.has(q.targetField));
|
|
50
|
+
const byScoreThenRequired = (a, b) => {
|
|
51
|
+
const byScore = b.score - a.score;
|
|
52
|
+
if (byScore !== 0)
|
|
53
|
+
return byScore;
|
|
54
|
+
return Number(b.required) - Number(a.required);
|
|
55
|
+
};
|
|
56
|
+
// Pin required questions so a small --max-questions/limit can never drop a
|
|
57
|
+
// required axis; optional questions fill the remaining slots by score.
|
|
58
|
+
const required = filtered.filter((q) => q.required).sort(byScoreThenRequired);
|
|
59
|
+
const optional = filtered.filter((q) => !q.required).sort(byScoreThenRequired);
|
|
60
|
+
const remaining = Math.max(0, limit - required.length);
|
|
61
|
+
return [...required, ...optional.slice(0, remaining)].sort(byScoreThenRequired);
|
|
62
|
+
}
|
|
63
|
+
/** Keyword/regex heuristics describing one ambiguity axis. */
|
|
64
|
+
const AMBIGUITY_AXES = [
|
|
65
|
+
{
|
|
66
|
+
weight: 0.18,
|
|
67
|
+
pattern: /\b(objective|goal|implement|build|create|add|fix|refactor|design|develop|generate|support|enable)\b/,
|
|
68
|
+
goalPresent: (s) => !!s.goal?.objective?.trim(),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
weight: 0.18,
|
|
72
|
+
pattern: /\b(acceptance|success criteria|criteria|criterion|definition of done|done when|must pass|requirement|expected result|should)\b/,
|
|
73
|
+
goalPresent: (s) => (s.goal?.successCriteria.length ?? 0) > 0,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
weight: 0.13,
|
|
77
|
+
pattern: /\b(test|verify|verification|check|lint|typecheck|build|coverage|assert|validate)\b/,
|
|
78
|
+
goalPresent: (s) => (s.goal?.expectedArtifacts.some((a) => a.gate === "command-pass") ?? false),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
weight: 0.12,
|
|
82
|
+
pattern: /(\.[a-z]{2,4}\b|\bfile\b|\bpath\b|\bmodule\b|\bcomponent\b|\bdirectory\b|\bartifact\b|\boutput\b)/,
|
|
83
|
+
goalPresent: (s) => (s.goal?.expectedArtifacts.length ?? 0) > 0,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
weight: 0.1,
|
|
87
|
+
pattern: /\b(constraint|must not|only|do not|don't|limit|restrict|without|avoid|never|forbidden)\b/,
|
|
88
|
+
goalPresent: (s) => (s.goal?.constraints.length ?? 0) > 0,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
weight: 0.08,
|
|
92
|
+
pattern: /\b(risk|danger|safe|safety|destructive|irreversible|production|rollback|backup|prod)\b/,
|
|
93
|
+
goalPresent: (s) => (s.goal?.risks.length ?? 0) > 0 || s.riskLevel != null,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
weight: 0.08,
|
|
97
|
+
pattern: /\b(write|edit|modify|delete|shell|command|merge|commit|push|deploy|permission|read.only|authority|scope)\b/,
|
|
98
|
+
goalPresent: (s) => !!s.goal?.intentFrame?.directives?.some((d) => d.kind === "read-only" || d.kind === "no-edits" || d.kind === "scope"),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
weight: 0.07,
|
|
102
|
+
pattern: /\b(non-goal|out of scope|exclude|except|not (include|touch|modify|change|do)|do not (edit|modify|change|touch))\b/,
|
|
103
|
+
goalPresent: (s) => (s.goal?.nonGoals.length ?? 0) > 0,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
weight: 0.06,
|
|
107
|
+
pattern: /\b(depend|dependenc|requires|require|prerequisite|blocked by|relies on|integrat|import)\b/,
|
|
108
|
+
goalPresent: () => false,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* Ambiguity 0..1 — higher means more interview is needed. Computed as the
|
|
113
|
+
* weighted sum of axes that are neither described in the prompt nor present on
|
|
114
|
+
* the goal. Axis weights sum to 1.
|
|
115
|
+
*/
|
|
116
|
+
export function computeAmbiguity(seed) {
|
|
117
|
+
const prompt = (seed.rawPrompt ?? "").toLowerCase();
|
|
118
|
+
let missing = 0;
|
|
119
|
+
for (const axis of AMBIGUITY_AXES) {
|
|
120
|
+
const present = axis.pattern.test(prompt) || axis.goalPresent(seed);
|
|
121
|
+
if (!present)
|
|
122
|
+
missing += axis.weight;
|
|
123
|
+
}
|
|
124
|
+
return round2(clamp01(missing));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Map an ambiguity score to a recommended depth.
|
|
128
|
+
* <0.25 light (caller may skip) · <0.50 light · <0.75 standard · else deep.
|
|
129
|
+
*/
|
|
130
|
+
export function recommendDepth(ambiguity) {
|
|
131
|
+
if (ambiguity < 0.5)
|
|
132
|
+
return "light";
|
|
133
|
+
if (ambiguity < 0.75)
|
|
134
|
+
return "standard";
|
|
135
|
+
return "deep";
|
|
136
|
+
}
|
|
137
|
+
/** Per-axis completeness: 0.6 if goal field populated, +0.4 if a finding matches. */
|
|
138
|
+
function axisScore(goalPopulated, fields, findings) {
|
|
139
|
+
const hasFinding = findings.some((f) => fields.includes(String(f.field)));
|
|
140
|
+
let score = 0;
|
|
141
|
+
if (goalPopulated)
|
|
142
|
+
score += 0.6;
|
|
143
|
+
if (hasFinding)
|
|
144
|
+
score += 0.4;
|
|
145
|
+
return round2(clamp01(score));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Completeness across required and supporting axes, blending populated goal
|
|
149
|
+
* fields with assimilated interview findings.
|
|
150
|
+
*/
|
|
151
|
+
export function computeCompleteness(goal, findings) {
|
|
152
|
+
const objective = axisScore(!!goal?.objective?.trim(), ["objective"], findings);
|
|
153
|
+
const successCriteria = axisScore((goal?.successCriteria.length ?? 0) > 0, ["successCriteria"], findings);
|
|
154
|
+
const evidence = axisScore((goal?.expectedArtifacts.some((a) => !!a.gate) ?? false) ||
|
|
155
|
+
(goal?.successCriteria.some((c) => c.requirement === "required") ?? false), ["evidence"], findings);
|
|
156
|
+
const artifacts = axisScore((goal?.expectedArtifacts.length ?? 0) > 0, ["expectedArtifacts", "artifact", "artifacts"], findings);
|
|
157
|
+
const constraints = axisScore((goal?.constraints.length ?? 0) > 0, ["constraints"], findings);
|
|
158
|
+
const risks = axisScore((goal?.risks.length ?? 0) > 0, ["risks", "riskLevel"], findings);
|
|
159
|
+
const authority = axisScore(!!goal?.intentFrame?.directives?.some((d) => d.kind === "read-only" || d.kind === "no-edits" || d.kind === "scope"), ["authority", "intentFrame"], findings);
|
|
160
|
+
const overall = round2(clamp01(objective * 0.15 +
|
|
161
|
+
successCriteria * 0.25 +
|
|
162
|
+
evidence * 0.2 +
|
|
163
|
+
artifacts * 0.15 +
|
|
164
|
+
constraints * 0.1 +
|
|
165
|
+
risks * 0.1 +
|
|
166
|
+
authority * 0.05));
|
|
167
|
+
const criticalMissing = [];
|
|
168
|
+
if (objective < 0.5)
|
|
169
|
+
criticalMissing.push("objective");
|
|
170
|
+
if (successCriteria < 0.5)
|
|
171
|
+
criticalMissing.push("successCriteria");
|
|
172
|
+
if (evidence < 0.5)
|
|
173
|
+
criticalMissing.push("evidence");
|
|
174
|
+
const contradictions = findings.filter((f) => f.conflict).map((f) => String(f.field));
|
|
175
|
+
return {
|
|
176
|
+
overall,
|
|
177
|
+
objective,
|
|
178
|
+
successCriteria,
|
|
179
|
+
evidence,
|
|
180
|
+
artifacts,
|
|
181
|
+
constraints,
|
|
182
|
+
risks,
|
|
183
|
+
authority,
|
|
184
|
+
criticalMissing,
|
|
185
|
+
contradictions,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InterviewAnswer, InterviewDepth, InterviewMode, InterviewSeed, InterviewSession } from "../contracts/interview.js";
|
|
2
|
+
/** Completeness threshold above which the interview is considered done. */
|
|
3
|
+
export declare const COMPLETENESS_THRESHOLD = 0.82;
|
|
4
|
+
export interface BuildInterviewSessionInput {
|
|
5
|
+
seed: InterviewSeed;
|
|
6
|
+
mode: InterviewMode;
|
|
7
|
+
depth?: InterviewDepth;
|
|
8
|
+
maxQuestions?: number;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
goalId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build a fresh interview session: score ambiguity, rank deterministic
|
|
14
|
+
* questions, and emit an empty spec delta. No answers are applied yet.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildInterviewSession(input: BuildInterviewSessionInput): InterviewSession;
|
|
17
|
+
/**
|
|
18
|
+
* Apply a batch of answers to an open session: assimilate into findings and a
|
|
19
|
+
* spec delta, recompute completeness, and decide the terminal status.
|
|
20
|
+
*
|
|
21
|
+
* Termination (spec): complete when completeness >= 0.82 AND no critical
|
|
22
|
+
* missing field AND no unresolved contradiction. Contradictions => blocked.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ingestAnswers(session: InterviewSession, seed: InterviewSeed, newAnswers: InterviewAnswer[]): InterviewSession;
|
|
25
|
+
export declare function decideStatus(completeness: InterviewSession["completeness"]): InterviewSession["status"];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/goal/interview-session.ts
|
|
2
|
+
// OMK Deep Interview — session orchestrator.
|
|
3
|
+
//
|
|
4
|
+
// Composes the three deterministic modules (question-bank, scoring,
|
|
5
|
+
// assimilation) into an InterviewSession lifecycle:
|
|
6
|
+
// seed -> ambiguity -> question ranking -> answers -> findings
|
|
7
|
+
// -> spec delta -> completeness -> status
|
|
8
|
+
//
|
|
9
|
+
// This module owns the critical integration path and is intentionally
|
|
10
|
+
// deterministic except for timestamps and the session id.
|
|
11
|
+
import { INTERVIEW_SCHEMA_VERSION, INTERVIEW_DELTA_SCHEMA_VERSION } from "../contracts/interview.js";
|
|
12
|
+
import { computeAmbiguity, computeCompleteness, recommendDepth, scoreInterviewQuestion, selectInterviewQuestions, } from "./interview-scoring.js";
|
|
13
|
+
import { buildInterviewQuestionBank } from "./interview-question-bank.js";
|
|
14
|
+
import { assimilateAnswers, applyInterviewDelta } from "./interview-assimilation.js";
|
|
15
|
+
import { createGoalSpec } from "./intake.js";
|
|
16
|
+
import { redactSecretText } from "./intent-frame.js";
|
|
17
|
+
/** Completeness threshold above which the interview is considered done. */
|
|
18
|
+
export const COMPLETENESS_THRESHOLD = 0.82;
|
|
19
|
+
function generateSessionId() {
|
|
20
|
+
return `iv-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a fresh interview session: score ambiguity, rank deterministic
|
|
24
|
+
* questions, and emit an empty spec delta. No answers are applied yet.
|
|
25
|
+
*/
|
|
26
|
+
export function buildInterviewSession(input) {
|
|
27
|
+
const now = new Date().toISOString();
|
|
28
|
+
const ambiguity = computeAmbiguity(input.seed);
|
|
29
|
+
const depth = input.depth ?? recommendDepth(ambiguity);
|
|
30
|
+
const candidates = buildInterviewQuestionBank(input.seed);
|
|
31
|
+
const scored = candidates.map((candidate) => scoreInterviewQuestion(candidate));
|
|
32
|
+
const questions = selectInterviewQuestions(input.seed.goal, scored, depth, input.maxQuestions);
|
|
33
|
+
const completeness = computeCompleteness(input.seed.goal, []);
|
|
34
|
+
return {
|
|
35
|
+
schemaVersion: INTERVIEW_SCHEMA_VERSION,
|
|
36
|
+
sessionId: input.sessionId ?? generateSessionId(),
|
|
37
|
+
goalId: input.goalId ?? input.seed.goal?.goalId,
|
|
38
|
+
mode: input.mode,
|
|
39
|
+
depth,
|
|
40
|
+
createdAt: now,
|
|
41
|
+
updatedAt: now,
|
|
42
|
+
rawPrompt: redactSecretText(input.seed.rawPrompt),
|
|
43
|
+
ambiguity,
|
|
44
|
+
questions,
|
|
45
|
+
answers: [],
|
|
46
|
+
findings: [],
|
|
47
|
+
completeness,
|
|
48
|
+
specDelta: {
|
|
49
|
+
schemaVersion: INTERVIEW_DELTA_SCHEMA_VERSION,
|
|
50
|
+
goalId: input.goalId ?? input.seed.goal?.goalId,
|
|
51
|
+
changes: [],
|
|
52
|
+
},
|
|
53
|
+
status: "open",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Apply a batch of answers to an open session: assimilate into findings and a
|
|
58
|
+
* spec delta, recompute completeness, and decide the terminal status.
|
|
59
|
+
*
|
|
60
|
+
* Termination (spec): complete when completeness >= 0.82 AND no critical
|
|
61
|
+
* missing field AND no unresolved contradiction. Contradictions => blocked.
|
|
62
|
+
*/
|
|
63
|
+
export function ingestAnswers(session, seed, newAnswers) {
|
|
64
|
+
// Redact secrets from answers before they are assimilated, persisted, or
|
|
65
|
+
// echoed so tokens never reach interview artifacts or the GoalSpec.
|
|
66
|
+
const redactedAnswers = newAnswers.map((answer) => ({
|
|
67
|
+
...answer,
|
|
68
|
+
answer: redactSecretText(answer.answer),
|
|
69
|
+
}));
|
|
70
|
+
const mergedAnswers = mergeAnswers(session.answers, redactedAnswers);
|
|
71
|
+
const { findings, specDelta, contradictions } = assimilateAnswers({
|
|
72
|
+
seed,
|
|
73
|
+
questions: session.questions,
|
|
74
|
+
answers: mergedAnswers,
|
|
75
|
+
goal: seed.goal,
|
|
76
|
+
});
|
|
77
|
+
// Completeness is measured against the PROJECTED goal (base + delta) so the
|
|
78
|
+
// score reflects what the answers actually contribute to the GoalSpec, not
|
|
79
|
+
// the empty seed goal. This makes the 0.82 termination threshold meaningful.
|
|
80
|
+
const baseGoal = seed.goal ?? createGoalSpec(seed.rawPrompt);
|
|
81
|
+
const projected = applyInterviewDelta(baseGoal, specDelta).goal;
|
|
82
|
+
const completeness = computeCompleteness(projected, findings);
|
|
83
|
+
completeness.contradictions = uniqueStrings([...completeness.contradictions, ...contradictions]);
|
|
84
|
+
const status = decideStatus(completeness);
|
|
85
|
+
return {
|
|
86
|
+
...session,
|
|
87
|
+
updatedAt: new Date().toISOString(),
|
|
88
|
+
answers: mergedAnswers,
|
|
89
|
+
findings,
|
|
90
|
+
completeness,
|
|
91
|
+
specDelta: {
|
|
92
|
+
...specDelta,
|
|
93
|
+
goalId: session.goalId ?? specDelta.goalId,
|
|
94
|
+
},
|
|
95
|
+
status,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function decideStatus(completeness) {
|
|
99
|
+
if (completeness.contradictions.length > 0)
|
|
100
|
+
return "blocked";
|
|
101
|
+
if (completeness.overall >= COMPLETENESS_THRESHOLD && completeness.criticalMissing.length === 0) {
|
|
102
|
+
return "complete";
|
|
103
|
+
}
|
|
104
|
+
return "open";
|
|
105
|
+
}
|
|
106
|
+
function mergeAnswers(existing, incoming) {
|
|
107
|
+
const byId = new Map();
|
|
108
|
+
for (const answer of existing)
|
|
109
|
+
byId.set(answer.questionId, answer);
|
|
110
|
+
for (const answer of incoming)
|
|
111
|
+
byId.set(answer.questionId, answer);
|
|
112
|
+
return [...byId.values()];
|
|
113
|
+
}
|
|
114
|
+
function uniqueStrings(values) {
|
|
115
|
+
return [...new Set(values.filter((value) => value.length > 0))];
|
|
116
|
+
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
export type InputKind = "plain-prompt" | "slash-command" | "goal-form" | "resume" | "verify" | "replan";
|
|
2
|
+
/**
|
|
3
|
+
* Image or file attached to a prompt (clipboard paste, --image flag, drag).
|
|
4
|
+
* Carries both the on-disk path and the base64 data URI for multimodal
|
|
5
|
+
* wire protocol use (image_url parts).
|
|
6
|
+
*/
|
|
7
|
+
export interface InputAttachment {
|
|
8
|
+
/** Original file name or "clipboard-image.png". */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Absolute or project-relative path to the saved file. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** MIME type: image/png, image/jpeg, image/webp, image/gif. */
|
|
13
|
+
mimeType: string;
|
|
14
|
+
/** Base64 data URI (data:image/png;base64,...) for wire protocol. */
|
|
15
|
+
dataUri: string;
|
|
16
|
+
/** Detected extension: png, jpg, webp, gif. */
|
|
17
|
+
ext: string;
|
|
18
|
+
/** Source of the attachment. */
|
|
19
|
+
source: "clipboard" | "file" | "drag";
|
|
20
|
+
}
|
|
2
21
|
export type InputSource = "chat" | "parallel" | "run" | "goal" | "api";
|
|
3
22
|
export type InputMcpScope = "all" | "project" | "none";
|
|
4
23
|
export interface InputRequestedArtifact {
|
|
@@ -32,6 +51,8 @@ export interface InputEnvelope {
|
|
|
32
51
|
theme?: string;
|
|
33
52
|
constraints: string[];
|
|
34
53
|
requestedArtifacts: InputRequestedArtifact[];
|
|
54
|
+
/** Images/files attached to this input (clipboard paste, --image, drag). */
|
|
55
|
+
attachments: InputAttachment[];
|
|
35
56
|
slashCommand?: InputSlashCommandEnvelope;
|
|
36
57
|
createdAt: string;
|
|
37
58
|
}
|
|
@@ -52,6 +73,7 @@ export interface BuildInputEnvelopeInput {
|
|
|
52
73
|
theme?: string;
|
|
53
74
|
constraints?: readonly string[];
|
|
54
75
|
requestedArtifacts?: readonly InputRequestedArtifact[];
|
|
76
|
+
attachments?: readonly InputAttachment[];
|
|
55
77
|
slashCommand?: InputSlashCommandEnvelope;
|
|
56
78
|
now?: () => Date;
|
|
57
79
|
}
|
|
@@ -60,6 +60,7 @@ export function buildInputEnvelope(input) {
|
|
|
60
60
|
theme: input.theme,
|
|
61
61
|
constraints: [...(input.constraints ?? [])],
|
|
62
62
|
requestedArtifacts: input.requestedArtifacts?.map((artifact) => ({ ...artifact })) ?? [],
|
|
63
|
+
attachments: input.attachments?.map((a) => ({ ...a })) ?? [],
|
|
63
64
|
slashCommand: input.slashCommand
|
|
64
65
|
? cloneSlashCommand(input.slashCommand)
|
|
65
66
|
: undefined,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MemorySettings, type MemoryStatus } from "./memory-config.js";
|
|
2
|
+
import type { RunManifest } from "../contracts/run.js";
|
|
2
3
|
export interface MemorySearchResult {
|
|
3
4
|
path: string;
|
|
4
5
|
content: string;
|
|
@@ -114,6 +115,20 @@ export declare class LocalGraphMemoryStore {
|
|
|
114
115
|
search(query: string, limit?: number): Promise<MemorySearchResult[]>;
|
|
115
116
|
ontology(): Promise<MemoryOntology>;
|
|
116
117
|
mindmap(query?: string, limit?: number): Promise<MemoryMindmap>;
|
|
118
|
+
/**
|
|
119
|
+
* Idempotent finalizer that links a finalized run manifest into the local
|
|
120
|
+
* graph. Emits run -> providerRoute -> provider, run -> evidence,
|
|
121
|
+
* run -> decision, and run -> artifact nodes/edges.
|
|
122
|
+
*
|
|
123
|
+
* Privacy: only ids, status, paths, sha256, and aggregate counts are stored.
|
|
124
|
+
* Raw evidence/decision message bodies and secrets are never persisted.
|
|
125
|
+
*
|
|
126
|
+
* Idempotency: node/edge ids are deterministic and prior run-generated
|
|
127
|
+
* nodes/edges (tagged `generatedFromRun`) are pruned before re-inserting, so
|
|
128
|
+
* re-running with the same (or a changed) manifest never duplicates state.
|
|
129
|
+
*/
|
|
130
|
+
linkRun(runId: string, manifest: RunManifest): Promise<void>;
|
|
131
|
+
private applyRunManifest;
|
|
117
132
|
graphQuery(query: string): Promise<GraphQueryResult>;
|
|
118
133
|
private loadState;
|
|
119
134
|
private emptyState;
|
|
@@ -339,6 +339,182 @@ export class LocalGraphMemoryStore {
|
|
|
339
339
|
const root = this.buildMindmapTree(rootNode, state, includedIds, new Set());
|
|
340
340
|
return { root, nodes: flatNodes, edges, ontology: ONTOLOGY };
|
|
341
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Idempotent finalizer that links a finalized run manifest into the local
|
|
344
|
+
* graph. Emits run -> providerRoute -> provider, run -> evidence,
|
|
345
|
+
* run -> decision, and run -> artifact nodes/edges.
|
|
346
|
+
*
|
|
347
|
+
* Privacy: only ids, status, paths, sha256, and aggregate counts are stored.
|
|
348
|
+
* Raw evidence/decision message bodies and secrets are never persisted.
|
|
349
|
+
*
|
|
350
|
+
* Idempotency: node/edge ids are deterministic and prior run-generated
|
|
351
|
+
* nodes/edges (tagged `generatedFromRun`) are pruned before re-inserting, so
|
|
352
|
+
* re-running with the same (or a changed) manifest never duplicates state.
|
|
353
|
+
*/
|
|
354
|
+
async linkRun(runId, manifest) {
|
|
355
|
+
const canonicalRunId = manifest.runId || runId;
|
|
356
|
+
if (runId && manifest.runId && runId !== manifest.runId) {
|
|
357
|
+
throw new Error(`linkRun: runId mismatch (param=${runId}, manifest=${manifest.runId})`);
|
|
358
|
+
}
|
|
359
|
+
await this.mutateState((state, now) => {
|
|
360
|
+
this.applyRunManifest(state, canonicalRunId, manifest, now);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
applyRunManifest(state, runId, manifest, now) {
|
|
364
|
+
state.updatedAt = now;
|
|
365
|
+
state.project = { ...this.settings.project };
|
|
366
|
+
state.ontology = ONTOLOGY;
|
|
367
|
+
// Prune prior run-generated child nodes/edges for idempotent re-runs. The
|
|
368
|
+
// Run node and shared Provider nodes are intentionally not pruned (the Run
|
|
369
|
+
// node is re-upserted preserving createdAt; Providers are shared by runs).
|
|
370
|
+
const staleNodeIds = new Set(state.nodes.filter((node) => node.properties.generatedFromRun === runId).map((node) => node.id));
|
|
371
|
+
state.nodes = state.nodes.filter((node) => !staleNodeIds.has(node.id));
|
|
372
|
+
state.edges = state.edges.filter((edge) => edge.properties.generatedFromRun !== runId && !staleNodeIds.has(edge.from) && !staleNodeIds.has(edge.to));
|
|
373
|
+
const tag = { generatedFromRun: runId };
|
|
374
|
+
const projectId = this.nodeId("Project", this.settings.project.key);
|
|
375
|
+
this.upsertNode(state, {
|
|
376
|
+
id: projectId,
|
|
377
|
+
type: "Project",
|
|
378
|
+
labels: ["OmkProject", "Project"],
|
|
379
|
+
label: this.settings.project.name,
|
|
380
|
+
summary: this.settings.project.root,
|
|
381
|
+
tags: ["project"],
|
|
382
|
+
properties: { key: this.settings.project.key, root: this.settings.project.root },
|
|
383
|
+
createdAt: now,
|
|
384
|
+
updatedAt: now,
|
|
385
|
+
});
|
|
386
|
+
const runNodeId = this.nodeId("Run", runId);
|
|
387
|
+
this.upsertNode(state, {
|
|
388
|
+
id: runNodeId,
|
|
389
|
+
type: "Run",
|
|
390
|
+
labels: ["OmkRun", "Run"],
|
|
391
|
+
label: runId,
|
|
392
|
+
summary: `Run ${runId} (${manifest.status})`,
|
|
393
|
+
tags: ["run", manifest.status],
|
|
394
|
+
properties: {
|
|
395
|
+
runId,
|
|
396
|
+
status: manifest.status,
|
|
397
|
+
schemaVersion: manifest.schemaVersion,
|
|
398
|
+
createdAt: manifest.createdAt,
|
|
399
|
+
completedAt: manifest.completedAt ?? null,
|
|
400
|
+
promptHash: manifest.promptHash ?? null,
|
|
401
|
+
decisionTracePath: manifest.decisionTracePath ?? null,
|
|
402
|
+
evidenceRequired: manifest.evidenceSummary.required,
|
|
403
|
+
evidencePassed: manifest.evidenceSummary.passed,
|
|
404
|
+
evidenceFailed: manifest.evidenceSummary.failed,
|
|
405
|
+
evidenceMissing: manifest.evidenceSummary.missing,
|
|
406
|
+
nodeCount: manifest.nodes.length,
|
|
407
|
+
artifactCount: manifest.artifacts.length,
|
|
408
|
+
projectKey: this.settings.project.key,
|
|
409
|
+
source: this.source,
|
|
410
|
+
},
|
|
411
|
+
createdAt: now,
|
|
412
|
+
updatedAt: now,
|
|
413
|
+
});
|
|
414
|
+
this.upsertEdge(state, projectId, runNodeId, "HAS_RUN", now, tag);
|
|
415
|
+
// Provider route -> provider.
|
|
416
|
+
const provider = manifest.providerPolicy.provider;
|
|
417
|
+
const mode = manifest.providerPolicy.mode ?? "auto";
|
|
418
|
+
const routeNodeId = this.nodeId("ProviderRoute", `${runId}:${provider}:${mode}`);
|
|
419
|
+
this.upsertNode(state, {
|
|
420
|
+
id: routeNodeId,
|
|
421
|
+
type: "ProviderRoute",
|
|
422
|
+
labels: ["OmkProviderRoute", "ProviderRoute"],
|
|
423
|
+
label: `${provider} (${mode})`,
|
|
424
|
+
summary: `Provider route for run ${runId}`,
|
|
425
|
+
tags: ["provider-route", provider, mode],
|
|
426
|
+
properties: { runId, provider, mode, projectKey: this.settings.project.key, generatedFromRun: runId },
|
|
427
|
+
createdAt: now,
|
|
428
|
+
updatedAt: now,
|
|
429
|
+
});
|
|
430
|
+
this.upsertEdge(state, runNodeId, routeNodeId, "HAS_PROVIDER_ROUTE", now, tag);
|
|
431
|
+
const providerNodeId = this.nodeId("Provider", provider);
|
|
432
|
+
this.upsertNode(state, {
|
|
433
|
+
id: providerNodeId,
|
|
434
|
+
type: "Provider",
|
|
435
|
+
labels: ["OmkProvider", "Provider"],
|
|
436
|
+
label: provider,
|
|
437
|
+
summary: `Provider ${provider}`,
|
|
438
|
+
tags: ["provider", provider],
|
|
439
|
+
properties: { provider, projectKey: this.settings.project.key },
|
|
440
|
+
createdAt: now,
|
|
441
|
+
updatedAt: now,
|
|
442
|
+
});
|
|
443
|
+
this.upsertEdge(state, routeNodeId, providerNodeId, "ROUTES_TO", now, tag);
|
|
444
|
+
// Evidence summary node (counts/paths/sha only — never raw bodies).
|
|
445
|
+
const evidenceArtifact = manifest.artifacts.find((artifact) => artifact.kind === "evidence" || /(^|\/)evidence\.jsonl$/i.test(artifact.path));
|
|
446
|
+
const evidenceNodeId = this.nodeId("Evidence", `${runId}:evidence`);
|
|
447
|
+
this.upsertNode(state, {
|
|
448
|
+
id: evidenceNodeId,
|
|
449
|
+
type: "Evidence",
|
|
450
|
+
labels: ["OmkEvidence", "Evidence"],
|
|
451
|
+
label: `evidence:${runId}`,
|
|
452
|
+
summary: `Evidence ${manifest.evidenceSummary.passed}/${manifest.evidenceSummary.required} passed`,
|
|
453
|
+
tags: ["evidence", manifest.status],
|
|
454
|
+
properties: {
|
|
455
|
+
runId,
|
|
456
|
+
required: manifest.evidenceSummary.required,
|
|
457
|
+
passed: manifest.evidenceSummary.passed,
|
|
458
|
+
failed: manifest.evidenceSummary.failed,
|
|
459
|
+
missing: manifest.evidenceSummary.missing,
|
|
460
|
+
path: evidenceArtifact?.path ?? null,
|
|
461
|
+
sha256: evidenceArtifact?.sha256 ?? null,
|
|
462
|
+
projectKey: this.settings.project.key,
|
|
463
|
+
generatedFromRun: runId,
|
|
464
|
+
},
|
|
465
|
+
createdAt: now,
|
|
466
|
+
updatedAt: now,
|
|
467
|
+
});
|
|
468
|
+
this.upsertEdge(state, runNodeId, evidenceNodeId, "HAS_EVIDENCE", now, tag);
|
|
469
|
+
// Decision trace node (path/sha only — never raw bodies).
|
|
470
|
+
const decisionArtifact = manifest.artifacts.find((artifact) => artifact.kind === "decision" ||
|
|
471
|
+
artifact.kind === "decisions" ||
|
|
472
|
+
/(^|\/)decisions\.jsonl$/i.test(artifact.path));
|
|
473
|
+
const decisionPath = manifest.decisionTracePath ?? decisionArtifact?.path ?? null;
|
|
474
|
+
const decisionNodeId = this.nodeId("Decision", `${runId}:decision`);
|
|
475
|
+
this.upsertNode(state, {
|
|
476
|
+
id: decisionNodeId,
|
|
477
|
+
type: "Decision",
|
|
478
|
+
labels: ["OmkDecision", "Decision"],
|
|
479
|
+
label: `decisions:${runId}`,
|
|
480
|
+
summary: `Decision trace for run ${runId}`,
|
|
481
|
+
tags: ["decision", manifest.status],
|
|
482
|
+
properties: {
|
|
483
|
+
runId,
|
|
484
|
+
path: decisionPath,
|
|
485
|
+
sha256: decisionArtifact?.sha256 ?? null,
|
|
486
|
+
projectKey: this.settings.project.key,
|
|
487
|
+
generatedFromRun: runId,
|
|
488
|
+
},
|
|
489
|
+
createdAt: now,
|
|
490
|
+
updatedAt: now,
|
|
491
|
+
});
|
|
492
|
+
this.upsertEdge(state, runNodeId, decisionNodeId, "HAS_DECISION", now, tag);
|
|
493
|
+
// Artifacts -> File/Artifact nodes (id/kind/path/sha only).
|
|
494
|
+
for (const artifact of manifest.artifacts) {
|
|
495
|
+
const artifactNodeId = this.nodeId("Artifact", `${runId}:${artifact.kind}:${artifact.path}`);
|
|
496
|
+
this.upsertNode(state, {
|
|
497
|
+
id: artifactNodeId,
|
|
498
|
+
type: "Artifact",
|
|
499
|
+
labels: ["OmkArtifact", "Artifact", "File"],
|
|
500
|
+
label: artifact.path,
|
|
501
|
+
path: artifact.path,
|
|
502
|
+
summary: `${artifact.kind} artifact`,
|
|
503
|
+
tags: ["artifact", artifact.kind, ...pathTags(artifact.path)],
|
|
504
|
+
properties: {
|
|
505
|
+
runId,
|
|
506
|
+
kind: artifact.kind,
|
|
507
|
+
path: artifact.path,
|
|
508
|
+
sha256: artifact.sha256 ?? null,
|
|
509
|
+
projectKey: this.settings.project.key,
|
|
510
|
+
generatedFromRun: runId,
|
|
511
|
+
},
|
|
512
|
+
createdAt: now,
|
|
513
|
+
updatedAt: now,
|
|
514
|
+
});
|
|
515
|
+
this.upsertEdge(state, runNodeId, artifactNodeId, "TOUCHES_FILE", now, tag);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
342
518
|
async graphQuery(query) {
|
|
343
519
|
const normalized = query.replace(/\s+/g, " ").trim();
|
|
344
520
|
const data = {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type MemoryStatus } from "./memory-config.js";
|
|
2
2
|
import type { GraphQueryResult, MemoryMindmap, MemoryOntology, MemorySearchResult } from "./local-graph-memory-store.js";
|
|
3
3
|
import type { MemoryBackend } from "./memory-config.js";
|
|
4
|
+
import type { RunManifest } from "../contracts/run.js";
|
|
4
5
|
export interface MemoryStoreOptions {
|
|
5
6
|
projectRoot?: string;
|
|
6
7
|
sessionId?: string;
|
|
@@ -19,7 +20,24 @@ export interface UnifiedMemoryStore {
|
|
|
19
20
|
ontology?: () => Promise<MemoryOntology | null>;
|
|
20
21
|
mindmap?: (query?: string, limit?: number) => Promise<MemoryMindmap | null>;
|
|
21
22
|
graphQuery?: (query: string) => Promise<GraphQueryResult>;
|
|
23
|
+
linkRun?: (runId: string, manifest: RunManifest) => Promise<void>;
|
|
22
24
|
}
|
|
25
|
+
export interface LinkRunToGraphOptions {
|
|
26
|
+
projectRoot?: string;
|
|
27
|
+
sessionId?: string;
|
|
28
|
+
source?: string;
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Idempotent, non-fatal finalizer: links a finalized run manifest into the
|
|
33
|
+
* local graph memory backend. Returns true when the active backend linked the
|
|
34
|
+
* run, false when no graph backend is available (e.g. file-only backend).
|
|
35
|
+
*
|
|
36
|
+
* Only ids/status/paths/sha256/counts are persisted — never raw evidence or
|
|
37
|
+
* decision bodies, and never secrets. Callers should still wrap this in their
|
|
38
|
+
* own try/catch so a graph write failure can never fail a run.
|
|
39
|
+
*/
|
|
40
|
+
export declare function linkRunToGraph(runId: string, manifest: RunManifest, options?: LinkRunToGraphOptions): Promise<boolean>;
|
|
23
41
|
export declare function createMemoryStore(backend: MemoryBackend, options?: MemoryStoreOptions): Promise<UnifiedMemoryStore | null>;
|
|
24
42
|
export declare class MemoryStore {
|
|
25
43
|
private readonly options;
|
|
@@ -2,6 +2,24 @@ import { readFile, writeFile, mkdir } from "fs/promises";
|
|
|
2
2
|
import { join, dirname, resolve, relative, isAbsolute } from "path";
|
|
3
3
|
import { getProjectRoot } from "../util/fs.js";
|
|
4
4
|
import { loadMemorySettings, summarizeMemorySettings, } from "./memory-config.js";
|
|
5
|
+
import { loadMemorySettings as loadMemorySettingsForLink } from "./memory-config.js";
|
|
6
|
+
/**
|
|
7
|
+
* Idempotent, non-fatal finalizer: links a finalized run manifest into the
|
|
8
|
+
* local graph memory backend. Returns true when the active backend linked the
|
|
9
|
+
* run, false when no graph backend is available (e.g. file-only backend).
|
|
10
|
+
*
|
|
11
|
+
* Only ids/status/paths/sha256/counts are persisted — never raw evidence or
|
|
12
|
+
* decision bodies, and never secrets. Callers should still wrap this in their
|
|
13
|
+
* own try/catch so a graph write failure can never fail a run.
|
|
14
|
+
*/
|
|
15
|
+
export async function linkRunToGraph(runId, manifest, options = {}) {
|
|
16
|
+
const settings = await loadMemorySettingsForLink(options.projectRoot, options.env);
|
|
17
|
+
const store = await createMemoryStore(settings.backend, options);
|
|
18
|
+
if (!store || typeof store.linkRun !== "function")
|
|
19
|
+
return false;
|
|
20
|
+
await store.linkRun(runId, manifest);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
5
23
|
export async function createMemoryStore(backend, options = {}) {
|
|
6
24
|
if (backend === "local_graph") {
|
|
7
25
|
const { LocalGraphMemoryStore } = await import("./local-graph-memory-store.js");
|