@minhpnq1807/contextos 0.5.8 → 0.5.10
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 +11 -0
- package/README.md +25 -5
- package/bin/ctx.js +35 -3
- package/package.json +1 -1
- package/plugins/ctx/lib/benchmark.js +18 -11
- package/plugins/ctx/lib/prompt-hook.js +5 -2
- package/plugins/ctx/lib/reporter.js +49 -18
- package/plugins/ctx/lib/scheduler.js +14 -1
- package/plugins/ctx/lib/score-context.js +13 -0
- package/plugins/ctx/lib/skill-discoverer.js +10 -0
- package/plugins/ctx/lib/stats.js +37 -27
- package/plugins/ctx/lib/terminal-ui.js +22 -0
- package/plugins/ctx/lib/workflow-discoverer.js +308 -0
- package/plugins/ctx/mcp/contextos-server.js +13 -1
- package/plugins/ctx/mcp/server.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.10
|
|
4
|
+
|
|
5
|
+
- Adds workflow discovery for `.claude/workflows/`, `.codex/workflows/`, `~/.claude/workflows/`, and `~/.codex/workflows/`.
|
|
6
|
+
- Adds `ctx sync --workflows` to parse markdown workflow headings, agent chains, and warm workflow embeddings.
|
|
7
|
+
- Injects prompt-relevant workflow hints into ContextOS prompt context and shows them in `ctx debug`.
|
|
8
|
+
|
|
9
|
+
## 0.5.9
|
|
10
|
+
|
|
11
|
+
- Formats `ctx report`, `ctx evidence`, `ctx stats`, and `ctx benchmark` with sectioned terminal tables for easier scanning and analysis.
|
|
12
|
+
- Adds a small shared terminal table formatter used by report, evidence, stats, and benchmark output.
|
|
13
|
+
|
|
3
14
|
## 0.5.8
|
|
4
15
|
|
|
5
16
|
- Adds explicit `ctx setup` interactive onboarding for installing agents, enabling injection, syncing Ruler rules/MCP servers, and syncing skills through skillshare.
|
package/README.md
CHANGED
|
@@ -214,6 +214,25 @@ ctx sync --skills --agents codex,claude,agy
|
|
|
214
214
|
|
|
215
215
|
After this, `ctx debug -- "task"` and prompt hooks can suggest skills from `~/.config/skillshare/skills/` plus agent-specific skill folders.
|
|
216
216
|
|
|
217
|
+
## Workflow Discovery
|
|
218
|
+
|
|
219
|
+
ContextOS can also index Claude/Codex workflow markdown files and suggest the right workflow for the current task:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
ctx sync --workflows
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
It scans project workflows first, then global workflows:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
.claude/workflows/
|
|
229
|
+
.codex/workflows/
|
|
230
|
+
~/.claude/workflows/
|
|
231
|
+
~/.codex/workflows/
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Workflow files do not need YAML frontmatter. ContextOS reads the top `#` heading, section headings, and referenced agent names such as `planner`, `tester`, `code-reviewer`, and `docs-manager`, then warms semantic embeddings. Prompt hooks inject a `Suggested workflow for this task` section only when a workflow is relevant enough.
|
|
235
|
+
|
|
217
236
|
## Modes
|
|
218
237
|
|
|
219
238
|
Injection mode is the default:
|
|
@@ -346,10 +365,10 @@ This warning comes from a transitive dependency in the local embedding/WASM stac
|
|
|
346
365
|
| `ctx setup --no-skills` | Skips skillshare sync during setup. | You do not want shared skills configured. | Does not run `ctx sync --skills`. |
|
|
347
366
|
| `ctx setup --quiet` | Runs setup in measurement-only mode. | You want reports/stats without visible injected prompt context. | Installs hooks with prompt context injection disabled. |
|
|
348
367
|
| `ctx debug -- "task"` | Runs the scheduler locally for a fake prompt. | You want to see which AGENTS.md rules and files ContextOS would inject before using Codex. | Prints rule scores, scoring reasons, suggested files, and final `additionalContext`. |
|
|
349
|
-
| `ctx report` | Shows the last Stop-hook compliance report for the current workspace. | An agent task has finished and you want the summary again. |
|
|
350
|
-
| `ctx evidence` | Shows detailed evidence behind the last report for the current workspace. | You want to inspect why a rule was marked `followed`, `ignored`, or `
|
|
351
|
-
| `ctx stats` | Shows aggregate runtime metrics for the current workspace. | You want to know whether ContextOS is active and useful over time. | Prints
|
|
352
|
-
| `ctx benchmark -- "task"` | Compares baseline AGENTS.md ordering with ContextOS task-aware scheduling. | You want a before/after signal for lost-in-the-middle risk. | Prints parsed/actionable/filtered
|
|
368
|
+
| `ctx report` | Shows the last Stop-hook compliance report for the current workspace. | An agent task has finished and you want the summary again. | Prints sectioned tables for summary, rule outcomes, suggested files, and runtime telemetry from `~/.ctx/contextos/workspaces/<workspace-id>/last-report.json`. |
|
|
369
|
+
| `ctx evidence` | Shows detailed evidence behind the last report for the current workspace. | You want to inspect why a rule was marked `followed`, `ignored`, `unknown`, or `unmeasurable`. | Prints a compact evidence table plus per-rule detail tables. |
|
|
370
|
+
| `ctx stats` | Shows aggregate runtime metrics for the current workspace. | You want to know whether ContextOS is active and useful over time. | Prints sectioned tables for prompt/report counts, injection rate, efficiency, rule outcomes, hook events, last prompt, and last report. |
|
|
371
|
+
| `ctx benchmark -- "task"` | Compares baseline AGENTS.md ordering with ContextOS task-aware scheduling. | You want a before/after signal for lost-in-the-middle risk. | Prints tables for parsed/actionable/filtered rules, baseline middle-risk, scheduled high/mid rules, recency reminder status, and top scored rules. |
|
|
353
372
|
| `ctx sync --rules` | Syncs project rules and MCP servers through Ruler. | You want Codex, Claude Code, and Antigravity to share one project rule/MCP source of truth. | Ensures `.ruler/ruler.toml`, injects `ctx-mcp`, imports existing MCP servers from Codex and project `.mcp.json`, runs `ruler apply --agents codex,claude,antigravity`, mirrors MCP servers to Antigravity MCP configs, and verifies generated config. |
|
|
354
373
|
| `ctx sync --rules --agents <list>` | Syncs only selected agents through Ruler. | You want to update one or two agents without touching the others. | Accepts comma-separated values such as `codex`, `claude`, `agy`, `antigravity`, or `codex,claude,agy`; `agy` is normalized to Ruler's `antigravity`. |
|
|
355
374
|
| `ctx sync --rules --dry-run` | Previews Ruler sync without writing files or running apply. | You want to inspect behavior before changing project config. | Prints the same flow with dry-run status. |
|
|
@@ -359,7 +378,8 @@ This warning comes from a transitive dependency in the local embedding/WASM stac
|
|
|
359
378
|
| `ctx sync --skills --agents <list>` | Syncs skills only for selected agents. | You want to target a subset such as `codex,claude` or `codex,claude,agy`. | Runs `skillshare sync --agents <list>` with `agy` normalized to `antigravity`, then refreshes skill embeddings. |
|
|
360
379
|
| `ctx sync --skills --dry-run` | Previews skillshare sync. | You want to inspect behavior before changing skill directories. | Runs `skillshare sync --dry-run` and skips embedding rebuild. |
|
|
361
380
|
| `ctx sync --skills --no-collect` | Skips collecting existing agent skills into skillshare. | You already manage `~/.config/skillshare/skills` and only want to push it out. | Initializes/syncs skillshare without running `skillshare backup` or `skillshare collect --all`. |
|
|
362
|
-
| `ctx
|
|
381
|
+
| `ctx sync --workflows` | Indexes agent workflow markdown files for prompt-time workflow suggestions. | You use `.claude/workflows/` or `.codex/workflows/` and want ContextOS to suggest the best workflow chain for each task. | Scans project/global workflow folders, parses headings and agent chain mentions, warms workflow embeddings, and makes `ctx debug`/prompt hooks show relevant workflow hints. |
|
|
382
|
+
| `ctx embeddings warm -- "task"` | Prepares local semantic embedding caches. | First install, CI smoke checks, or after changing AGENTS.md/project files/skills/workflows. | Loads/downloads `Xenova/all-MiniLM-L6-v2` and writes rule, file-path, skill, and workflow vectors to `~/.ctx/contextos/embeddings.db`. |
|
|
363
383
|
| `ctx ruler -- <args>` | Forwards args to the installed `ruler` CLI. | You need native Ruler commands such as `init`, `apply`, or `revert`. | Preserves Ruler stdout/stderr and exit status. |
|
|
364
384
|
| `ctx skillshare -- <args>` | Forwards args to the installed `skillshare` CLI. | You need native skillshare commands such as `status`, `target list`, `doctor`, `push`, or `pull`. | Preserves skillshare stdout/stderr and exit status. |
|
|
365
385
|
| `ctx --version` | Prints the installed ContextOS CLI version. | You want to confirm which npm version is being executed. | Prints the version from package metadata. |
|
package/bin/ctx.js
CHANGED
|
@@ -28,6 +28,7 @@ import { syncSkills } from "../plugins/ctx/lib/skillshare-sync.js";
|
|
|
28
28
|
import { scanSkills, warmSkillEmbeddings } from "../plugins/ctx/lib/skill-discoverer.js";
|
|
29
29
|
import { parsePassthroughArgs, runPassthrough } from "../plugins/ctx/lib/passthrough.js";
|
|
30
30
|
import { parseAgentList, parseSetupArgs, setupSummaryLines } from "../plugins/ctx/lib/setup-wizard.js";
|
|
31
|
+
import { syncWorkflows, warmWorkflowEmbeddings } from "../plugins/ctx/lib/workflow-discoverer.js";
|
|
31
32
|
|
|
32
33
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
33
34
|
const rootDir = path.resolve(__dirname, "..");
|
|
@@ -61,6 +62,7 @@ Usage:
|
|
|
61
62
|
ctx sync --rules --dry-run
|
|
62
63
|
ctx sync --rules --no-import-codex-mcp
|
|
63
64
|
ctx sync --skills
|
|
65
|
+
ctx sync --workflows
|
|
64
66
|
ctx sync --skills --dry-run
|
|
65
67
|
ctx sync --skills --no-collect
|
|
66
68
|
ctx sync --skills --agents codex,claude,antigravity
|
|
@@ -186,6 +188,7 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
|
|
|
186
188
|
console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
|
|
187
189
|
console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
|
|
188
190
|
console.log(`Skill embeddings warmed: ${warmResult.skillCount || 0}`);
|
|
191
|
+
console.log(`Workflow embeddings warmed: ${warmResult.workflowCount || 0}`);
|
|
189
192
|
console.log(`Prompt context injection: ${inject ? "enabled" : "quiet logging only"}`);
|
|
190
193
|
console.log("Restart Claude Code if it was already running, then submit a task to trigger ContextOS.");
|
|
191
194
|
return;
|
|
@@ -209,6 +212,7 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
|
|
|
209
212
|
console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
|
|
210
213
|
console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
|
|
211
214
|
console.log(`Skill embeddings warmed: ${warmResult.skillCount || 0}`);
|
|
215
|
+
console.log(`Workflow embeddings warmed: ${warmResult.workflowCount || 0}`);
|
|
212
216
|
console.log(`Prompt context injection: ${inject ? "enabled" : "quiet logging only"}`);
|
|
213
217
|
console.log("Restart Antigravity or agy if it was already running, then submit a task to trigger ContextOS.");
|
|
214
218
|
return;
|
|
@@ -247,6 +251,7 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
|
|
|
247
251
|
console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
|
|
248
252
|
console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
|
|
249
253
|
console.log(`Skill embeddings warmed: ${warmResult.skillCount || 0}`);
|
|
254
|
+
console.log(`Workflow embeddings warmed: ${warmResult.workflowCount || 0}`);
|
|
250
255
|
console.log(`Prompt context injection: ${inject ? "enabled" : "quiet logging only"}`);
|
|
251
256
|
console.log("Restart Codex if it was already running, then submit a task to trigger ContextOS.");
|
|
252
257
|
} catch (error) {
|
|
@@ -282,7 +287,12 @@ async function warmInstallEmbeddings() {
|
|
|
282
287
|
dataDir,
|
|
283
288
|
allowRemote: !modelReady
|
|
284
289
|
});
|
|
285
|
-
|
|
290
|
+
const workflowResult = await warmWorkflowEmbeddings({
|
|
291
|
+
cwd: process.cwd(),
|
|
292
|
+
dataDir,
|
|
293
|
+
allowRemote: !modelReady
|
|
294
|
+
});
|
|
295
|
+
return { ...result, modelAlreadyCached: modelReady, fileCount: fileResult.count, skillCount: skillResult.count, workflowCount: workflowResult.count };
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
function tryRunCodex(args) {
|
|
@@ -340,7 +350,8 @@ async function debug(task) {
|
|
|
340
350
|
const rules = scored.scoredRules;
|
|
341
351
|
const relevantFiles = scored.suggestedFiles.slice(0, 3);
|
|
342
352
|
const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
|
|
343
|
-
const
|
|
353
|
+
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, 2);
|
|
354
|
+
const scheduled = scheduleContext({ rules, relevantFiles, suggestedSkills, suggestedWorkflows });
|
|
344
355
|
|
|
345
356
|
console.log("ContextOS debug");
|
|
346
357
|
console.log(`cwd: ${cwd}`);
|
|
@@ -372,6 +383,15 @@ async function debug(task) {
|
|
|
372
383
|
}
|
|
373
384
|
if (!suggestedSkills.length) console.log("(none)");
|
|
374
385
|
console.log("");
|
|
386
|
+
console.log("Suggested workflows:");
|
|
387
|
+
for (const workflow of suggestedWorkflows) {
|
|
388
|
+
const score = Number(workflow.score || 0).toFixed(2);
|
|
389
|
+
const chain = workflow.chain?.length ? ` chain:${workflow.chain.join(" -> ")}` : "";
|
|
390
|
+
const location = workflow.relativePath || workflow.path ? ` path:${workflow.relativePath || workflow.path}` : "";
|
|
391
|
+
console.log(`${score} ${workflow.title || workflow.name}${chain}${location}`);
|
|
392
|
+
}
|
|
393
|
+
if (!suggestedWorkflows.length) console.log("(none)");
|
|
394
|
+
console.log("");
|
|
375
395
|
console.log("Final additionalContext:");
|
|
376
396
|
console.log(scheduled.additionalContext || "(empty)");
|
|
377
397
|
}
|
|
@@ -397,9 +417,15 @@ async function warmEmbeddings(task) {
|
|
|
397
417
|
dataDir: contextOSDataDir(),
|
|
398
418
|
allowRemote: true
|
|
399
419
|
});
|
|
420
|
+
const workflowResult = await warmWorkflowEmbeddings({
|
|
421
|
+
cwd,
|
|
422
|
+
dataDir: contextOSDataDir(),
|
|
423
|
+
allowRemote: true
|
|
424
|
+
});
|
|
400
425
|
console.log(`Warmed ${result.count} embeddings`);
|
|
401
426
|
console.log(`Warmed ${fileResult.count} file path embeddings`);
|
|
402
427
|
console.log(`Warmed ${skillResult.count} skill embeddings`);
|
|
428
|
+
console.log(`Warmed ${workflowResult.count} workflow embeddings`);
|
|
403
429
|
console.log(`Cache: ${result.cachePath}`);
|
|
404
430
|
}
|
|
405
431
|
|
|
@@ -541,7 +567,13 @@ try {
|
|
|
541
567
|
if (!task.trim()) throw new Error('Usage: ctx benchmark -- "task"');
|
|
542
568
|
console.log(formatBenchmark(benchmarkWorkspace({ cwd: process.cwd(), task })));
|
|
543
569
|
} else if (command === "sync") {
|
|
544
|
-
if (args.includes("--
|
|
570
|
+
if (args.includes("--workflows")) {
|
|
571
|
+
await syncWorkflows({
|
|
572
|
+
cwd: process.cwd(),
|
|
573
|
+
dataDir: contextOSDataDir(),
|
|
574
|
+
allowRemote: !isModelCacheReady(contextOSDataDir())
|
|
575
|
+
});
|
|
576
|
+
} else if (args.includes("--skills")) {
|
|
545
577
|
await syncSkills({
|
|
546
578
|
cwd: process.cwd(),
|
|
547
579
|
args: args.slice(1),
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseRules, filterActionableRules, scoreRules } from "./analyzer.js";
|
|
2
2
|
import { readAgentsChain } from "./reader.js";
|
|
3
3
|
import { scheduleContext } from "./scheduler.js";
|
|
4
|
+
import { section, table, truncateCell } from "./terminal-ui.js";
|
|
4
5
|
|
|
5
6
|
export function benchmarkContext({ markdown, sources = [], task = "", openFiles = [], topK = 8 } = {}) {
|
|
6
7
|
const parsedRules = parseRules(markdown);
|
|
@@ -55,18 +56,24 @@ export function benchmarkWorkspace({ cwd = process.cwd(), task = "", openFiles =
|
|
|
55
56
|
export function formatBenchmark(result) {
|
|
56
57
|
const lines = [];
|
|
57
58
|
lines.push("ContextOS benchmark");
|
|
58
|
-
lines.push(
|
|
59
|
-
lines.push(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
lines.push(section("Summary"));
|
|
60
|
+
lines.push(table(["Metric", "Value"], [
|
|
61
|
+
["Task", truncateCell(result.task || "(empty)", 100)],
|
|
62
|
+
["Rules parsed", result.rulesParsed],
|
|
63
|
+
["Actionable rules", result.actionableRules],
|
|
64
|
+
["Filtered rules", result.filteredRules],
|
|
65
|
+
["Relevant rules", result.relevantRules],
|
|
66
|
+
["Baseline middle-risk", `${result.baseline.relevantRulesInMiddle}/${result.relevantRules} relevant rules (${result.baseline.middleRiskPercent}%)`],
|
|
67
|
+
["ContextOS scheduled", `${result.contextOS.highRules} high, ${result.contextOS.midRules} mid`],
|
|
68
|
+
["Recency reminder", result.contextOS.repeatsHighRulesAtEnd ? "enabled" : "not needed"]
|
|
69
|
+
]));
|
|
64
70
|
if (result.contextOS.topRules.length) {
|
|
65
|
-
lines.push("Top
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
lines.push(section("Top Rules"));
|
|
72
|
+
lines.push(table(["Score", "Rule", "Reasons"], result.contextOS.topRules.map((rule) => [
|
|
73
|
+
Number(rule.score || 0).toFixed(2),
|
|
74
|
+
truncateCell(rule.content, 88),
|
|
75
|
+
truncateCell(rule.reasons?.join(", ") || "", 40)
|
|
76
|
+
])));
|
|
70
77
|
}
|
|
71
78
|
return lines.join("\n");
|
|
72
79
|
}
|
|
@@ -35,7 +35,8 @@ export async function handlePromptPayload(
|
|
|
35
35
|
const scoredRules = scored.scoredRules || [];
|
|
36
36
|
const relevantFiles = (scored.suggestedFiles || []).slice(0, 3);
|
|
37
37
|
const suggestedSkills = (scored.suggestedSkills || []).slice(0, 3);
|
|
38
|
-
const
|
|
38
|
+
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, 2);
|
|
39
|
+
const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows });
|
|
39
40
|
|
|
40
41
|
const runtime = {
|
|
41
42
|
at: now.toISOString(),
|
|
@@ -48,11 +49,13 @@ export async function handlePromptPayload(
|
|
|
48
49
|
},
|
|
49
50
|
relevantFiles,
|
|
50
51
|
suggestedSkills,
|
|
52
|
+
suggestedWorkflows,
|
|
51
53
|
telemetry: {
|
|
52
54
|
...(scored.telemetry || {}),
|
|
53
55
|
rulesInjected: (scheduled.highRules?.length || 0) + (scheduled.midRules?.length || 0),
|
|
54
56
|
filesSuggested: relevantFiles.length,
|
|
55
|
-
skillsSuggested: suggestedSkills.length
|
|
57
|
+
skillsSuggested: suggestedSkills.length,
|
|
58
|
+
workflowsSuggested: suggestedWorkflows.length
|
|
56
59
|
},
|
|
57
60
|
scheduled,
|
|
58
61
|
injected: injectContext,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isSystemUserRule } from "./analyzer.js";
|
|
2
|
+
import { section, table, truncateCell } from "./terminal-ui.js";
|
|
2
3
|
|
|
3
4
|
export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot, compliance, runtimeEvidence }) {
|
|
4
5
|
const actionableCompliance = compliance.filter((item) => !isSystemUserRule(item.rule));
|
|
@@ -33,17 +34,33 @@ export function formatReport(report) {
|
|
|
33
34
|
report = sanitizeReport(report);
|
|
34
35
|
const lines = [];
|
|
35
36
|
lines.push("ContextOS report");
|
|
36
|
-
lines.push(
|
|
37
|
-
lines.push(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
lines.push(section("Summary"));
|
|
38
|
+
lines.push(table(["Metric", "Value"], [
|
|
39
|
+
["Efficiency", report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`],
|
|
40
|
+
["Injected rules", report.injectedRuleCount || 0],
|
|
41
|
+
["Measured rules", report.measuredRuleCount ?? ((report.followed?.length || 0) + (report.ignored?.length || 0))],
|
|
42
|
+
["Changed files", report.changedFiles?.length ? report.changedFiles.length : "none detected"]
|
|
43
|
+
]));
|
|
44
|
+
|
|
45
|
+
lines.push(section("Rule Outcomes"));
|
|
46
|
+
lines.push(table(["Status", "Count"], [
|
|
47
|
+
["followed", report.followed?.length || 0],
|
|
48
|
+
["ignored", report.ignored?.length || 0],
|
|
49
|
+
["unknown", report.unknown?.length || 0],
|
|
50
|
+
["unmeasurable", report.unmeasurable?.length || 0]
|
|
51
|
+
]));
|
|
41
52
|
|
|
42
53
|
if (report.relevantFiles?.length) {
|
|
43
|
-
lines.push(
|
|
54
|
+
lines.push(section("Suggested Files"));
|
|
55
|
+
lines.push(table(["#", "Path", "Score"], report.relevantFiles.slice(0, 10).map((file, index) => [
|
|
56
|
+
index + 1,
|
|
57
|
+
truncateCell(file.path, 90),
|
|
58
|
+
typeof file.score === "number" ? file.score.toFixed(2) : ""
|
|
59
|
+
])));
|
|
44
60
|
}
|
|
45
61
|
if (report.runtimeEvidence?.signals?.length) {
|
|
46
|
-
lines.push(
|
|
62
|
+
lines.push(section("Runtime Telemetry"));
|
|
63
|
+
lines.push(table(["#", "Signal"], report.runtimeEvidence.signals.map((signal, index) => [index + 1, signal])));
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
for (const warning of report.warnings || []) lines.push(`Warning: ${warning}`);
|
|
@@ -66,9 +83,12 @@ export function formatEvidence(report) {
|
|
|
66
83
|
report = sanitizeReport(report);
|
|
67
84
|
const lines = [];
|
|
68
85
|
lines.push("ContextOS evidence");
|
|
69
|
-
lines.push(
|
|
70
|
-
lines.push(
|
|
71
|
-
|
|
86
|
+
lines.push(section("Summary"));
|
|
87
|
+
lines.push(table(["Field", "Value"], [
|
|
88
|
+
["Prompt", truncateCell(report.prompt || "(empty)", 100)],
|
|
89
|
+
["Efficiency", report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`],
|
|
90
|
+
["Changed files", report.changedFiles?.length ? report.changedFiles.join(", ") : "none detected"]
|
|
91
|
+
]));
|
|
72
92
|
|
|
73
93
|
for (const warning of report.warnings || []) lines.push(`Warning: ${warning}`);
|
|
74
94
|
|
|
@@ -85,15 +105,26 @@ export function formatEvidence(report) {
|
|
|
85
105
|
return lines.join("\n");
|
|
86
106
|
}
|
|
87
107
|
|
|
108
|
+
lines.push(section("Evidence Table"));
|
|
109
|
+
lines.push(table(["#", "Status", "Score", "Kind", "Rule", "Evidence"], items.map((item, index) => [
|
|
110
|
+
index + 1,
|
|
111
|
+
item.status.toUpperCase(),
|
|
112
|
+
typeof item.rule?.score === "number" ? item.rule.score.toFixed(2) : "",
|
|
113
|
+
item.kind || "",
|
|
114
|
+
truncateCell(item.rule?.content || "(missing rule)", 46),
|
|
115
|
+
truncateCell(item.evidence || "(none)", 58)
|
|
116
|
+
])));
|
|
117
|
+
|
|
88
118
|
items.forEach((item, index) => {
|
|
89
|
-
lines.push(
|
|
90
|
-
lines.push(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
lines.push(section(`${index + 1}. ${item.status.toUpperCase()}`));
|
|
120
|
+
lines.push(table(["Field", "Value"], [
|
|
121
|
+
["Rule", truncateCell(item.rule?.content || "(missing rule)", 120)],
|
|
122
|
+
["Source", item.rule?.sourcePath || ""],
|
|
123
|
+
["Score", typeof item.rule?.score === "number" ? item.rule.score.toFixed(2) : ""],
|
|
124
|
+
["Kind", item.kind || ""],
|
|
125
|
+
["Keywords", item.keywords?.length ? truncateCell(item.keywords.join(", "), 120) : ""],
|
|
126
|
+
["Evidence", truncateCell(item.evidence || "(none)", 120)]
|
|
127
|
+
].filter(([, value]) => value !== "")));
|
|
97
128
|
for (const line of item.matchedLines || []) {
|
|
98
129
|
const where = line.file ? `${line.file}${typeof line.line === "number" ? `:${line.line}` : ""}` : "diff";
|
|
99
130
|
lines.push(`Matched line: ${where} ${truncate(line.content || "", 140)}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const MAX_CONTEXT_CHARS = 4000;
|
|
2
2
|
|
|
3
|
-
export function scheduleContext({ rules = [], relevantFiles = [], suggestedSkills = [], maxChars = MAX_CONTEXT_CHARS } = {}) {
|
|
3
|
+
export function scheduleContext({ rules = [], relevantFiles = [], suggestedSkills = [], suggestedWorkflows = [], maxChars = MAX_CONTEXT_CHARS } = {}) {
|
|
4
4
|
const high = rules.filter((rule) => rule.score >= 0.5);
|
|
5
5
|
const mid = rules.filter((rule) => rule.score >= 0.1 && rule.score < 0.5);
|
|
6
6
|
const dropped = rules.filter((rule) => rule.score < 0.1);
|
|
@@ -15,6 +15,9 @@ export function scheduleContext({ rules = [], relevantFiles = [], suggestedSkill
|
|
|
15
15
|
if (suggestedSkills.length) {
|
|
16
16
|
sections.push(section("Skills to activate for this task", suggestedSkills.map(formatSkill)));
|
|
17
17
|
}
|
|
18
|
+
if (suggestedWorkflows.length) {
|
|
19
|
+
sections.push(section("Suggested workflow for this task", suggestedWorkflows.map(formatWorkflow)));
|
|
20
|
+
}
|
|
18
21
|
if (mid.length) {
|
|
19
22
|
sections.push(section("Additional relevant rules", mid.slice(0, 8).map(formatRule)));
|
|
20
23
|
}
|
|
@@ -29,6 +32,7 @@ export function scheduleContext({ rules = [], relevantFiles = [], suggestedSkill
|
|
|
29
32
|
droppedRules: dropped,
|
|
30
33
|
relevantFiles,
|
|
31
34
|
suggestedSkills,
|
|
35
|
+
suggestedWorkflows,
|
|
32
36
|
additionalContext
|
|
33
37
|
};
|
|
34
38
|
}
|
|
@@ -49,6 +53,15 @@ function formatSkill(skill) {
|
|
|
49
53
|
return `- ${skill.name}${description}${location}`;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
function formatWorkflow(workflow) {
|
|
57
|
+
const name = workflow.title || workflow.name;
|
|
58
|
+
const hint = workflow.hint ? `: ${workflow.hint}` : "";
|
|
59
|
+
const chain = workflow.chain?.length ? `\n chain: ${workflow.chain.join(" -> ")}` : "";
|
|
60
|
+
const location = workflow.relativePath || workflow.path;
|
|
61
|
+
const source = location ? `\n see: ${location}` : "";
|
|
62
|
+
return `- ${name}${hint}${chain}${source}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
function trimToLimit(value, maxChars) {
|
|
53
66
|
if (value.length <= maxChars) return value;
|
|
54
67
|
return `${value.slice(0, Math.max(0, maxChars - 80)).trimEnd()}\n\n[ContextOS truncated context to ${maxChars} chars]`;
|
|
@@ -4,6 +4,7 @@ import { readAgentsChain } from "./reader.js";
|
|
|
4
4
|
import { filterActionableRules, parseRules, scoreRules, findRelevantFiles } from "./analyzer.js";
|
|
5
5
|
import { enhanceRuleScoresWithEmbeddings } from "./embedding-scorer.js";
|
|
6
6
|
import { scanSkills, suggestSkills } from "./skill-discoverer.js";
|
|
7
|
+
import { scanWorkflows, suggestWorkflows } from "./workflow-discoverer.js";
|
|
7
8
|
|
|
8
9
|
export async function scoreContext({
|
|
9
10
|
cwd = process.cwd(),
|
|
@@ -12,7 +13,9 @@ export async function scoreContext({
|
|
|
12
13
|
dataDir,
|
|
13
14
|
maxFiles = 5,
|
|
14
15
|
maxSkills = 3,
|
|
16
|
+
maxWorkflows = 2,
|
|
15
17
|
skills = null,
|
|
18
|
+
workflows = null,
|
|
16
19
|
embeddingTimeoutMs = 5000,
|
|
17
20
|
fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 80)
|
|
18
21
|
} = {}) {
|
|
@@ -46,6 +49,13 @@ export async function scoreContext({
|
|
|
46
49
|
dataDir,
|
|
47
50
|
limit: maxSkills
|
|
48
51
|
});
|
|
52
|
+
const workflowCatalog = Array.isArray(workflows) ? workflows : scanWorkflows({ cwd });
|
|
53
|
+
const suggestedWorkflows = await suggestWorkflows({
|
|
54
|
+
prompt,
|
|
55
|
+
workflows: workflowCatalog,
|
|
56
|
+
dataDir,
|
|
57
|
+
limit: maxWorkflows
|
|
58
|
+
});
|
|
49
59
|
|
|
50
60
|
return {
|
|
51
61
|
cwd,
|
|
@@ -53,6 +63,7 @@ export async function scoreContext({
|
|
|
53
63
|
scoredRules,
|
|
54
64
|
suggestedFiles,
|
|
55
65
|
suggestedSkills,
|
|
66
|
+
suggestedWorkflows,
|
|
56
67
|
telemetry: {
|
|
57
68
|
elapsedMs: Date.now() - started,
|
|
58
69
|
modelStatus: embedding.status,
|
|
@@ -64,6 +75,8 @@ export async function scoreContext({
|
|
|
64
75
|
filesSuggested: suggestedFiles.length,
|
|
65
76
|
skillsScanned: skillCatalog.length,
|
|
66
77
|
skillsSuggested: suggestedSkills.length,
|
|
78
|
+
workflowsScanned: workflowCatalog.length,
|
|
79
|
+
workflowsSuggested: suggestedWorkflows.length,
|
|
67
80
|
sources: merged.sources.map((source) => path.relative(cwd, source))
|
|
68
81
|
}
|
|
69
82
|
};
|
|
@@ -8,6 +8,9 @@ const DEFAULT_LIMIT = 3;
|
|
|
8
8
|
const DEFAULT_MAX_SKILLS = 2000;
|
|
9
9
|
const DEFAULT_EMBEDDING_CANDIDATES = 120;
|
|
10
10
|
const DEFAULT_SEMANTIC_CATALOG_LIMIT = 300;
|
|
11
|
+
const SCAN_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
const scanCache = new Map();
|
|
11
14
|
|
|
12
15
|
export function skillSearchRoots({ cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
13
16
|
return [
|
|
@@ -60,6 +63,12 @@ function firstParagraph(body) {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
export function scanSkills({ cwd = process.cwd(), roots = skillSearchRoots({ cwd }), maxSkills = DEFAULT_MAX_SKILLS } = {}) {
|
|
66
|
+
const cacheKey = `${path.resolve(cwd)}\0${maxSkills}\0${roots.map((root) => path.resolve(root)).join("\0")}`;
|
|
67
|
+
const cached = scanCache.get(cacheKey);
|
|
68
|
+
if (cached && Date.now() - cached.createdAt < SCAN_CACHE_TTL_MS) {
|
|
69
|
+
return cached.skills;
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
const skills = [];
|
|
64
73
|
const seen = new Set();
|
|
65
74
|
for (const root of roots) {
|
|
@@ -87,6 +96,7 @@ export function scanSkills({ cwd = process.cwd(), roots = skillSearchRoots({ cwd
|
|
|
87
96
|
});
|
|
88
97
|
}
|
|
89
98
|
}
|
|
99
|
+
scanCache.set(cacheKey, { createdAt: Date.now(), skills });
|
|
90
100
|
return skills;
|
|
91
101
|
}
|
|
92
102
|
|
package/plugins/ctx/lib/stats.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { safeReadText } from "./fs-utils.js";
|
|
5
|
+
import { section, table, truncateCell } from "./terminal-ui.js";
|
|
5
6
|
|
|
6
7
|
function readJsonLines(filePath) {
|
|
7
8
|
return safeReadText(filePath)
|
|
@@ -81,33 +82,47 @@ export function loadStats(dataDir) {
|
|
|
81
82
|
export function formatStats(stats) {
|
|
82
83
|
const lines = [];
|
|
83
84
|
lines.push("ContextOS stats");
|
|
84
|
-
lines.push(
|
|
85
|
-
lines.push(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
lines.push(
|
|
85
|
+
lines.push(section("Summary"));
|
|
86
|
+
lines.push(table(["Metric", "Value"], [
|
|
87
|
+
["Data dir", stats.dataDir],
|
|
88
|
+
["Prompts analyzed", stats.promptCount],
|
|
89
|
+
["Reports generated", stats.reportCount],
|
|
90
|
+
["Prompt mode", `${stats.injectedCount} injected, ${stats.quietCount} quiet (${stats.injectionRate}% injected)`],
|
|
91
|
+
["Average prompt analysis", stats.averagePromptMs == null ? "unknown" : `${stats.averagePromptMs}ms`],
|
|
92
|
+
["Average efficiency", formatAverageEfficiency(stats)]
|
|
93
|
+
]));
|
|
94
|
+
|
|
95
|
+
lines.push(section("Rule Outcomes"));
|
|
96
|
+
lines.push(table(["Status", "Count"], [
|
|
97
|
+
["followed", stats.followed],
|
|
98
|
+
["ignored", stats.ignored],
|
|
99
|
+
["unknown", stats.unknown],
|
|
100
|
+
["unmeasurable", stats.unmeasurable || 0]
|
|
101
|
+
]));
|
|
102
|
+
|
|
103
|
+
lines.push(section("Hook Events"));
|
|
104
|
+
lines.push(Object.keys(stats.events).length
|
|
105
|
+
? table(["Event", "Count"], Object.entries(stats.events))
|
|
106
|
+
: "none");
|
|
96
107
|
|
|
97
108
|
if (stats.lastPrompt) {
|
|
98
|
-
lines.push(
|
|
99
|
-
lines.push(
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
lines.push(section("Last Prompt"));
|
|
110
|
+
lines.push(table(["Field", "Value"], [
|
|
111
|
+
["Prompt", truncateCell(stats.lastPrompt.prompt || "(empty)", 100)],
|
|
112
|
+
["Scheduled rules", scheduledRuleCount(stats.lastPrompt)],
|
|
113
|
+
["Suggested files", (stats.lastPrompt.relevantFiles || []).map((file) => file.path).join(", ") || "none"]
|
|
114
|
+
]));
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
if (stats.lastReport) {
|
|
105
|
-
lines.push(
|
|
106
|
-
lines.push(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
lines.push(section("Last Report"));
|
|
119
|
+
lines.push(table(["Metric", "Value"], [
|
|
120
|
+
["Efficiency", stats.lastReport.efficiencyScore == null ? "unknown" : `${stats.lastReport.efficiencyScore}%`],
|
|
121
|
+
["Measured rules", stats.lastReport.measuredRuleCount ?? ((stats.lastReport.followed?.length || 0) + (stats.lastReport.ignored?.length || 0))],
|
|
122
|
+
["Unknown rules", stats.lastReport.unknownRuleCount ?? (stats.lastReport.unknown?.length || 0)],
|
|
123
|
+
["Unmeasurable rules", stats.lastReport.unmeasurableRuleCount ?? (stats.lastReport.unmeasurable?.length || 0)],
|
|
124
|
+
["Changed files", stats.lastReport.changedFiles?.join(", ") || "none"]
|
|
125
|
+
]));
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
return lines.join("\n");
|
|
@@ -123,8 +138,3 @@ function formatAverageEfficiency(stats) {
|
|
|
123
138
|
function scheduledRuleCount(prompt) {
|
|
124
139
|
return (prompt.scheduled?.highRules?.length || 0) + (prompt.scheduled?.midRules?.length || 0);
|
|
125
140
|
}
|
|
126
|
-
|
|
127
|
-
function truncateLine(value, max) {
|
|
128
|
-
const normalized = String(value).replace(/\s+/g, " ").trim();
|
|
129
|
-
return normalized.length > max ? `${normalized.slice(0, max - 3)}...` : normalized;
|
|
130
|
-
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function section(title) {
|
|
2
|
+
return `\n${title}\n${"-".repeat(title.length)}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function table(headers, rows) {
|
|
6
|
+
const normalizedRows = rows.map((row) => row.map((cell) => String(cell ?? "")));
|
|
7
|
+
const widths = headers.map((header, index) => Math.max(
|
|
8
|
+
String(header).length,
|
|
9
|
+
...normalizedRows.map((row) => row[index]?.length || 0)
|
|
10
|
+
));
|
|
11
|
+
const formatRow = (row) => row.map((cell, index) => String(cell ?? "").padEnd(widths[index])).join(" ");
|
|
12
|
+
return [
|
|
13
|
+
formatRow(headers),
|
|
14
|
+
widths.map((width) => "-".repeat(width)).join(" "),
|
|
15
|
+
...normalizedRows.map(formatRow)
|
|
16
|
+
].join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function truncateCell(value, max = 80) {
|
|
20
|
+
const normalized = String(value || "").replace(/\s+/g, " ").trim();
|
|
21
|
+
return normalized.length > max ? `${normalized.slice(0, max - 3)}...` : normalized;
|
|
22
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { enhanceRuleScoresWithEmbeddings, warmRuleEmbeddings } from "./embedding-scorer.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LIMIT = 2;
|
|
8
|
+
const MIN_WORKFLOW_BYTES = 100;
|
|
9
|
+
const MAX_DESCRIPTION_CHARS = 500;
|
|
10
|
+
const DEFAULT_EMBEDDING_CANDIDATES = 40;
|
|
11
|
+
const KNOWN_AGENT_NAMES = new Set([
|
|
12
|
+
"planner",
|
|
13
|
+
"tester",
|
|
14
|
+
"code-reviewer",
|
|
15
|
+
"docs-manager",
|
|
16
|
+
"debugger",
|
|
17
|
+
"researcher",
|
|
18
|
+
"project-manager",
|
|
19
|
+
"mcp-manager",
|
|
20
|
+
"database-admin",
|
|
21
|
+
"ui-ux-designer",
|
|
22
|
+
"copywriter",
|
|
23
|
+
"scout",
|
|
24
|
+
"scout-external",
|
|
25
|
+
"journal-writer",
|
|
26
|
+
"git-manager",
|
|
27
|
+
"brainstormer"
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function workflowSearchRoots({ cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
31
|
+
return [
|
|
32
|
+
path.join(cwd, ".claude", "workflows"),
|
|
33
|
+
path.join(cwd, ".codex", "workflows"),
|
|
34
|
+
path.join(home, ".claude", "workflows"),
|
|
35
|
+
path.join(home, ".codex", "workflows")
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function scanWorkflows({ cwd = process.cwd(), roots = workflowSearchRoots({ cwd }) } = {}) {
|
|
40
|
+
const workflows = [];
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (const root of roots) {
|
|
43
|
+
let entries = [];
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
51
|
+
const filePath = path.join(root, entry.name);
|
|
52
|
+
const realPath = safeRealpath(filePath) || filePath;
|
|
53
|
+
if (seen.has(realPath)) continue;
|
|
54
|
+
seen.add(realPath);
|
|
55
|
+
const workflow = parseWorkflowFile(filePath, { cwd, root });
|
|
56
|
+
if (workflow) workflows.push(workflow);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return workflows;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseWorkflowFile(filePath, { cwd = process.cwd(), root = path.dirname(filePath) } = {}) {
|
|
63
|
+
let stat;
|
|
64
|
+
let content;
|
|
65
|
+
try {
|
|
66
|
+
stat = fs.statSync(filePath);
|
|
67
|
+
if (stat.size < MIN_WORKFLOW_BYTES) return null;
|
|
68
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const body = stripFrontmatter(content);
|
|
74
|
+
const title = extractTitle(body) || titleFromFile(filePath);
|
|
75
|
+
const sectionTitles = extractSectionTitles(body);
|
|
76
|
+
const chain = extractAgentChain(body);
|
|
77
|
+
const description = buildWorkflowDescription({ title, sectionTitles, chain, body });
|
|
78
|
+
if (!description) return null;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name: path.basename(filePath, ".md"),
|
|
82
|
+
title,
|
|
83
|
+
description,
|
|
84
|
+
chain,
|
|
85
|
+
path: filePath,
|
|
86
|
+
relativePath: path.relative(cwd, filePath),
|
|
87
|
+
root,
|
|
88
|
+
scope: isInsidePath(filePath, cwd) ? "project" : "global",
|
|
89
|
+
mtime: Math.round(stat.mtimeMs)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function suggestWorkflows({
|
|
94
|
+
prompt = "",
|
|
95
|
+
workflows = [],
|
|
96
|
+
dataDir,
|
|
97
|
+
limit = DEFAULT_LIMIT,
|
|
98
|
+
timeoutMs = Number(process.env.CONTEXTOS_WORKFLOW_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || 800)
|
|
99
|
+
} = {}) {
|
|
100
|
+
if (!String(prompt || "").trim() || !workflows.length) return [];
|
|
101
|
+
const base = scoreWorkflowsByKeyword({ prompt, workflows });
|
|
102
|
+
const embeddingCandidates = selectWorkflowEmbeddingCandidates(base);
|
|
103
|
+
if (!embeddingCandidates.length) return [];
|
|
104
|
+
|
|
105
|
+
const embedding = await enhanceRuleScoresWithEmbeddings(embeddingCandidates, prompt, {
|
|
106
|
+
dataDir,
|
|
107
|
+
sources: embeddingCandidates.map((workflow) => workflow.path).filter(Boolean),
|
|
108
|
+
timeoutMs,
|
|
109
|
+
allowRemote: false
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return finalizeWorkflowScores(embedding.rules, limit);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function selectWorkflowEmbeddingCandidates(workflows) {
|
|
116
|
+
return workflows
|
|
117
|
+
.filter((workflow) => Number(workflow.keywordScore || 0) > 0)
|
|
118
|
+
.sort((a, b) => Number(b.keywordScore || 0) - Number(a.keywordScore || 0) || a.name.localeCompare(b.name))
|
|
119
|
+
.slice(0, DEFAULT_EMBEDDING_CANDIDATES);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function warmWorkflowEmbeddings({
|
|
123
|
+
cwd = process.cwd(),
|
|
124
|
+
dataDir,
|
|
125
|
+
allowRemote = true,
|
|
126
|
+
workflows = scanWorkflows({ cwd })
|
|
127
|
+
} = {}) {
|
|
128
|
+
if (!dataDir || !workflows.length) return { count: 0, cachePath: null };
|
|
129
|
+
return warmRuleEmbeddings({
|
|
130
|
+
rules: workflows.map((workflow) => ({ content: workflowEmbeddingText(workflow) })),
|
|
131
|
+
task: "workflow discovery semantic retrieval feature implementation documentation testing",
|
|
132
|
+
dataDir,
|
|
133
|
+
sources: workflows.map((workflow) => workflow.path).filter(Boolean),
|
|
134
|
+
allowRemote
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function syncWorkflows({
|
|
139
|
+
cwd = process.cwd(),
|
|
140
|
+
dataDir,
|
|
141
|
+
allowRemote = true,
|
|
142
|
+
logger = console.log
|
|
143
|
+
} = {}) {
|
|
144
|
+
const workflows = scanWorkflows({ cwd });
|
|
145
|
+
logger("ContextOS workflow index");
|
|
146
|
+
logger(`Found workflows: ${workflows.length}`);
|
|
147
|
+
if (workflows.length) {
|
|
148
|
+
for (const workflow of workflows) {
|
|
149
|
+
logger(`- ${workflow.relativePath || workflow.path} (${workflow.chain.join(" -> ") || "no chain"})`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const result = await warmWorkflowEmbeddings({ cwd, dataDir, allowRemote, workflows });
|
|
153
|
+
logger(`Indexed workflows: ${workflows.length}`);
|
|
154
|
+
if (result.cachePath) logger(`Cache: ${result.cachePath}`);
|
|
155
|
+
return { workflows, embeddings: result };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stripFrontmatter(content) {
|
|
159
|
+
return String(content || "").replace(/^---\s*\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$)/, "");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function extractTitle(content) {
|
|
163
|
+
const match = String(content || "").match(/^#\s+(.+?)\s*$/m);
|
|
164
|
+
return match ? match[1].trim() : "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function extractSectionTitles(content) {
|
|
168
|
+
return [...String(content || "").matchAll(/^#{3,4}\s+(.+?)\s*$/gm)]
|
|
169
|
+
.map((match) => match[1].replace(/^\d+\.\s*/, "").trim())
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractAgentChain(content) {
|
|
174
|
+
const names = [];
|
|
175
|
+
const add = (value) => {
|
|
176
|
+
const normalized = normalizeAgentName(value);
|
|
177
|
+
if (!normalized || !KNOWN_AGENT_NAMES.has(normalized) || names.includes(normalized)) return;
|
|
178
|
+
names.push(normalized);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (const match of String(content || "").matchAll(/`([a-z][a-z0-9-]*(?:-agent)?)`/gi)) {
|
|
182
|
+
add(match[1]);
|
|
183
|
+
}
|
|
184
|
+
for (const name of KNOWN_AGENT_NAMES) {
|
|
185
|
+
if (new RegExp(`\\b${escapeRegExp(name)}\\b`, "i").test(content)) add(name);
|
|
186
|
+
}
|
|
187
|
+
return names;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildWorkflowDescription({ title, sectionTitles, chain, body }) {
|
|
191
|
+
const parts = [
|
|
192
|
+
title,
|
|
193
|
+
sectionTitles.join(", "),
|
|
194
|
+
chain.length ? `delegates to ${chain.join(", ")} agents` : "",
|
|
195
|
+
firstParagraph(body)
|
|
196
|
+
].filter(Boolean);
|
|
197
|
+
return parts.join(" — ").replace(/\s+/g, " ").trim().slice(0, MAX_DESCRIPTION_CHARS);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function firstParagraph(body) {
|
|
201
|
+
return String(body || "")
|
|
202
|
+
.split(/\n\s*\n/)
|
|
203
|
+
.filter((part) => !/^\s*#/.test(part))
|
|
204
|
+
.map((part) => part.replace(/^#+\s*/gm, "").replace(/[*_`#>-]/g, "").replace(/\s+/g, " ").trim())
|
|
205
|
+
.find(Boolean) || "";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function titleFromFile(filePath) {
|
|
209
|
+
return path.basename(filePath, ".md").split(/[-_]+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function scoreWorkflowsByKeyword({ prompt, workflows }) {
|
|
213
|
+
const normalizedPrompt = normalize(prompt);
|
|
214
|
+
const promptTokens = new Set(normalizedPrompt.split(/\s+/).filter(Boolean));
|
|
215
|
+
return workflows.map((workflow, index) => {
|
|
216
|
+
const content = workflowEmbeddingText(workflow);
|
|
217
|
+
const workflowTokens = new Set(normalize(content).split(/\s+/).filter(Boolean));
|
|
218
|
+
const matches = [...workflowTokens].filter((token) => promptTokens.has(token) && token.length > 2);
|
|
219
|
+
const nameHit = normalizedPrompt.includes(normalize(workflow.name)) || normalizedPrompt.includes(normalize(workflow.title));
|
|
220
|
+
const chainHit = workflow.chain.some((agent) => normalizedPrompt.includes(normalize(agent)));
|
|
221
|
+
const actionBonus = actionIntentBonus(normalizedPrompt, workflow);
|
|
222
|
+
const scopeBonus = workflow.scope === "project" ? 0.08 : 0;
|
|
223
|
+
const score = Math.min(1, (matches.length ? 0.22 + matches.length * 0.08 : 0) + (nameHit ? 0.22 : 0) + (chainHit ? 0.16 : 0) + actionBonus + scopeBonus);
|
|
224
|
+
return {
|
|
225
|
+
id: `workflow-${index + 1}`,
|
|
226
|
+
...workflow,
|
|
227
|
+
content,
|
|
228
|
+
score,
|
|
229
|
+
keywordScore: score,
|
|
230
|
+
reasons: [
|
|
231
|
+
...(matches.length ? [`keyword:${matches.slice(0, 5).join(",")}`] : []),
|
|
232
|
+
...(nameHit ? ["name-match"] : []),
|
|
233
|
+
...(chainHit ? ["chain-match"] : []),
|
|
234
|
+
...(actionBonus ? ["workflow-intent"] : [])
|
|
235
|
+
],
|
|
236
|
+
originalOrder: index
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function actionIntentBonus(normalizedPrompt, workflow) {
|
|
242
|
+
const name = normalize(`${workflow.name} ${workflow.title} ${workflow.description}`);
|
|
243
|
+
const implementationIntent = /\b(implement|feature|build|create|fix|debug|test|ci|failing)\b/.test(normalizedPrompt);
|
|
244
|
+
const docsIntent = /\b(doc|docs|documentation|readme|changelog|roadmap)\b/.test(normalizedPrompt);
|
|
245
|
+
const orchestrationIntent = /\b(parallel|sequential|chain|delegate|agent|subagent|orchestrat)\b/.test(normalizedPrompt);
|
|
246
|
+
if (implementationIntent && /\b(primary|implementation|testing|debugging|quality)\b/.test(name)) return 0.35;
|
|
247
|
+
if (docsIntent && /\b(documentation|docs|changelog|roadmap)\b/.test(name)) return 0.35;
|
|
248
|
+
if (orchestrationIntent && /\b(orchestration|parallel|sequential|chaining)\b/.test(name)) return 0.35;
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function finalizeWorkflowScores(workflows, limit) {
|
|
253
|
+
return workflows
|
|
254
|
+
.map((workflow) => ({
|
|
255
|
+
name: workflow.name,
|
|
256
|
+
title: workflow.title,
|
|
257
|
+
description: workflow.description,
|
|
258
|
+
chain: workflow.chain || [],
|
|
259
|
+
path: workflow.path,
|
|
260
|
+
relativePath: workflow.relativePath,
|
|
261
|
+
scope: workflow.scope,
|
|
262
|
+
keywordScore: workflow.keywordScore,
|
|
263
|
+
score: Math.min(1, Number(workflow.score || 0)),
|
|
264
|
+
embeddingScore: workflow.embeddingScore,
|
|
265
|
+
reasons: workflow.reasons || [],
|
|
266
|
+
hint: workflowHint(workflow)
|
|
267
|
+
}))
|
|
268
|
+
.filter((workflow) => Number(workflow.score || 0) >= 0.4 || Number(workflow.embeddingScore || 0) >= 0.62)
|
|
269
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
270
|
+
.slice(0, limit);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function workflowHint(workflow) {
|
|
274
|
+
const text = normalize(`${workflow.name} ${workflow.title} ${workflow.description}`);
|
|
275
|
+
if (text.includes("documentation")) return "use when documentation, changelog, or roadmap updates are needed";
|
|
276
|
+
if (text.includes("orchestration")) return "use when chaining or parallelizing agents";
|
|
277
|
+
if (text.includes("development rules")) return "use for coding conventions and pre-commit discipline";
|
|
278
|
+
return "use for feature implementation, testing, review, and debugging";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function workflowEmbeddingText(workflow) {
|
|
282
|
+
return `${workflow.title || workflow.name} ${workflow.description || ""} ${(workflow.chain || []).join(" ")}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalize(value) {
|
|
286
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeAgentName(value) {
|
|
290
|
+
return String(value || "").toLowerCase().replace(/-agent$/, "");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function escapeRegExp(value) {
|
|
294
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function safeRealpath(filePath) {
|
|
298
|
+
try {
|
|
299
|
+
return fs.realpathSync(filePath);
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isInsidePath(filePath, parentPath) {
|
|
306
|
+
const relative = path.relative(path.resolve(parentPath), path.resolve(filePath));
|
|
307
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
308
|
+
}
|
|
@@ -18,16 +18,25 @@ export function createContextOSMcpServer({ dataDir }) {
|
|
|
18
18
|
openFiles: z.array(z.string()).optional(),
|
|
19
19
|
maxFiles: z.number().int().positive().max(20).optional(),
|
|
20
20
|
maxSkills: z.number().int().positive().max(10).optional(),
|
|
21
|
+
maxWorkflows: z.number().int().positive().max(10).optional(),
|
|
21
22
|
skills: z.array(z.object({
|
|
22
23
|
name: z.string(),
|
|
23
24
|
description: z.string(),
|
|
24
25
|
path: z.string().optional()
|
|
26
|
+
})).optional(),
|
|
27
|
+
workflows: z.array(z.object({
|
|
28
|
+
name: z.string(),
|
|
29
|
+
title: z.string().optional(),
|
|
30
|
+
description: z.string(),
|
|
31
|
+
chain: z.array(z.string()).optional(),
|
|
32
|
+
path: z.string().optional()
|
|
25
33
|
})).optional()
|
|
26
34
|
},
|
|
27
35
|
outputSchema: {
|
|
28
36
|
scoredRules: z.array(z.any()),
|
|
29
37
|
suggestedFiles: z.array(z.any()),
|
|
30
38
|
suggestedSkills: z.array(z.any()),
|
|
39
|
+
suggestedWorkflows: z.array(z.any()),
|
|
31
40
|
telemetry: z.record(z.string(), z.any())
|
|
32
41
|
}
|
|
33
42
|
}, async (args) => {
|
|
@@ -38,7 +47,9 @@ export function createContextOSMcpServer({ dataDir }) {
|
|
|
38
47
|
dataDir,
|
|
39
48
|
maxFiles: args.maxFiles || 5,
|
|
40
49
|
maxSkills: args.maxSkills || 3,
|
|
41
|
-
|
|
50
|
+
maxWorkflows: args.maxWorkflows || 2,
|
|
51
|
+
skills: args.skills,
|
|
52
|
+
workflows: args.workflows
|
|
42
53
|
});
|
|
43
54
|
return {
|
|
44
55
|
content: [
|
|
@@ -51,6 +62,7 @@ export function createContextOSMcpServer({ dataDir }) {
|
|
|
51
62
|
scoredRules: result.scoredRules,
|
|
52
63
|
suggestedFiles: result.suggestedFiles,
|
|
53
64
|
suggestedSkills: result.suggestedSkills,
|
|
65
|
+
suggestedWorkflows: result.suggestedWorkflows,
|
|
54
66
|
telemetry: result.telemetry
|
|
55
67
|
}
|
|
56
68
|
};
|
|
@@ -70,7 +70,9 @@ async function handleBridgeRequest(socket, raw) {
|
|
|
70
70
|
dataDir,
|
|
71
71
|
maxFiles: payload.maxFiles || 5,
|
|
72
72
|
maxSkills: payload.maxSkills || 3,
|
|
73
|
-
|
|
73
|
+
maxWorkflows: payload.maxWorkflows || 2,
|
|
74
|
+
skills: payload.skills,
|
|
75
|
+
workflows: payload.workflows
|
|
74
76
|
});
|
|
75
77
|
socket.end(JSON.stringify(result));
|
|
76
78
|
} catch (error) {
|
|
@@ -79,6 +81,7 @@ async function handleBridgeRequest(socket, raw) {
|
|
|
79
81
|
scoredRules: [],
|
|
80
82
|
suggestedFiles: [],
|
|
81
83
|
suggestedSkills: [],
|
|
84
|
+
suggestedWorkflows: [],
|
|
82
85
|
telemetry: { elapsedMs: 0, modelStatus: "error" }
|
|
83
86
|
}));
|
|
84
87
|
}
|