opencode-agent-skill 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +163 -0
- package/LICENSE +9 -0
- package/README.md +581 -0
- package/bin/ocskill.mjs +975 -0
- package/docs/DETERMINISTIC-TOOLS.md +88 -0
- package/docs/ENGINEERING-DESIGN.md +176 -0
- package/docs/EVALS.md +136 -0
- package/docs/NPM-PUBLISH.md +102 -0
- package/docs/OPENCODE-COMPAT.md +109 -0
- package/docs/RESEARCH-SOURCES.md +37 -0
- package/docs/TRACE-SCHEMA.md +109 -0
- package/docs/V7-INTELLIGENCE-RUNTIME.md +166 -0
- package/evals/live/fixtures/engineering-bench/package.json +1 -0
- package/evals/live/fixtures/engineering-bench/src/api-errors.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/authz.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/cache-tags.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/config.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/contract-consumer.mjs +6 -0
- package/evals/live/fixtures/engineering-bench/src/contract-producer.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/dedupe.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/dependency.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/discount.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/inventory.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/migration.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/money.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/pagination.mjs +5 -0
- package/evals/live/fixtures/engineering-bench/src/path-safe.mjs +5 -0
- package/evals/live/fixtures/engineering-bench/src/payment.mjs +5 -0
- package/evals/live/fixtures/engineering-bench/src/query-sort.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/react-state.mjs +8 -0
- package/evals/live/fixtures/engineering-bench/src/retry.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/rn-platform.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/upload.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/webhook.mjs +3 -0
- package/evals/live/graders/engineering-bench.mjs +239 -0
- package/evals/live/tasks.json +126 -0
- package/evals/long/fixtures/long-horizon/package.json +1 -0
- package/evals/long/fixtures/long-horizon/src/auth.mjs +5 -0
- package/evals/long/fixtures/long-horizon/src/checkout.mjs +9 -0
- package/evals/long/fixtures/long-horizon/src/inventory.mjs +5 -0
- package/evals/long/fixtures/long-horizon/src/money.mjs +3 -0
- package/evals/long/fixtures/long-horizon/src/payment.mjs +7 -0
- package/evals/long/fixtures/long-horizon/src/product-api.mjs +10 -0
- package/evals/long/fixtures/long-horizon/src/product-cache.mjs +3 -0
- package/evals/long/fixtures/long-horizon/src/product-service.mjs +6 -0
- package/evals/long/fixtures/long-horizon/src/product-state.mjs +8 -0
- package/evals/long/fixtures/long-horizon/src/project-api.mjs +9 -0
- package/evals/long/fixtures/long-horizon/src/project-service.mjs +7 -0
- package/evals/long/fixtures/long-horizon/src/user-consumer.mjs +3 -0
- package/evals/long/fixtures/long-horizon/src/user-migration.mjs +7 -0
- package/evals/long/fixtures/long-horizon/src/user-serializer.mjs +3 -0
- package/evals/long/fixtures/long-horizon/src/user-validation.mjs +3 -0
- package/evals/long/graders/long-horizon.mjs +209 -0
- package/evals/long/tasks.json +36 -0
- package/evals/router-triggers.json +1238 -0
- package/evals/routing.json +321 -0
- package/global-config/AGENTS.md +212 -0
- package/global-config/agents/architect.md +38 -0
- package/global-config/agents/codebase-mapper.md +41 -0
- package/global-config/agents/critic.md +37 -0
- package/global-config/agents/debugger.md +36 -0
- package/global-config/agents/executor.md +48 -0
- package/global-config/agents/integration-verifier.md +43 -0
- package/global-config/agents/plan-checker.md +43 -0
- package/global-config/agents/researcher.md +30 -0
- package/global-config/agents/reviewer.md +30 -0
- package/global-config/agents/verifier.md +33 -0
- package/global-config/commands/audit.md +8 -0
- package/global-config/commands/critique.md +8 -0
- package/global-config/commands/debug.md +8 -0
- package/global-config/commands/feature.md +8 -0
- package/global-config/commands/fix.md +8 -0
- package/global-config/commands/plan.md +8 -0
- package/global-config/commands/research.md +8 -0
- package/global-config/commands/resume.md +22 -0
- package/global-config/commands/review.md +8 -0
- package/global-config/commands/run.md +29 -0
- package/global-config/commands/verify.md +8 -0
- package/global-config/plugins/ues-router/capabilities.js +20 -0
- package/global-config/plugins/ues-router/index.js +277 -0
- package/global-config/plugins/ues-router/router.js +74 -0
- package/global-config/plugins/ues-router/safety.js +16 -0
- package/global-config/skills/accessibility/SKILL.md +10 -0
- package/global-config/skills/accessibility/references/workflow.md +17 -0
- package/global-config/skills/api-contract/SKILL.md +12 -0
- package/global-config/skills/api-contract/references/workflow.md +17 -0
- package/global-config/skills/auth-security/SKILL.md +12 -0
- package/global-config/skills/auth-security/references/workflow.md +15 -0
- package/global-config/skills/bug-diagnosis/SKILL.md +28 -0
- package/global-config/skills/change-impact-analysis/SKILL.md +23 -0
- package/global-config/skills/code-review/SKILL.md +19 -0
- package/global-config/skills/context-engineering/SKILL.md +18 -0
- package/global-config/skills/context-engineering/references/large-repo.md +16 -0
- package/global-config/skills/database-engineering/SKILL.md +12 -0
- package/global-config/skills/database-engineering/references/workflow.md +15 -0
- package/global-config/skills/dependency-management/SKILL.md +12 -0
- package/global-config/skills/dependency-management/references/workflow.md +14 -0
- package/global-config/skills/devops-engineering/SKILL.md +10 -0
- package/global-config/skills/devops-engineering/references/workflow.md +11 -0
- package/global-config/skills/django-engineering/SKILL.md +10 -0
- package/global-config/skills/django-engineering/references/workflow.md +11 -0
- package/global-config/skills/documentation-engineering/SKILL.md +10 -0
- package/global-config/skills/documentation-engineering/references/workflow.md +15 -0
- package/global-config/skills/dotnet-engineering/SKILL.md +10 -0
- package/global-config/skills/dotnet-engineering/references/workflow.md +11 -0
- package/global-config/skills/ecommerce-engineering/SKILL.md +10 -0
- package/global-config/skills/ecommerce-engineering/references/workflow.md +17 -0
- package/global-config/skills/engineering-orchestrator/SKILL.md +29 -0
- package/global-config/skills/engineering-orchestrator/references/delegation.md +22 -0
- package/global-config/skills/engineering-orchestrator/references/evaluator-loop.md +18 -0
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +57 -0
- package/global-config/skills/engineering-orchestrator/references/model-escalation.md +19 -0
- package/global-config/skills/engineering-orchestrator/references/retry-policy.md +12 -0
- package/global-config/skills/engineering-orchestrator/references/routing.md +39 -0
- package/global-config/skills/engineering-orchestrator/references/verification-matrix.md +18 -0
- package/global-config/skills/fastapi-engineering/SKILL.md +10 -0
- package/global-config/skills/fastapi-engineering/references/workflow.md +13 -0
- package/global-config/skills/file-upload-engineering/SKILL.md +10 -0
- package/global-config/skills/file-upload-engineering/references/workflow.md +17 -0
- package/global-config/skills/flutter-engineering/SKILL.md +10 -0
- package/global-config/skills/flutter-engineering/references/workflow.md +13 -0
- package/global-config/skills/git-safety/SKILL.md +10 -0
- package/global-config/skills/git-safety/references/workflow.md +15 -0
- package/global-config/skills/implementation-engineer/SKILL.md +10 -0
- package/global-config/skills/implementation-engineer/references/workflow.md +14 -0
- package/global-config/skills/java-spring-engineering/SKILL.md +10 -0
- package/global-config/skills/java-spring-engineering/references/workflow.md +13 -0
- package/global-config/skills/long-task-state/SKILL.md +28 -0
- package/global-config/skills/long-task-state/references/context-ledger.md +27 -0
- package/global-config/skills/long-task-state/templates/STATE.md +48 -0
- package/global-config/skills/nestjs-engineering/SKILL.md +10 -0
- package/global-config/skills/nestjs-engineering/references/workflow.md +11 -0
- package/global-config/skills/nextjs-engineering/SKILL.md +12 -0
- package/global-config/skills/nextjs-engineering/references/workflow.md +15 -0
- package/global-config/skills/nodejs-engineering/SKILL.md +12 -0
- package/global-config/skills/nodejs-engineering/references/workflow.md +11 -0
- package/global-config/skills/payment-engineering/SKILL.md +12 -0
- package/global-config/skills/payment-engineering/references/workflow.md +19 -0
- package/global-config/skills/performance-engineering/SKILL.md +10 -0
- package/global-config/skills/performance-engineering/references/workflow.md +17 -0
- package/global-config/skills/python-engineering/SKILL.md +10 -0
- package/global-config/skills/python-engineering/references/workflow.md +11 -0
- package/global-config/skills/react-engineering/SKILL.md +14 -0
- package/global-config/skills/react-engineering/references/workflow.md +16 -0
- package/global-config/skills/react-native-engineering/SKILL.md +14 -0
- package/global-config/skills/react-native-engineering/references/workflow.md +16 -0
- package/global-config/skills/repo-explorer/SKILL.md +17 -0
- package/global-config/skills/research-verification/SKILL.md +18 -0
- package/global-config/skills/research-verification/references/source-hierarchy.md +12 -0
- package/global-config/skills/rest-api-design/SKILL.md +10 -0
- package/global-config/skills/rest-api-design/references/workflow.md +18 -0
- package/global-config/skills/software-architect/SKILL.md +10 -0
- package/global-config/skills/software-architect/references/workflow.md +18 -0
- package/global-config/skills/task-planner/SKILL.md +21 -0
- package/global-config/skills/task-planner/references/plan-schema.md +56 -0
- package/global-config/skills/test-driven-development/SKILL.md +22 -0
- package/global-config/skills/test-driven-development/references/writing-good-tests.md +21 -0
- package/global-config/skills/test-verification/SKILL.md +20 -0
- package/global-config/skills/ui-ux-engineering/SKILL.md +10 -0
- package/global-config/skills/ui-ux-engineering/references/workflow.md +17 -0
- package/global-config/skills/web-security-review/SKILL.md +10 -0
- package/global-config/skills/web-security-review/references/workflow.md +22 -0
- package/lib/context-manifest.mjs +101 -0
- package/lib/control-center.mjs +148 -0
- package/lib/eval-auth.mjs +21 -0
- package/lib/eval-report.mjs +72 -0
- package/lib/eval-telemetry.mjs +155 -0
- package/lib/evidence-receipt.mjs +50 -0
- package/lib/hermes-bridge.mjs +28 -0
- package/lib/ids.mjs +21 -0
- package/lib/installer.mjs +636 -0
- package/lib/learning-engine.mjs +161 -0
- package/lib/model-config.mjs +88 -0
- package/lib/model-policy.mjs +71 -0
- package/lib/opencode-compat.mjs +86 -0
- package/lib/orchestrator-policy.mjs +35 -0
- package/lib/process-runner.mjs +117 -0
- package/lib/repo-graph.mjs +135 -0
- package/lib/repo-inspect.mjs +264 -0
- package/lib/review-scope.mjs +98 -0
- package/lib/router-config.mjs +40 -0
- package/lib/task-engine.mjs +777 -0
- package/lib/task-graph.mjs +262 -0
- package/lib/update-resolver.mjs +43 -0
- package/lib/verification-plan.mjs +52 -0
- package/lib/version.mjs +47 -0
- package/lib/workspace-snapshot.mjs +45 -0
- package/lib/worktree-sandbox.mjs +59 -0
- package/package.json +69 -0
- package/scripts/check-working-tree.mjs +2 -0
- package/scripts/collect-evidence.mjs +2 -0
- package/scripts/control-center.mjs +37 -0
- package/scripts/detect-stack.mjs +2 -0
- package/scripts/detect-test-commands.mjs +2 -0
- package/scripts/eval-live.mjs +437 -0
- package/scripts/eval-report.mjs +49 -0
- package/scripts/eval-router.mjs +45 -0
- package/scripts/eval-skills.mjs +71 -0
- package/scripts/impact-map.mjs +4 -0
- package/scripts/install.mjs +61 -0
- package/scripts/repo-map.mjs +2 -0
- package/scripts/smoke-packed-install.mjs +326 -0
- package/scripts/syntax-check.mjs +35 -0
- package/scripts/uninstall.mjs +21 -0
- package/scripts/validate-live-suite.mjs +65 -0
- package/scripts/validate.mjs +133 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { readFile, stat } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { buildRepoGraph } from "./repo-graph.mjs"
|
|
5
|
+
import { taskFiles } from "./task-graph.mjs"
|
|
6
|
+
|
|
7
|
+
const INSTRUCTION_FILES = ["AGENTS.md", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle", "build.gradle.kts"]
|
|
8
|
+
|
|
9
|
+
function rel(root, file) {
|
|
10
|
+
return path.relative(root, file).replaceAll("\\", "/")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function unique(values) {
|
|
14
|
+
return [...new Set(values.filter(Boolean))]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function fileInfo(root, relative) {
|
|
18
|
+
const full = path.resolve(root, relative)
|
|
19
|
+
if (!full.startsWith(path.resolve(root) + path.sep) && full !== path.resolve(root)) return null
|
|
20
|
+
const info = await stat(full).catch(() => null)
|
|
21
|
+
if (!info?.isFile()) return null
|
|
22
|
+
return { path: rel(root, full), bytes: info.size }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function excerpt(root, relative, limit) {
|
|
26
|
+
const full = path.resolve(root, relative)
|
|
27
|
+
const source = await readFile(full, "utf8").catch(() => "")
|
|
28
|
+
if (!source) return null
|
|
29
|
+
return {
|
|
30
|
+
path: rel(root, full),
|
|
31
|
+
text: source.length <= limit ? source : source.slice(0, limit) + "\n...[truncated]",
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function buildContextManifest(root, task, options = {}) {
|
|
36
|
+
root = path.resolve(root)
|
|
37
|
+
const budget = Math.max(4_000, Number(options.budget ?? 24_000))
|
|
38
|
+
const declared = taskFiles(task)
|
|
39
|
+
const graph = await buildRepoGraph(root, { maxFiles: options.maxFiles ?? 2500 })
|
|
40
|
+
const nodeMap = new Map(graph.nodes.map((node) => [node.path, node]))
|
|
41
|
+
const incoming = new Map()
|
|
42
|
+
for (const edge of graph.edges) {
|
|
43
|
+
const list = incoming.get(edge.to) || []
|
|
44
|
+
list.push(edge.from)
|
|
45
|
+
incoming.set(edge.to, list)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const related = []
|
|
49
|
+
for (const file of declared) {
|
|
50
|
+
related.push(...(nodeMap.get(file)?.localImports || []))
|
|
51
|
+
related.push(...(incoming.get(file) || []))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const baseNames = declared.map((file) => path.basename(file, path.extname(file)).toLowerCase())
|
|
55
|
+
const tests = graph.nodes
|
|
56
|
+
.map((node) => node.path)
|
|
57
|
+
.filter((file) => /(^|\/)(test|tests|__tests__|spec)(\/|$)|\.(test|spec)\./i.test(file))
|
|
58
|
+
.filter((file) => baseNames.some((name) => name && file.toLowerCase().includes(name)))
|
|
59
|
+
.slice(0, 20)
|
|
60
|
+
|
|
61
|
+
const instructions = []
|
|
62
|
+
for (const name of INSTRUCTION_FILES) {
|
|
63
|
+
if (existsSync(path.join(root, name))) instructions.push(name)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const candidates = unique([...declared, ...related.slice(0, 30), ...tests, ...instructions])
|
|
67
|
+
const files = []
|
|
68
|
+
for (const file of candidates) {
|
|
69
|
+
const info = await fileInfo(root, file)
|
|
70
|
+
if (info) files.push(info)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let remaining = budget
|
|
74
|
+
const excerpts = []
|
|
75
|
+
for (const file of unique([...declared, ...tests, ...related.slice(0, 12), ...instructions])) {
|
|
76
|
+
if (remaining <= 0) break
|
|
77
|
+
const perFile = Math.min(4_000, remaining)
|
|
78
|
+
const item = await excerpt(root, file, perFile)
|
|
79
|
+
if (!item) continue
|
|
80
|
+
remaining -= item.text.length
|
|
81
|
+
excerpts.push(item)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
task: task?.id || null,
|
|
87
|
+
declared,
|
|
88
|
+
related: unique(related).filter((file) => !declared.includes(file)).slice(0, 30),
|
|
89
|
+
tests,
|
|
90
|
+
instructions,
|
|
91
|
+
files,
|
|
92
|
+
excerpts,
|
|
93
|
+
graph: {
|
|
94
|
+
scannedFiles: graph.scannedFiles,
|
|
95
|
+
truncated: graph.truncated,
|
|
96
|
+
hotspots: graph.hotspots.slice(0, 12),
|
|
97
|
+
},
|
|
98
|
+
budget,
|
|
99
|
+
used: budget - remaining,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { readLearningState } from "./learning-engine.mjs"
|
|
5
|
+
|
|
6
|
+
function escapeHtml(value) {
|
|
7
|
+
return String(value ?? "")
|
|
8
|
+
.replaceAll("&", "&")
|
|
9
|
+
.replaceAll("<", "<")
|
|
10
|
+
.replaceAll(">", ">")
|
|
11
|
+
.replaceAll('"', """)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function readJson(file, fallback = null) {
|
|
15
|
+
try { return JSON.parse(await readFile(file, "utf8")) } catch { return fallback }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function collectWork(root) {
|
|
19
|
+
const workRoot = path.join(root, ".ues-work")
|
|
20
|
+
const entries = await readdir(workRoot, { withFileTypes: true }).catch(() => [])
|
|
21
|
+
const items = []
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!entry.isDirectory()) continue
|
|
24
|
+
const dir = path.join(workRoot, entry.name)
|
|
25
|
+
const [state, plan, evidence] = await Promise.all([
|
|
26
|
+
readJson(path.join(dir, "STATE.json"), {}),
|
|
27
|
+
readJson(path.join(dir, "PLAN.json"), {}),
|
|
28
|
+
readJson(path.join(dir, "EVIDENCE.json"), {}),
|
|
29
|
+
])
|
|
30
|
+
items.push({
|
|
31
|
+
slug: entry.name,
|
|
32
|
+
status: state.status || "unknown",
|
|
33
|
+
goal: state.goal || plan.goal || "",
|
|
34
|
+
nextAction: state.nextAction || "",
|
|
35
|
+
blockers: state.blockers || [],
|
|
36
|
+
tasks: (plan.tasks || []).map((task) => ({
|
|
37
|
+
id: task.id,
|
|
38
|
+
title: task.title,
|
|
39
|
+
risk: task.risk || "medium",
|
|
40
|
+
state: state.tasks?.[task.id] || {},
|
|
41
|
+
})),
|
|
42
|
+
receipts: (evidence.receipts || []).length,
|
|
43
|
+
updatedAt: state.updatedAt || null,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
return items.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function collectEvalSummary(root) {
|
|
50
|
+
const evalRoot = path.join(root, ".ues-evals")
|
|
51
|
+
const entries = await readdir(evalRoot, { withFileTypes: true }).catch(() => [])
|
|
52
|
+
const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort().slice(-20)
|
|
53
|
+
const rows = []
|
|
54
|
+
for (const name of files) {
|
|
55
|
+
const payload = await readJson(path.join(evalRoot, name), null)
|
|
56
|
+
if (!payload) continue
|
|
57
|
+
rows.push({ file: name, model: payload.model, suite: payload.suite, summary: payload.summary || {} })
|
|
58
|
+
}
|
|
59
|
+
return rows.reverse()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function collectControlCenterData(root = process.cwd()) {
|
|
63
|
+
root = path.resolve(root)
|
|
64
|
+
const [work, learning, evals] = await Promise.all([
|
|
65
|
+
collectWork(root),
|
|
66
|
+
readLearningState(root),
|
|
67
|
+
collectEvalSummary(root),
|
|
68
|
+
])
|
|
69
|
+
return {
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
generatedAt: new Date().toISOString(),
|
|
72
|
+
root,
|
|
73
|
+
work,
|
|
74
|
+
learning,
|
|
75
|
+
evals,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function renderControlCenter(data) {
|
|
80
|
+
const payload = JSON.stringify(data).replaceAll("<", "\\u003c")
|
|
81
|
+
return `<!doctype html>
|
|
82
|
+
<html lang="en">
|
|
83
|
+
<head>
|
|
84
|
+
<meta charset="utf-8">
|
|
85
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
86
|
+
<title>UES Control Center</title>
|
|
87
|
+
<style>
|
|
88
|
+
:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color-scheme:dark;background:#0b1020;color:#e7ecf5}
|
|
89
|
+
body{margin:0}.shell{max-width:1200px;margin:auto;padding:28px}.top{display:flex;justify-content:space-between;gap:20px;align-items:end}
|
|
90
|
+
h1{margin:0;font-size:30px}.muted{color:#9da9bd}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px;margin-top:20px}
|
|
91
|
+
.card{background:#121a2c;border:1px solid #26324a;border-radius:14px;padding:16px}.pill{display:inline-block;padding:3px 8px;border:1px solid #3b4966;border-radius:999px;font-size:12px}
|
|
92
|
+
.task{display:grid;grid-template-columns:1fr auto;gap:8px;border-top:1px solid #243049;padding:9px 0}.ok{color:#7ee787}.warn{color:#f2cc60}.bad{color:#ff7b72}
|
|
93
|
+
table{width:100%;border-collapse:collapse;margin-top:10px}th,td{text-align:left;padding:8px;border-bottom:1px solid #26324a;font-size:13px}
|
|
94
|
+
pre{white-space:pre-wrap;word-break:break-word;background:#0a0f1c;padding:10px;border-radius:10px}.section{margin-top:28px}
|
|
95
|
+
</style>
|
|
96
|
+
</head>
|
|
97
|
+
<body><div class="shell">
|
|
98
|
+
<div class="top"><div><h1>UES Control Center</h1><div class="muted" id="root"></div></div><div class="muted" id="generated"></div></div>
|
|
99
|
+
<div class="section"><h2>Long-horizon work</h2><div class="grid" id="work"></div></div>
|
|
100
|
+
<div class="section"><h2>Learning loop</h2><div class="grid" id="learning"></div></div>
|
|
101
|
+
<div class="section"><h2>Recent evaluations</h2><div class="card"><table><thead><tr><th>Suite</th><th>Model</th><th>Baseline</th><th>UES</th></tr></thead><tbody id="evals"></tbody></table></div></div>
|
|
102
|
+
</div>
|
|
103
|
+
<script>
|
|
104
|
+
let data=${payload};
|
|
105
|
+
const esc=(v)=>String(v??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
|
106
|
+
function render(next){
|
|
107
|
+
data=next;
|
|
108
|
+
document.querySelector("#root").textContent=data.root;
|
|
109
|
+
document.querySelector("#generated").textContent="Generated "+data.generatedAt;
|
|
110
|
+
const work=document.querySelector("#work");
|
|
111
|
+
work.innerHTML="";
|
|
112
|
+
for(const item of data.work){
|
|
113
|
+
const tasks=item.tasks.map(t=>'<div class="task"><span>'+esc(t.id)+' · '+esc(t.title||'')+'</span><span class="pill">'+esc(t.state.status||'pending')+'</span></div>').join('');
|
|
114
|
+
work.insertAdjacentHTML("beforeend",'<div class="card"><div><span class="pill">'+esc(item.status)+'</span></div><h3>'+esc(item.slug)+'</h3><div class="muted">'+esc(item.goal)+'</div><p>'+esc(item.nextAction)+'</p>'+tasks+'<div class="muted">receipts: '+item.receipts+' · blockers: '+item.blockers.length+'</div></div>');
|
|
115
|
+
}
|
|
116
|
+
if(!data.work.length) work.innerHTML='<div class="card muted">No .ues-work items found.</div>';
|
|
117
|
+
const learning=document.querySelector("#learning");
|
|
118
|
+
learning.innerHTML='<div class="card"><h3>Accepted lessons</h3><strong>'+data.learning.accepted.length+'</strong></div><div class="card"><h3>Proposals</h3><strong>'+data.learning.proposals.length+'</strong></div>';
|
|
119
|
+
const tbody=document.querySelector("#evals");
|
|
120
|
+
tbody.innerHTML="";
|
|
121
|
+
for(const row of data.evals){
|
|
122
|
+
const fmt=(x)=>x?((x.passed||0)+'/'+(x.total||0)):'—';
|
|
123
|
+
tbody.insertAdjacentHTML("beforeend",'<tr><td>'+esc(row.suite)+'</td><td>'+esc(row.model)+'</td><td>'+fmt(row.summary.baseline)+'</td><td>'+fmt(row.summary.ues)+'</td></tr>');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
render(data);
|
|
127
|
+
if(location.protocol==="http:" || location.protocol==="https:"){
|
|
128
|
+
setInterval(async()=>{
|
|
129
|
+
try{
|
|
130
|
+
const response=await fetch("/data.json",{cache:"no-store"});
|
|
131
|
+
if(response.ok) render(await response.json());
|
|
132
|
+
}catch{}
|
|
133
|
+
},3000);
|
|
134
|
+
}
|
|
135
|
+
</script></body></html>`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function writeControlCenter(root = process.cwd(), outputDir = null) {
|
|
139
|
+
root = path.resolve(root)
|
|
140
|
+
const dir = path.resolve(outputDir || path.join(root, ".ues-dashboard"))
|
|
141
|
+
await mkdir(dir, { recursive: true })
|
|
142
|
+
const data = await collectControlCenterData(root)
|
|
143
|
+
const html = renderControlCenter(data)
|
|
144
|
+
const file = path.join(dir, "index.html")
|
|
145
|
+
await writeFile(file, html, "utf8")
|
|
146
|
+
await writeFile(path.join(dir, "data.json"), JSON.stringify(data, null, 2) + "\n", "utf8")
|
|
147
|
+
return { dir, file, data }
|
|
148
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { copyFile, mkdir } from "node:fs/promises"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
export function currentOpenCodeAuthPath(home = os.homedir()) {
|
|
7
|
+
return path.join(home, ".local", "share", "opencode", "auth.json")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function copyCurrentOpenCodeAuth(dataRoot, options = {}) {
|
|
11
|
+
const source = options.source || currentOpenCodeAuthPath(options.home)
|
|
12
|
+
const target = path.join(dataRoot, "opencode", "auth.json")
|
|
13
|
+
|
|
14
|
+
if (!existsSync(source)) {
|
|
15
|
+
return { copied: false, source, target, reason: "not-found" }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
19
|
+
await copyFile(source, target)
|
|
20
|
+
return { copied: true, source, target }
|
|
21
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function summarizeEvalResults(results) {
|
|
2
|
+
const modes = {}
|
|
3
|
+
for (const item of results) {
|
|
4
|
+
const mode = item.mode || "unknown"
|
|
5
|
+
const bucket = modes[mode] ??= {
|
|
6
|
+
passed: 0,
|
|
7
|
+
total: 0,
|
|
8
|
+
durationMs: 0,
|
|
9
|
+
toolCalls: 0,
|
|
10
|
+
toolSamples: 0,
|
|
11
|
+
tokens: 0,
|
|
12
|
+
tokenSamples: 0,
|
|
13
|
+
cost: 0,
|
|
14
|
+
costSamples: 0,
|
|
15
|
+
}
|
|
16
|
+
bucket.total += 1
|
|
17
|
+
if (item.passed) bucket.passed += 1
|
|
18
|
+
bucket.durationMs += Number(item.durationMs) || 0
|
|
19
|
+
|
|
20
|
+
if ((item.telemetry?.jsonLines || 0) > 0) {
|
|
21
|
+
bucket.toolCalls += Number(item.telemetry?.toolCalls) || 0
|
|
22
|
+
bucket.toolSamples += 1
|
|
23
|
+
}
|
|
24
|
+
if ((item.telemetry?.usageSamples || 0) > 0) {
|
|
25
|
+
bucket.tokens += Number(item.telemetry?.tokens?.total) || 0
|
|
26
|
+
bucket.tokenSamples += 1
|
|
27
|
+
}
|
|
28
|
+
if ((item.telemetry?.costSamples || 0) > 0) {
|
|
29
|
+
bucket.cost += Number(item.telemetry?.cost) || 0
|
|
30
|
+
bucket.costSamples += 1
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const bucket of Object.values(modes)) {
|
|
35
|
+
bucket.passRate = bucket.total ? bucket.passed / bucket.total : 0
|
|
36
|
+
bucket.avgDurationMs = bucket.total ? bucket.durationMs / bucket.total : 0
|
|
37
|
+
bucket.avgToolCalls = bucket.toolSamples ? bucket.toolCalls / bucket.toolSamples : null
|
|
38
|
+
bucket.avgTokens = bucket.tokenSamples ? bucket.tokens / bucket.tokenSamples : null
|
|
39
|
+
bucket.avgCost = bucket.costSamples ? bucket.cost / bucket.costSamples : null
|
|
40
|
+
bucket.telemetryCoverage = {
|
|
41
|
+
tools: bucket.total ? bucket.toolSamples / bucket.total : 0,
|
|
42
|
+
tokens: bucket.total ? bucket.tokenSamples / bucket.total : 0,
|
|
43
|
+
cost: bucket.total ? bucket.costSamples / bucket.total : 0,
|
|
44
|
+
}
|
|
45
|
+
delete bucket.durationMs
|
|
46
|
+
delete bucket.toolCalls
|
|
47
|
+
delete bucket.toolSamples
|
|
48
|
+
delete bucket.tokens
|
|
49
|
+
delete bucket.tokenSamples
|
|
50
|
+
delete bucket.cost
|
|
51
|
+
delete bucket.costSamples
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const byTaskMap = new Map()
|
|
55
|
+
for (const item of results) {
|
|
56
|
+
const task = byTaskMap.get(item.task) || {}
|
|
57
|
+
const bucket = task[item.mode] || { passed: 0, total: 0 }
|
|
58
|
+
bucket.total += 1
|
|
59
|
+
if (item.passed) bucket.passed += 1
|
|
60
|
+
bucket.passRate = bucket.passed / bucket.total
|
|
61
|
+
task[item.mode] = bucket
|
|
62
|
+
byTaskMap.set(item.task, task)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const baseline = modes.baseline?.passRate
|
|
66
|
+
const ues = modes.ues?.passRate
|
|
67
|
+
return {
|
|
68
|
+
modes,
|
|
69
|
+
passRateDelta: typeof baseline === "number" && typeof ues === "number" ? ues - baseline : null,
|
|
70
|
+
byTask: Object.fromEntries([...byTaskMap.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
function asNumber(value) {
|
|
2
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function normalizedToolName(value) {
|
|
6
|
+
return typeof value === "string" ? value.trim().toLowerCase() : ""
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function visit(value, callback, path = []) {
|
|
10
|
+
if (!value || typeof value !== "object") return
|
|
11
|
+
callback(value, path)
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
value.forEach((item, index) => visit(item, callback, [...path, index]))
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
for (const [key, child] of Object.entries(value)) {
|
|
17
|
+
if (child && typeof child === "object") visit(child, callback, [...path, key])
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function usageFromObject(object) {
|
|
22
|
+
const usage = object?.usage
|
|
23
|
+
if (!usage || typeof usage !== "object") return null
|
|
24
|
+
|
|
25
|
+
const input =
|
|
26
|
+
asNumber(usage.inputTokens) ||
|
|
27
|
+
asNumber(usage.input_tokens) ||
|
|
28
|
+
asNumber(usage.promptTokens) ||
|
|
29
|
+
asNumber(usage.prompt_tokens) ||
|
|
30
|
+
asNumber(usage.input)
|
|
31
|
+
|
|
32
|
+
const output =
|
|
33
|
+
asNumber(usage.outputTokens) ||
|
|
34
|
+
asNumber(usage.output_tokens) ||
|
|
35
|
+
asNumber(usage.completionTokens) ||
|
|
36
|
+
asNumber(usage.completion_tokens) ||
|
|
37
|
+
asNumber(usage.output)
|
|
38
|
+
|
|
39
|
+
const total =
|
|
40
|
+
asNumber(usage.totalTokens) ||
|
|
41
|
+
asNumber(usage.total_tokens) ||
|
|
42
|
+
asNumber(usage.total) ||
|
|
43
|
+
input + output
|
|
44
|
+
|
|
45
|
+
if (input === 0 && output === 0 && total === 0) return null
|
|
46
|
+
return { input, output, total }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toolCandidate(object) {
|
|
50
|
+
const type = String(object.type || object.kind || object.event || "").toLowerCase()
|
|
51
|
+
let name = normalizedToolName(object.tool || object.toolName || object.tool_name)
|
|
52
|
+
|
|
53
|
+
if (!name && type.includes("tool")) {
|
|
54
|
+
name = normalizedToolName(object.name)
|
|
55
|
+
}
|
|
56
|
+
if (!name) return null
|
|
57
|
+
|
|
58
|
+
const input =
|
|
59
|
+
object.input && typeof object.input === "object" ? object.input :
|
|
60
|
+
object.args && typeof object.args === "object" ? object.args :
|
|
61
|
+
object.arguments && typeof object.arguments === "object" ? object.arguments :
|
|
62
|
+
{}
|
|
63
|
+
|
|
64
|
+
const id =
|
|
65
|
+
object.callID || object.callId || object.toolCallID || object.toolCallId ||
|
|
66
|
+
object.id || null
|
|
67
|
+
|
|
68
|
+
return { name, input, id: id == null ? null : String(id) }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseOpenCodeTelemetry(stdout) {
|
|
72
|
+
const lines = String(stdout || "").split(/\r?\n/).filter((line) => line.trim())
|
|
73
|
+
const parsed = []
|
|
74
|
+
let parseErrors = 0
|
|
75
|
+
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
try {
|
|
78
|
+
parsed.push(JSON.parse(line))
|
|
79
|
+
} catch {
|
|
80
|
+
parseErrors += 1
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const toolKeys = new Set()
|
|
85
|
+
const tools = new Map()
|
|
86
|
+
const skills = new Set()
|
|
87
|
+
const subagents = new Set()
|
|
88
|
+
const tokens = { input: 0, output: 0, total: 0 }
|
|
89
|
+
let cost = 0
|
|
90
|
+
let usageSamples = 0
|
|
91
|
+
let costSamples = 0
|
|
92
|
+
|
|
93
|
+
parsed.forEach((event, eventIndex) => {
|
|
94
|
+
let eventUsage = null
|
|
95
|
+
let eventCost = null
|
|
96
|
+
|
|
97
|
+
visit(event, (object, objectPath) => {
|
|
98
|
+
const tool = toolCandidate(object)
|
|
99
|
+
if (tool) {
|
|
100
|
+
const fallbackKey = `${eventIndex}:${objectPath.join(".")}:${tool.name}`
|
|
101
|
+
const key = tool.id ? `${tool.name}:${tool.id}` : fallbackKey
|
|
102
|
+
if (!toolKeys.has(key)) {
|
|
103
|
+
toolKeys.add(key)
|
|
104
|
+
tools.set(tool.name, (tools.get(tool.name) || 0) + 1)
|
|
105
|
+
|
|
106
|
+
if (tool.name === "skill") {
|
|
107
|
+
const id = tool.input.id || tool.input.skill || tool.input.name
|
|
108
|
+
if (typeof id === "string" && id) skills.add(id)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (tool.name === "task" || tool.name === "subagent") {
|
|
112
|
+
const id = tool.input.agent || tool.input.agentID || tool.input.agentId || tool.input.id
|
|
113
|
+
if (typeof id === "string" && id) subagents.add(id)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!eventUsage) eventUsage = usageFromObject(object)
|
|
119
|
+
if (eventCost === null) {
|
|
120
|
+
for (const key of ["cost", "totalCost", "total_cost"]) {
|
|
121
|
+
if (typeof object[key] === "number" && Number.isFinite(object[key])) {
|
|
122
|
+
eventCost = object[key]
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if (eventUsage) {
|
|
130
|
+
usageSamples += 1
|
|
131
|
+
tokens.input += eventUsage.input
|
|
132
|
+
tokens.output += eventUsage.output
|
|
133
|
+
tokens.total += eventUsage.total
|
|
134
|
+
}
|
|
135
|
+
if (eventCost !== null) {
|
|
136
|
+
costSamples += 1
|
|
137
|
+
cost += eventCost
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
schemaVersion: 1,
|
|
143
|
+
format: "best-effort-opencode-jsonl",
|
|
144
|
+
jsonLines: parsed.length,
|
|
145
|
+
parseErrors,
|
|
146
|
+
toolCalls: [...tools.values()].reduce((sum, value) => sum + value, 0),
|
|
147
|
+
tools: Object.fromEntries([...tools.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
148
|
+
skillsLoaded: [...skills].sort(),
|
|
149
|
+
subagents: [...subagents].sort(),
|
|
150
|
+
tokens,
|
|
151
|
+
usageSamples,
|
|
152
|
+
cost,
|
|
153
|
+
costSamples,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
function digest(value) {
|
|
4
|
+
return createHash("sha256").update(String(value || "")).digest("hex")
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createVerificationReceipt(input = {}) {
|
|
8
|
+
const command = String(input.command || "").trim()
|
|
9
|
+
if (!command) throw new Error("verification receipt command is required")
|
|
10
|
+
|
|
11
|
+
const exitCode = Number(input.exitCode)
|
|
12
|
+
if (!Number.isInteger(exitCode)) throw new Error("verification receipt exitCode must be an integer")
|
|
13
|
+
|
|
14
|
+
const startedAt = String(input.startedAt || new Date().toISOString())
|
|
15
|
+
const finishedAt = String(input.finishedAt || new Date().toISOString())
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
id: input.id || randomUUID(),
|
|
20
|
+
task: input.task || null,
|
|
21
|
+
runId: input.runId || null,
|
|
22
|
+
command,
|
|
23
|
+
args: Array.isArray(input.args) ? input.args.map(String) : [],
|
|
24
|
+
cwd: input.cwd || null,
|
|
25
|
+
exitCode,
|
|
26
|
+
passed: exitCode === 0,
|
|
27
|
+
startedAt,
|
|
28
|
+
finishedAt,
|
|
29
|
+
durationMs: Math.max(0, Number(input.durationMs) || 0),
|
|
30
|
+
stdoutSha256: digest(input.stdout),
|
|
31
|
+
stderrSha256: digest(input.stderr),
|
|
32
|
+
workspaceBefore: input.workspaceBefore || null,
|
|
33
|
+
workspaceAfter: input.workspaceAfter || null,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateVerificationReceipt(receipt) {
|
|
38
|
+
const errors = []
|
|
39
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
40
|
+
return { valid: false, errors: ["receipt must be an object"] }
|
|
41
|
+
}
|
|
42
|
+
if (receipt.schemaVersion !== 1) errors.push("schemaVersion must be 1")
|
|
43
|
+
if (!String(receipt.id || "").trim()) errors.push("id is required")
|
|
44
|
+
if (!String(receipt.command || "").trim()) errors.push("command is required")
|
|
45
|
+
if (!Number.isInteger(receipt.exitCode)) errors.push("exitCode must be an integer")
|
|
46
|
+
if (typeof receipt.passed !== "boolean") errors.push("passed must be boolean")
|
|
47
|
+
if (!String(receipt.stdoutSha256 || "").match(/^[a-f0-9]{64}$/)) errors.push("stdoutSha256 is invalid")
|
|
48
|
+
if (!String(receipt.stderrSha256 || "").match(/^[a-f0-9]{64}$/)) errors.push("stderrSha256 is invalid")
|
|
49
|
+
return { valid: errors.length === 0, errors }
|
|
50
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
export function hermesStatus() {
|
|
4
|
+
const result = spawnSync("hermes", ["--version"], { encoding: "utf8", shell: process.platform === "win32" })
|
|
5
|
+
return {
|
|
6
|
+
available: result.status === 0,
|
|
7
|
+
version: result.status === 0 ? String(result.stdout || result.stderr || "").trim() : null,
|
|
8
|
+
error: result.status === 0 ? null : String(result.stderr || result.stdout || "Hermes CLI not found").trim(),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildHermesDelegationPrompt(contextPack) {
|
|
13
|
+
return [
|
|
14
|
+
"You are an optional external executor for a UES task.",
|
|
15
|
+
"Implement exactly the approved task described below.",
|
|
16
|
+
"Do not broaden scope, merge, push, publish, deploy, or alter durable UES state.",
|
|
17
|
+
"Run the declared verification and return a concise structured report.",
|
|
18
|
+
"",
|
|
19
|
+
JSON.stringify(contextPack, null, 2),
|
|
20
|
+
].join("\n")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export function hermesOneShotArgs(prompt) {
|
|
25
|
+
const text = String(prompt || "").trim()
|
|
26
|
+
if (!text) throw new Error("Hermes one-shot prompt is required")
|
|
27
|
+
return ["chat", "-q", text]
|
|
28
|
+
}
|
package/lib/ids.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const SKILL_PREFIX = "ues-"
|
|
2
|
+
|
|
3
|
+
const RESOURCE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
4
|
+
const MANAGED_SKILL_PATTERN = /^ues-[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
5
|
+
const MANAGED_MARKDOWN_PATTERN = /^ues-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/
|
|
6
|
+
|
|
7
|
+
function testPattern(pattern, value) {
|
|
8
|
+
return typeof value === "string" && pattern.test(value)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validResourceID(value) {
|
|
12
|
+
return testPattern(RESOURCE_ID_PATTERN, value)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validSkillID(value) {
|
|
16
|
+
return testPattern(MANAGED_SKILL_PATTERN, value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validManagedMarkdown(value) {
|
|
20
|
+
return testPattern(MANAGED_MARKDOWN_PATTERN, value)
|
|
21
|
+
}
|