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,396 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile, readdir } from "fs/promises";
|
|
2
|
+
import { basename, join } from "path";
|
|
3
|
+
import { getProjectRoot } from "../util/fs.js";
|
|
4
|
+
import { style, header, status, label } from "../util/theme.js";
|
|
5
|
+
import { NotFoundError, UsageError, emitError } from "../util/cli-contract.js";
|
|
6
|
+
import { createGoalPersister } from "../goal/persistence.js";
|
|
7
|
+
import { createGoalSpec, updateGoalStatus } from "../goal/intake.js";
|
|
8
|
+
import { buildInterviewSession, ingestAnswers } from "../goal/interview-session.js";
|
|
9
|
+
import { applyInterviewDelta } from "../goal/interview-assimilation.js";
|
|
10
|
+
import { redactSecretText } from "../goal/intent-frame.js";
|
|
11
|
+
import { readImageFile } from "../util/clipboard-image.js";
|
|
12
|
+
const IMAGE_ATTACHMENT_QUESTION_ID = "q-image-attachment";
|
|
13
|
+
function getGoalBasePath() {
|
|
14
|
+
return join(getProjectRoot(), ".omk", "goals");
|
|
15
|
+
}
|
|
16
|
+
function printAlphaWarning(json) {
|
|
17
|
+
if (json)
|
|
18
|
+
return;
|
|
19
|
+
if (!process.env.OMK_GOAL_ALPHA) {
|
|
20
|
+
console.log(style.orange("⚠️ Goal interview is alpha. Set OMK_GOAL_ALPHA=1 to suppress this warning."));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function parseDepth(value) {
|
|
24
|
+
if (!value)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (value === "light" || value === "standard" || value === "deep")
|
|
27
|
+
return value;
|
|
28
|
+
throw new UsageError(`Invalid depth: ${value} (expected light | standard | deep)`);
|
|
29
|
+
}
|
|
30
|
+
function parseMode(value) {
|
|
31
|
+
if (!value || value === "create")
|
|
32
|
+
return "create";
|
|
33
|
+
if (value === "refine")
|
|
34
|
+
return "refine";
|
|
35
|
+
throw new UsageError(`Invalid mode: ${value} (expected create | refine)`);
|
|
36
|
+
}
|
|
37
|
+
async function loadAnswersFile(root, filePath) {
|
|
38
|
+
const abs = join(root, filePath);
|
|
39
|
+
let content;
|
|
40
|
+
try {
|
|
41
|
+
content = await readFile(abs, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new NotFoundError(`Answers file not found: ${filePath}`);
|
|
45
|
+
}
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(content);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new UsageError(`Answers file is not valid JSON: ${filePath}`);
|
|
52
|
+
}
|
|
53
|
+
const rows = Array.isArray(parsed)
|
|
54
|
+
? parsed
|
|
55
|
+
: (parsed && typeof parsed === "object" && Array.isArray(parsed.answers)
|
|
56
|
+
? parsed.answers
|
|
57
|
+
: null);
|
|
58
|
+
if (!rows) {
|
|
59
|
+
throw new UsageError(`Answers file must be an array or { "answers": [...] }: ${filePath}`);
|
|
60
|
+
}
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
const answers = [];
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
if (!row || typeof row !== "object")
|
|
65
|
+
continue;
|
|
66
|
+
const r = row;
|
|
67
|
+
if (typeof r.questionId !== "string")
|
|
68
|
+
continue;
|
|
69
|
+
answers.push({
|
|
70
|
+
questionId: r.questionId,
|
|
71
|
+
answer: typeof r.answer === "string" ? r.answer : "",
|
|
72
|
+
answeredAt: typeof r.answeredAt === "string" ? r.answeredAt : now,
|
|
73
|
+
skipped: r.skipped === true ? true : undefined,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return answers;
|
|
77
|
+
}
|
|
78
|
+
function buildImageAttachment(imagePath) {
|
|
79
|
+
const img = readImageFile(imagePath);
|
|
80
|
+
if (!img.ok) {
|
|
81
|
+
throw new UsageError(img.error ?? `Unable to read image file: ${imagePath}`);
|
|
82
|
+
}
|
|
83
|
+
if (typeof img.dataUri !== "string" || typeof img.ext !== "string") {
|
|
84
|
+
throw new UsageError(`Image file could not be converted to a data URI: ${imagePath}`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
name: basename(imagePath),
|
|
88
|
+
path: imagePath,
|
|
89
|
+
mimeType: `image/${img.ext}`,
|
|
90
|
+
dataUri: img.dataUri,
|
|
91
|
+
ext: img.ext,
|
|
92
|
+
source: "file",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function attachImageToSession(session, imagePath) {
|
|
96
|
+
const attachment = buildImageAttachment(imagePath);
|
|
97
|
+
const answeredAt = new Date().toISOString();
|
|
98
|
+
return {
|
|
99
|
+
...session,
|
|
100
|
+
updatedAt: answeredAt,
|
|
101
|
+
answers: [
|
|
102
|
+
...session.answers.filter((answer) => answer.questionId !== IMAGE_ATTACHMENT_QUESTION_ID),
|
|
103
|
+
{
|
|
104
|
+
questionId: IMAGE_ATTACHMENT_QUESTION_ID,
|
|
105
|
+
answer: `Image file: ${imagePath}`,
|
|
106
|
+
answeredAt,
|
|
107
|
+
skipped: false,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
findings: [
|
|
111
|
+
...session.findings.filter((finding) => finding.sourceQuestionId !== IMAGE_ATTACHMENT_QUESTION_ID),
|
|
112
|
+
{
|
|
113
|
+
field: "attachments",
|
|
114
|
+
value: attachment,
|
|
115
|
+
sourceQuestionId: IMAGE_ATTACHMENT_QUESTION_ID,
|
|
116
|
+
confidence: 1,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function renderQuestionsMarkdown(session) {
|
|
122
|
+
const lines = [
|
|
123
|
+
`# Interview Questions — ${session.sessionId}`,
|
|
124
|
+
"",
|
|
125
|
+
`**Mode:** ${session.mode} | **Depth:** ${session.depth} | **Ambiguity:** ${session.ambiguity}`,
|
|
126
|
+
"",
|
|
127
|
+
"| # | Score | Req | Target | Kind | Prompt |",
|
|
128
|
+
"|---|-------|-----|--------|------|--------|",
|
|
129
|
+
];
|
|
130
|
+
session.questions.forEach((q, i) => {
|
|
131
|
+
const req = q.required ? "✓" : "";
|
|
132
|
+
lines.push(`| ${i + 1} | ${q.score} | ${req} | ${q.targetField} | ${q.kind} | ${q.prompt.replace(/\|/g, "\\|")} |`);
|
|
133
|
+
});
|
|
134
|
+
lines.push("");
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
function renderReportMarkdown(session) {
|
|
138
|
+
const c = session.completeness;
|
|
139
|
+
const lines = [
|
|
140
|
+
`# Interview Report — ${session.sessionId}`,
|
|
141
|
+
"",
|
|
142
|
+
`**Status:** ${session.status} | **Mode:** ${session.mode} | **Depth:** ${session.depth}`,
|
|
143
|
+
`**Ambiguity:** ${session.ambiguity} | **Completeness:** ${c.overall}`,
|
|
144
|
+
"",
|
|
145
|
+
"## Completeness by axis",
|
|
146
|
+
"",
|
|
147
|
+
`- objective: ${c.objective}`,
|
|
148
|
+
`- successCriteria: ${c.successCriteria}`,
|
|
149
|
+
`- evidence: ${c.evidence}`,
|
|
150
|
+
`- artifacts: ${c.artifacts}`,
|
|
151
|
+
`- constraints: ${c.constraints}`,
|
|
152
|
+
`- risks: ${c.risks}`,
|
|
153
|
+
`- authority: ${c.authority}`,
|
|
154
|
+
"",
|
|
155
|
+
];
|
|
156
|
+
if (c.criticalMissing.length > 0) {
|
|
157
|
+
lines.push("## Critical missing fields", "", ...c.criticalMissing.map((m) => `- ${m}`), "");
|
|
158
|
+
}
|
|
159
|
+
if (c.contradictions.length > 0) {
|
|
160
|
+
lines.push("## Unresolved contradictions", "", ...c.contradictions.map((m) => `- ${m}`), "");
|
|
161
|
+
}
|
|
162
|
+
lines.push("## Answers", "");
|
|
163
|
+
if (session.answers.length === 0) {
|
|
164
|
+
lines.push("_No answers recorded yet._", "");
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
for (const a of session.answers) {
|
|
168
|
+
if (a.skipped) {
|
|
169
|
+
lines.push(`- **${a.questionId}**: _(skipped)_`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
lines.push(`- **${a.questionId}**: ${a.answer}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
lines.push("");
|
|
176
|
+
}
|
|
177
|
+
lines.push("## Spec delta", "");
|
|
178
|
+
if (session.specDelta.changes.length === 0) {
|
|
179
|
+
lines.push("_No structured changes derived._", "");
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
for (const ch of session.specDelta.changes) {
|
|
183
|
+
lines.push(`- \`${ch.op}\` **${String(ch.field)}** (conf ${ch.confidence}): ${JSON.stringify(ch.value)}`);
|
|
184
|
+
}
|
|
185
|
+
lines.push("");
|
|
186
|
+
}
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
async function writeSessionArtifacts(dir, session) {
|
|
190
|
+
await mkdir(dir, { recursive: true });
|
|
191
|
+
await writeFile(join(dir, "interview.json"), `${JSON.stringify(session, null, 2)}\n`);
|
|
192
|
+
await writeFile(join(dir, "spec-delta.json"), `${JSON.stringify(session.specDelta, null, 2)}\n`);
|
|
193
|
+
await writeFile(join(dir, "questions.md"), renderQuestionsMarkdown(session));
|
|
194
|
+
await writeFile(join(dir, "interview-report.md"), renderReportMarkdown(session));
|
|
195
|
+
const answersJsonl = session.answers.map((a) => JSON.stringify(a)).join("\n");
|
|
196
|
+
await writeFile(join(dir, "answers.jsonl"), answersJsonl.length > 0 ? `${answersJsonl}\n` : "");
|
|
197
|
+
}
|
|
198
|
+
function printSessionSummary(session, sessionDir) {
|
|
199
|
+
console.log(header("Goal Interview"));
|
|
200
|
+
console.log(label("Session", session.sessionId));
|
|
201
|
+
console.log(label("Mode", session.mode));
|
|
202
|
+
console.log(label("Depth", session.depth));
|
|
203
|
+
console.log(label("Ambiguity", String(session.ambiguity)));
|
|
204
|
+
console.log(label("Completeness", String(session.completeness.overall)));
|
|
205
|
+
console.log(label("Status", session.status));
|
|
206
|
+
console.log(label("Questions", String(session.questions.length)));
|
|
207
|
+
console.log(label("Answers", String(session.answers.length)));
|
|
208
|
+
if (session.completeness.criticalMissing.length > 0) {
|
|
209
|
+
console.log(label("Missing", session.completeness.criticalMissing.join(", ")));
|
|
210
|
+
}
|
|
211
|
+
if (session.completeness.contradictions.length > 0) {
|
|
212
|
+
console.log(style.pink(`Contradictions: ${session.completeness.contradictions.join("; ")}`));
|
|
213
|
+
}
|
|
214
|
+
console.log("");
|
|
215
|
+
if (session.answers.length === 0) {
|
|
216
|
+
console.log(style.purpleBold("Top questions"));
|
|
217
|
+
for (const q of session.questions.slice(0, 6)) {
|
|
218
|
+
const req = q.required ? style.pink("[required]") : style.gray("[optional]");
|
|
219
|
+
console.log(` ${req} (${q.score}) ${q.prompt}`);
|
|
220
|
+
}
|
|
221
|
+
console.log("");
|
|
222
|
+
console.log(style.gray("Answer via: omk goal interview \"<prompt>\" --answers answers.json --write-spec"));
|
|
223
|
+
}
|
|
224
|
+
console.log(status.success(`interview.json → ${join(sessionDir, "interview.json")}`));
|
|
225
|
+
}
|
|
226
|
+
export async function goalInterviewCommand(input, options) {
|
|
227
|
+
printAlphaWarning(options.json);
|
|
228
|
+
const root = getProjectRoot();
|
|
229
|
+
const persister = createGoalPersister(getGoalBasePath());
|
|
230
|
+
const mode = parseMode(options.mode);
|
|
231
|
+
const depth = parseDepth(options.depth);
|
|
232
|
+
const maxQuestions = options.maxQuestions ? Number.parseInt(options.maxQuestions, 10) : undefined;
|
|
233
|
+
if (maxQuestions !== undefined && (!Number.isFinite(maxQuestions) || maxQuestions <= 0)) {
|
|
234
|
+
throw new UsageError(`Invalid --max-questions: ${options.maxQuestions}`);
|
|
235
|
+
}
|
|
236
|
+
// Resolve existing goal for refine mode or when --goal-id / id positional is given.
|
|
237
|
+
const goalIdHint = options.goalId ?? (mode === "refine" ? input : undefined);
|
|
238
|
+
let existingGoal = null;
|
|
239
|
+
if (goalIdHint) {
|
|
240
|
+
existingGoal = await persister.load(goalIdHint);
|
|
241
|
+
if (!existingGoal) {
|
|
242
|
+
const msg = `Goal not found: ${goalIdHint}`;
|
|
243
|
+
emitError(msg, Boolean(options.json));
|
|
244
|
+
throw new NotFoundError(msg);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const rawPrompt = existingGoal
|
|
248
|
+
? (existingGoal.rawPrompt || existingGoal.objective || existingGoal.title)
|
|
249
|
+
: (input ?? "");
|
|
250
|
+
if (!rawPrompt.trim()) {
|
|
251
|
+
throw new UsageError("A raw goal prompt or an existing --goal-id is required.");
|
|
252
|
+
}
|
|
253
|
+
// Redact secrets from the prompt before it reaches the session, the GoalSpec,
|
|
254
|
+
// persisted artifacts, or --json output.
|
|
255
|
+
const safePrompt = redactSecretText(rawPrompt);
|
|
256
|
+
const seed = {
|
|
257
|
+
rawPrompt: safePrompt,
|
|
258
|
+
riskLevel: existingGoal?.riskLevel,
|
|
259
|
+
goal: existingGoal ?? undefined,
|
|
260
|
+
};
|
|
261
|
+
let session = buildInterviewSession({
|
|
262
|
+
seed,
|
|
263
|
+
mode: existingGoal ? "refine" : mode,
|
|
264
|
+
depth,
|
|
265
|
+
maxQuestions,
|
|
266
|
+
goalId: existingGoal?.goalId,
|
|
267
|
+
});
|
|
268
|
+
if (options.answers) {
|
|
269
|
+
const answers = await loadAnswersFile(root, options.answers);
|
|
270
|
+
session = ingestAnswers(session, seed, answers);
|
|
271
|
+
}
|
|
272
|
+
if (options.image) {
|
|
273
|
+
session = attachImageToSession(session, options.image);
|
|
274
|
+
}
|
|
275
|
+
// Optionally apply the delta to a GoalSpec.
|
|
276
|
+
let savedGoalId = existingGoal?.goalId;
|
|
277
|
+
if (options.writeSpec) {
|
|
278
|
+
let goal;
|
|
279
|
+
if (existingGoal) {
|
|
280
|
+
const result = applyInterviewDelta(existingGoal, session.specDelta);
|
|
281
|
+
goal = updateGoalStatus(result.goal, result.goal.status, { planRevision: result.goal.planRevision + 1 });
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const base = createGoalSpec(safePrompt);
|
|
285
|
+
goal = applyInterviewDelta(base, session.specDelta).goal;
|
|
286
|
+
}
|
|
287
|
+
await persister.save(goal);
|
|
288
|
+
await persister.appendHistory(goal.goalId, {
|
|
289
|
+
at: new Date().toISOString(),
|
|
290
|
+
action: existingGoal ? "interview-refine" : "interview-create",
|
|
291
|
+
detail: {
|
|
292
|
+
sessionId: session.sessionId,
|
|
293
|
+
ambiguity: session.ambiguity,
|
|
294
|
+
completeness: session.completeness.overall,
|
|
295
|
+
changes: session.specDelta.changes.length,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
savedGoalId = goal.goalId;
|
|
299
|
+
session.goalId = goal.goalId;
|
|
300
|
+
session.specDelta.goalId = goal.goalId;
|
|
301
|
+
}
|
|
302
|
+
// Persist interview artifacts. With a goal id, store under the goal dir.
|
|
303
|
+
const sessionDir = savedGoalId
|
|
304
|
+
? join(getGoalBasePath(), savedGoalId, "interviews", session.sessionId)
|
|
305
|
+
: join(root, ".omk", "interviews", session.sessionId);
|
|
306
|
+
await writeSessionArtifacts(sessionDir, session);
|
|
307
|
+
if (options.json) {
|
|
308
|
+
console.log(JSON.stringify(session, null, 2));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
printSessionSummary(session, sessionDir);
|
|
312
|
+
if (savedGoalId) {
|
|
313
|
+
console.log(status.success(`goal spec written → ${savedGoalId}`));
|
|
314
|
+
console.log(style.gray(`Next: omk goal plan ${savedGoalId}`));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function findLatestInterviewSessionId(goalDir) {
|
|
318
|
+
const interviewsDir = join(goalDir, "interviews");
|
|
319
|
+
try {
|
|
320
|
+
const entries = await readdir(interviewsDir, { withFileTypes: true });
|
|
321
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
322
|
+
return dirs.length > 0 ? dirs[dirs.length - 1] : null;
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
export async function goalRefineCommand(goalId, options) {
|
|
329
|
+
printAlphaWarning(options.json);
|
|
330
|
+
const persister = createGoalPersister(getGoalBasePath());
|
|
331
|
+
const goal = await persister.load(goalId);
|
|
332
|
+
if (!goal) {
|
|
333
|
+
const msg = `Goal not found: ${goalId}`;
|
|
334
|
+
emitError(msg, Boolean(options.json));
|
|
335
|
+
throw new NotFoundError(msg);
|
|
336
|
+
}
|
|
337
|
+
const goalDir = join(getGoalBasePath(), goalId);
|
|
338
|
+
const from = options.fromInterview ?? "latest";
|
|
339
|
+
const sessionId = from === "latest" ? await findLatestInterviewSessionId(goalDir) : from;
|
|
340
|
+
if (!sessionId) {
|
|
341
|
+
const msg = `No interview session found for goal: ${goalId}. Run: omk goal interview ${goalId} --mode refine --answers answers.json`;
|
|
342
|
+
emitError(msg, Boolean(options.json));
|
|
343
|
+
throw new NotFoundError(msg);
|
|
344
|
+
}
|
|
345
|
+
const deltaPath = join(goalDir, "interviews", sessionId, "spec-delta.json");
|
|
346
|
+
let delta;
|
|
347
|
+
try {
|
|
348
|
+
delta = JSON.parse(await readFile(deltaPath, "utf-8"));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
const msg = `Interview spec-delta not found: ${deltaPath}`;
|
|
352
|
+
emitError(msg, Boolean(options.json));
|
|
353
|
+
throw new NotFoundError(msg);
|
|
354
|
+
}
|
|
355
|
+
const result = applyInterviewDelta(goal, delta);
|
|
356
|
+
const refined = updateGoalStatus(result.goal, result.goal.status, { planRevision: result.goal.planRevision + 1 });
|
|
357
|
+
await persister.save(refined);
|
|
358
|
+
await persister.appendHistory(goalId, {
|
|
359
|
+
at: new Date().toISOString(),
|
|
360
|
+
action: "refine",
|
|
361
|
+
detail: {
|
|
362
|
+
sessionId,
|
|
363
|
+
applied: result.appliedChanges.length,
|
|
364
|
+
skipped: result.skippedChanges.length,
|
|
365
|
+
contradictions: result.contradictions.length,
|
|
366
|
+
planRevision: refined.planRevision,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
if (options.plan) {
|
|
370
|
+
const { goalPlanCommand } = await import("./goal.js");
|
|
371
|
+
await goalPlanCommand(goalId, { json: options.json });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (options.json) {
|
|
375
|
+
console.log(JSON.stringify({
|
|
376
|
+
goalId,
|
|
377
|
+
sessionId,
|
|
378
|
+
applied: result.appliedChanges.length,
|
|
379
|
+
skipped: result.skippedChanges.length,
|
|
380
|
+
contradictions: result.contradictions,
|
|
381
|
+
planRevision: refined.planRevision,
|
|
382
|
+
status: refined.status,
|
|
383
|
+
}, null, 2));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
console.log(header("Goal Refined"));
|
|
387
|
+
console.log(label("ID", goalId));
|
|
388
|
+
console.log(label("From interview", sessionId));
|
|
389
|
+
console.log(label("Applied changes", String(result.appliedChanges.length)));
|
|
390
|
+
console.log(label("Skipped changes", String(result.skippedChanges.length)));
|
|
391
|
+
console.log(label("Plan revision", String(refined.planRevision)));
|
|
392
|
+
if (result.contradictions.length > 0) {
|
|
393
|
+
console.log(style.pink(`Contradictions: ${result.contradictions.join("; ")}`));
|
|
394
|
+
}
|
|
395
|
+
console.log(style.gray(`Next: omk goal plan ${goalId}`));
|
|
396
|
+
}
|
package/dist/commands/graph.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { RunManifest } from "../contracts/run.js";
|
|
2
|
+
import type { GraphState } from "../memory/local-graph-memory-store.js";
|
|
1
3
|
export interface GraphViewCommandOptions {
|
|
2
4
|
input?: string;
|
|
3
5
|
output?: string;
|
|
@@ -7,3 +9,63 @@ export interface GraphViewCommandOptions {
|
|
|
7
9
|
open?: boolean;
|
|
8
10
|
}
|
|
9
11
|
export declare function graphViewCommand(options?: GraphViewCommandOptions): Promise<void>;
|
|
12
|
+
export type GraphAuditVerdict = "passed" | "partial" | "failed";
|
|
13
|
+
export interface GraphAuditCounts {
|
|
14
|
+
providerRoute: number;
|
|
15
|
+
provider: number;
|
|
16
|
+
evidence: number;
|
|
17
|
+
decision: number;
|
|
18
|
+
artifact: number;
|
|
19
|
+
}
|
|
20
|
+
export interface GraphAuditMismatch {
|
|
21
|
+
field: string;
|
|
22
|
+
graph: number | string | null;
|
|
23
|
+
manifest: number | string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface GraphAuditDangler {
|
|
26
|
+
edgeId: string;
|
|
27
|
+
type: string;
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
missing: "from" | "to";
|
|
31
|
+
}
|
|
32
|
+
export interface GraphAuditRunResult {
|
|
33
|
+
runId: string;
|
|
34
|
+
runNodeFound: boolean;
|
|
35
|
+
orphan: boolean;
|
|
36
|
+
manifestPresent: boolean;
|
|
37
|
+
counts: GraphAuditCounts;
|
|
38
|
+
mismatches: GraphAuditMismatch[];
|
|
39
|
+
danglers: GraphAuditDangler[];
|
|
40
|
+
verdict: GraphAuditVerdict;
|
|
41
|
+
notes: string[];
|
|
42
|
+
}
|
|
43
|
+
export interface GraphAuditReport {
|
|
44
|
+
schemaVersion: "omk.graph-audit.v1";
|
|
45
|
+
verdict: GraphAuditVerdict;
|
|
46
|
+
statePath?: string;
|
|
47
|
+
generatedAt: string;
|
|
48
|
+
summary: {
|
|
49
|
+
total: number;
|
|
50
|
+
passed: number;
|
|
51
|
+
partial: number;
|
|
52
|
+
failed: number;
|
|
53
|
+
};
|
|
54
|
+
runs: GraphAuditRunResult[];
|
|
55
|
+
}
|
|
56
|
+
export interface GraphAuditCommandOptions {
|
|
57
|
+
run?: string;
|
|
58
|
+
input?: string;
|
|
59
|
+
json?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pure auditor: cross-checks linked run subgraphs against their manifests.
|
|
63
|
+
* For each run it asserts the Run node exists with >=1 provider-route, provider,
|
|
64
|
+
* evidence, decision, and artifact edge, cross-checks evidence counts/artifact
|
|
65
|
+
* counts/status against the manifest, and flags orphan runs and dangling edges.
|
|
66
|
+
*/
|
|
67
|
+
export declare function auditGraphRuns(state: GraphState, entries: ReadonlyArray<{
|
|
68
|
+
runId: string;
|
|
69
|
+
manifest?: RunManifest | null;
|
|
70
|
+
}>, statePath?: string): GraphAuditReport;
|
|
71
|
+
export declare function graphAuditCommand(options?: GraphAuditCommandOptions): Promise<void>;
|
package/dist/commands/graph.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
1
2
|
import { join, resolve } from "path";
|
|
2
3
|
import { getProjectRoot } from "../util/fs.js";
|
|
3
4
|
import { createGraphView } from "../memory/graph-viewer.js";
|
|
4
5
|
import { header, label, status } from "../util/theme.js";
|
|
6
|
+
import { createOmkJsonEnvelope } from "../util/json-envelope.js";
|
|
7
|
+
import { getRunArtifactPath, listValidRunIds, validateRunId } from "../util/run-store.js";
|
|
8
|
+
import { RunManifestSchema } from "../schema/run-manifest.schema.js";
|
|
5
9
|
export async function graphViewCommand(options = {}) {
|
|
6
10
|
const root = getProjectRoot();
|
|
7
11
|
const inputPath = options.input ? resolve(root, options.input) : join(root, ".omk", "memory", "graph-state.json");
|
|
@@ -24,3 +28,181 @@ export async function graphViewCommand(options = {}) {
|
|
|
24
28
|
console.log(label("Edges", String(result.edgeCount)));
|
|
25
29
|
console.log(status.ok("Graph HTML generated"));
|
|
26
30
|
}
|
|
31
|
+
const EVIDENCE_SUMMARY_FIELDS = ["required", "passed", "failed", "missing"];
|
|
32
|
+
/**
|
|
33
|
+
* Pure auditor: cross-checks linked run subgraphs against their manifests.
|
|
34
|
+
* For each run it asserts the Run node exists with >=1 provider-route, provider,
|
|
35
|
+
* evidence, decision, and artifact edge, cross-checks evidence counts/artifact
|
|
36
|
+
* counts/status against the manifest, and flags orphan runs and dangling edges.
|
|
37
|
+
*/
|
|
38
|
+
export function auditGraphRuns(state, entries, statePath) {
|
|
39
|
+
const nodeIds = new Set(state.nodes.map((node) => node.id));
|
|
40
|
+
const runs = entries.map((entry) => auditSingleRun(state, nodeIds, entry.runId, entry.manifest ?? null));
|
|
41
|
+
const summary = { total: runs.length, passed: 0, partial: 0, failed: 0 };
|
|
42
|
+
for (const run of runs)
|
|
43
|
+
summary[run.verdict] += 1;
|
|
44
|
+
const verdict = summary.failed > 0 ? "failed" : summary.partial > 0 ? "partial" : "passed";
|
|
45
|
+
return {
|
|
46
|
+
schemaVersion: "omk.graph-audit.v1",
|
|
47
|
+
verdict,
|
|
48
|
+
statePath,
|
|
49
|
+
generatedAt: new Date().toISOString(),
|
|
50
|
+
summary,
|
|
51
|
+
runs,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function auditSingleRun(state, nodeIds, runId, manifest) {
|
|
55
|
+
const notes = [];
|
|
56
|
+
const mismatches = [];
|
|
57
|
+
const danglers = [];
|
|
58
|
+
const counts = { providerRoute: 0, provider: 0, evidence: 0, decision: 0, artifact: 0 };
|
|
59
|
+
const manifestPresent = manifest !== null;
|
|
60
|
+
const runNode = state.nodes.find((node) => node.type === "Run" && node.properties.runId === runId);
|
|
61
|
+
if (!runNode) {
|
|
62
|
+
notes.push("run node not found in graph");
|
|
63
|
+
if (!manifestPresent)
|
|
64
|
+
notes.push("run-manifest.json not found");
|
|
65
|
+
return { runId, runNodeFound: false, orphan: false, manifestPresent, counts, mismatches, danglers, verdict: "failed", notes };
|
|
66
|
+
}
|
|
67
|
+
const outgoing = state.edges.filter((edge) => edge.from === runNode.id);
|
|
68
|
+
const routeEdges = outgoing.filter((edge) => edge.type === "HAS_PROVIDER_ROUTE");
|
|
69
|
+
counts.providerRoute = routeEdges.length;
|
|
70
|
+
counts.evidence = outgoing.filter((edge) => edge.type === "HAS_EVIDENCE").length;
|
|
71
|
+
counts.decision = outgoing.filter((edge) => edge.type === "HAS_DECISION").length;
|
|
72
|
+
counts.artifact = outgoing.filter((edge) => edge.type === "TOUCHES_FILE").length;
|
|
73
|
+
const routeToEdges = routeEdges.flatMap((routeEdge) => state.edges.filter((edge) => edge.from === routeEdge.to && edge.type === "ROUTES_TO"));
|
|
74
|
+
counts.provider = routeToEdges.length;
|
|
75
|
+
const orphan = outgoing.length === 0;
|
|
76
|
+
// Dangler detection across the run subgraph (run edges + route ROUTES_TO).
|
|
77
|
+
for (const edge of [...outgoing, ...routeToEdges]) {
|
|
78
|
+
if (!nodeIds.has(edge.from))
|
|
79
|
+
danglers.push({ edgeId: edge.id, type: edge.type, from: edge.from, to: edge.to, missing: "from" });
|
|
80
|
+
if (!nodeIds.has(edge.to))
|
|
81
|
+
danglers.push({ edgeId: edge.id, type: edge.type, from: edge.from, to: edge.to, missing: "to" });
|
|
82
|
+
}
|
|
83
|
+
if (manifest) {
|
|
84
|
+
const evidenceNode = outgoing
|
|
85
|
+
.filter((edge) => edge.type === "HAS_EVIDENCE")
|
|
86
|
+
.map((edge) => state.nodes.find((node) => node.id === edge.to))
|
|
87
|
+
.find((node) => node !== undefined);
|
|
88
|
+
if (evidenceNode) {
|
|
89
|
+
for (const field of EVIDENCE_SUMMARY_FIELDS) {
|
|
90
|
+
const graphValue = Number(evidenceNode.properties[field] ?? Number.NaN);
|
|
91
|
+
const manifestValue = manifest.evidenceSummary[field];
|
|
92
|
+
if (graphValue !== manifestValue) {
|
|
93
|
+
mismatches.push({ field: `evidence.${field}`, graph: Number.isNaN(graphValue) ? null : graphValue, manifest: manifestValue });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
notes.push("evidence node missing for cross-check");
|
|
99
|
+
}
|
|
100
|
+
if (counts.artifact !== manifest.artifacts.length) {
|
|
101
|
+
mismatches.push({ field: "artifact.count", graph: counts.artifact, manifest: manifest.artifacts.length });
|
|
102
|
+
}
|
|
103
|
+
const graphStatus = String(runNode.properties.status ?? "");
|
|
104
|
+
if (graphStatus !== manifest.status) {
|
|
105
|
+
mismatches.push({ field: "run.status", graph: graphStatus, manifest: manifest.status });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
notes.push("run-manifest.json not found; structural audit only");
|
|
110
|
+
}
|
|
111
|
+
const hasAllEdges = counts.providerRoute >= 1 &&
|
|
112
|
+
counts.provider >= 1 &&
|
|
113
|
+
counts.evidence >= 1 &&
|
|
114
|
+
counts.decision >= 1 &&
|
|
115
|
+
counts.artifact >= 1;
|
|
116
|
+
let verdict;
|
|
117
|
+
if (orphan || !hasAllEdges)
|
|
118
|
+
verdict = "failed";
|
|
119
|
+
else if (danglers.length > 0 || mismatches.length > 0 || !manifestPresent)
|
|
120
|
+
verdict = "partial";
|
|
121
|
+
else
|
|
122
|
+
verdict = "passed";
|
|
123
|
+
return { runId, runNodeFound: true, orphan, manifestPresent, counts, mismatches, danglers, verdict, notes };
|
|
124
|
+
}
|
|
125
|
+
async function loadGraphStateForAudit(statePath) {
|
|
126
|
+
try {
|
|
127
|
+
const raw = await readFile(statePath, "utf-8");
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
if (Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)) {
|
|
130
|
+
return { ...parsed, nodes: parsed.nodes, edges: parsed.edges };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// fall through to an empty state
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
version: 1,
|
|
138
|
+
ontology: { version: "", classes: [], relationTypes: [], description: "" },
|
|
139
|
+
project: { key: "", name: "", root: "" },
|
|
140
|
+
updatedAt: new Date().toISOString(),
|
|
141
|
+
nodes: [],
|
|
142
|
+
edges: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function loadRunManifestForAudit(runId, root) {
|
|
146
|
+
try {
|
|
147
|
+
const raw = await readFile(getRunArtifactPath(runId, "run-manifest.json", root), "utf-8");
|
|
148
|
+
const parsed = RunManifestSchema.safeParse(JSON.parse(raw));
|
|
149
|
+
return parsed.success ? parsed.data : null;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export async function graphAuditCommand(options = {}) {
|
|
156
|
+
const startedAt = Date.now();
|
|
157
|
+
const root = getProjectRoot();
|
|
158
|
+
const statePath = options.input ? resolve(root, options.input) : join(root, ".omk", "memory", "graph-state.json");
|
|
159
|
+
const state = await loadGraphStateForAudit(statePath);
|
|
160
|
+
const runIds = options.run ? [validateRunId(options.run)] : await listValidRunIds(root);
|
|
161
|
+
const entries = [];
|
|
162
|
+
for (const runId of runIds) {
|
|
163
|
+
entries.push({ runId, manifest: await loadRunManifestForAudit(runId, root) });
|
|
164
|
+
}
|
|
165
|
+
const report = auditGraphRuns(state, entries, statePath);
|
|
166
|
+
if (options.json) {
|
|
167
|
+
console.log(JSON.stringify(createOmkJsonEnvelope({
|
|
168
|
+
command: "graph",
|
|
169
|
+
status: report.verdict,
|
|
170
|
+
data: report,
|
|
171
|
+
durationMs: Date.now() - startedAt,
|
|
172
|
+
})));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
printGraphAuditReport(report);
|
|
176
|
+
if (report.verdict === "failed")
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
}
|
|
179
|
+
function printGraphAuditReport(report) {
|
|
180
|
+
console.log(header("OMK Graph Audit"));
|
|
181
|
+
if (report.statePath)
|
|
182
|
+
console.log(label("State", report.statePath));
|
|
183
|
+
console.log(label("Runs", `${report.summary.total} (passed ${report.summary.passed}, partial ${report.summary.partial}, failed ${report.summary.failed})`));
|
|
184
|
+
for (const run of report.runs) {
|
|
185
|
+
const line = `${run.runId} — routes ${run.counts.providerRoute}/providers ${run.counts.provider}/evidence ${run.counts.evidence}/decisions ${run.counts.decision}/artifacts ${run.counts.artifact}`;
|
|
186
|
+
if (run.verdict === "passed")
|
|
187
|
+
console.log(status.ok(line));
|
|
188
|
+
else if (run.verdict === "partial")
|
|
189
|
+
console.log(status.warn(line));
|
|
190
|
+
else
|
|
191
|
+
console.log(status.fail(line));
|
|
192
|
+
for (const mismatch of run.mismatches) {
|
|
193
|
+
console.log(label(" mismatch", `${mismatch.field}: graph=${String(mismatch.graph)} manifest=${String(mismatch.manifest)}`));
|
|
194
|
+
}
|
|
195
|
+
for (const dangler of run.danglers) {
|
|
196
|
+
console.log(label(" dangler", `${dangler.type} missing ${dangler.missing} (${dangler.from} -> ${dangler.to})`));
|
|
197
|
+
}
|
|
198
|
+
for (const note of run.notes)
|
|
199
|
+
console.log(label(" note", note));
|
|
200
|
+
}
|
|
201
|
+
const verdictLine = `Verdict: ${report.verdict}`;
|
|
202
|
+
if (report.verdict === "passed")
|
|
203
|
+
console.log(status.ok(verdictLine));
|
|
204
|
+
else if (report.verdict === "partial")
|
|
205
|
+
console.log(status.warn(verdictLine));
|
|
206
|
+
else
|
|
207
|
+
console.log(status.fail(verdictLine));
|
|
208
|
+
}
|