oris-skills 2.2.2 → 2.2.3
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/package.json +1 -1
- package/references/conventions.md +3 -0
- package/references/devops.md +58 -0
- package/references/devops.schema.json +54 -0
- package/scripts/flow/oris-flow-devops.mjs +77 -0
- package/scripts/install/install-user-skills.mjs +14 -0
- package/scripts/install/oris-mcp.mjs +68 -0
- package/scripts/tests/run-all-tests.mjs +2 -0
- package/scripts/tests/test-install-mcp.mjs +66 -0
- package/scripts/tests/test-oris-flow-devops.mjs +64 -0
- package/skills/oris-flow/SKILL.md +7 -1
- package/skills/oris-flow/references/commit.md +45 -0
- package/skills/oris-flow/references/pr.md +44 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oris-skills",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "Oris Skills: one entry point (/oris-flow), guided discovery-to-implementation flow, and self-tuning verified loops for Cursor, Claude Code, and Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Baldassarre",
|
|
@@ -55,6 +55,9 @@ The `oris-skills` CLI is NOT installed globally. ALWAYS invoke it as `npx oris-s
|
|
|
55
55
|
- GENERATED documents: `orisFlow.artifactLanguage` from `.oris-flow/settings.json`
|
|
56
56
|
(default `en`).
|
|
57
57
|
- Skill instructions and code stay English.
|
|
58
|
+
- COMMIT messages and PULL REQUEST titles/descriptions: ALWAYS English, whatever the chat
|
|
59
|
+
language or `artifactLanguage`. Work-item references follow the DevOps provider's
|
|
60
|
+
`workItemSyntax` (`references/devops.md`).
|
|
58
61
|
|
|
59
62
|
## Response style
|
|
60
63
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Oris DevOps
|
|
2
|
+
|
|
3
|
+
How Oris connects to a DevOps platform for commits, pull requests, and work items.
|
|
4
|
+
Swappable by design: today Azure DevOps, tomorrow Jira or GitHub — same flows, a different
|
|
5
|
+
provider block. Non-secret only; Oris stores NO credentials
|
|
6
|
+
(`references/conventions.md` `## Gates`).
|
|
7
|
+
|
|
8
|
+
## Config surface
|
|
9
|
+
|
|
10
|
+
Resolved in order, each layer overriding the previous:
|
|
11
|
+
|
|
12
|
+
1. Built-in Oris default (`azure-devops`, org `orislineweb`, MCP `oris-devops`).
|
|
13
|
+
2. Organization: `~/.oris/devops.json` — shared defaults for everyone in the org.
|
|
14
|
+
3. Repository: `.oris-flow/devops.json` — this repo's override.
|
|
15
|
+
|
|
16
|
+
Shape (`references/devops.schema.json`):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"version": 1,
|
|
21
|
+
"devops": {
|
|
22
|
+
"provider": "azure-devops",
|
|
23
|
+
"organization": "orislineweb",
|
|
24
|
+
"workItemSyntax": "#{id}",
|
|
25
|
+
"commitLanguage": "en",
|
|
26
|
+
"prLanguage": "en",
|
|
27
|
+
"mcp": {
|
|
28
|
+
"name": "oris-devops",
|
|
29
|
+
"package": "@azure-devops/mcp@2.7.0",
|
|
30
|
+
"domains": ["core", "work", "work-items", "repositories"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `provider` — the DevOps platform. Swap this (and the `mcp` block) to move off Azure.
|
|
37
|
+
- `organization` — org/account slug; substituted into the MCP command.
|
|
38
|
+
- `workItemSyntax` — how a work item id is written in commits/PRs. `#{id}` → `#1234`.
|
|
39
|
+
- `commitLanguage` / `prLanguage` — always `en`; commits and PRs are English by contract.
|
|
40
|
+
- `mcp` — the MCP server Oris seeds (`oris-devops`) so agents can read work items and
|
|
41
|
+
repositories. Auth is the platform's own (PAT / sign-in), never Oris's.
|
|
42
|
+
|
|
43
|
+
## Rules
|
|
44
|
+
|
|
45
|
+
1. READ the resolved config before composing a commit or PR.
|
|
46
|
+
2. NEVER store credentials in `.oris-flow/**`, `~/.oris/**`, or any Oris file.
|
|
47
|
+
3. Missing provider auth → degrade: compose the message, tell the user, do not block.
|
|
48
|
+
4. CONFIRM before writing any devops config file.
|
|
49
|
+
|
|
50
|
+
## Never
|
|
51
|
+
|
|
52
|
+
- Put secrets or tokens in a devops config file.
|
|
53
|
+
- Hardcode a platform in the flows — read `provider` and act generically.
|
|
54
|
+
|
|
55
|
+
## Done when
|
|
56
|
+
|
|
57
|
+
- [ ] Effective config resolved from default → org → repo before acting.
|
|
58
|
+
- [ ] No credentials written anywhere under Oris.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Oris DevOps Provider",
|
|
4
|
+
"$id": "https://oris.dev/schemas/oris-flow-devops-v1.json",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["version", "devops"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"const": 1
|
|
12
|
+
},
|
|
13
|
+
"devops": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"properties": {
|
|
17
|
+
"provider": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "DevOps platform id (e.g. azure-devops, github, jira). Swap this and the mcp block to change platform."
|
|
20
|
+
},
|
|
21
|
+
"organization": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Org/account slug substituted into the MCP command. Non-secret."
|
|
24
|
+
},
|
|
25
|
+
"workItemSyntax": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "How a work item id is written in commits/PRs. Template with {id}, e.g. #{id} -> #1234."
|
|
28
|
+
},
|
|
29
|
+
"commitLanguage": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 2,
|
|
32
|
+
"description": "Language for commit messages. Always en by contract."
|
|
33
|
+
},
|
|
34
|
+
"prLanguage": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"minLength": 2,
|
|
37
|
+
"description": "Language for pull request titles/descriptions. Always en by contract."
|
|
38
|
+
},
|
|
39
|
+
"mcp": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"description": "MCP server Oris seeds so agents can read work items and repositories. No credentials.",
|
|
43
|
+
"properties": {
|
|
44
|
+
"name": { "type": "string" },
|
|
45
|
+
"package": { "type": "string" },
|
|
46
|
+
"domains": { "type": "array", "items": { "type": "string" } },
|
|
47
|
+
"command": { "type": "string" },
|
|
48
|
+
"args": { "type": "array", "items": { "type": "string" } }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Built-in Oris default DevOps provider. Non-secret only — org and repo layers override it.
|
|
9
|
+
* The provider is swappable: move off Azure DevOps by overriding `provider` and `mcp`.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_DEVOPS = {
|
|
12
|
+
version: 1,
|
|
13
|
+
devops: {
|
|
14
|
+
provider: "azure-devops",
|
|
15
|
+
organization: "orislineweb",
|
|
16
|
+
workItemSyntax: "#{id}",
|
|
17
|
+
commitLanguage: "en",
|
|
18
|
+
prLanguage: "en",
|
|
19
|
+
mcp: {
|
|
20
|
+
name: "oris-devops",
|
|
21
|
+
package: "@azure-devops/mcp@2.7.0",
|
|
22
|
+
domains: ["core", "work", "work-items", "repositories"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function isPlainObject(value) {
|
|
28
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Deep-merge source over target; arrays and scalars replace, plain objects merge. */
|
|
32
|
+
function deepMerge(target, source) {
|
|
33
|
+
if (!isPlainObject(source)) return source;
|
|
34
|
+
const out = isPlainObject(target) ? { ...target } : {};
|
|
35
|
+
for (const [key, value] of Object.entries(source)) {
|
|
36
|
+
out[key] = isPlainObject(value) ? deepMerge(out[key], value) : value;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readJson(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the effective DevOps config: built-in default → org (`~/.oris/devops.json`) →
|
|
51
|
+
* repository (`.oris-flow/devops.json`). Missing or invalid layers are skipped, never fatal.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveDevops(repositoryRoot, options = {}) {
|
|
54
|
+
const home = options.home ?? os.homedir();
|
|
55
|
+
const orgFile = readJson(path.join(home, ".oris", "devops.json"));
|
|
56
|
+
const repoFile = repositoryRoot ? readJson(path.join(repositoryRoot, ".oris-flow", "devops.json")) : null;
|
|
57
|
+
let resolved = DEFAULT_DEVOPS;
|
|
58
|
+
for (const layer of [orgFile, repoFile]) {
|
|
59
|
+
if (isPlainObject(layer)) resolved = deepMerge(resolved, layer);
|
|
60
|
+
}
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the MCP server entry for an agent config from a resolved `devops` block. Azure
|
|
66
|
+
* DevOps command shape by default; any provider can supply explicit `mcp.args` instead.
|
|
67
|
+
*/
|
|
68
|
+
export function buildMcpServer(devops) {
|
|
69
|
+
const mcp = devops.mcp ?? {};
|
|
70
|
+
if (Array.isArray(mcp.args)) return { command: mcp.command ?? "npx", args: [...mcp.args] };
|
|
71
|
+
const domains = Array.isArray(mcp.domains) ? mcp.domains : [];
|
|
72
|
+
return { command: "npx", args: ["-y", mcp.package, devops.organization, "-d", ...domains] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function mcpServerName(devops) {
|
|
76
|
+
return devops.mcp?.name ?? "oris-devops";
|
|
77
|
+
}
|
|
@@ -22,6 +22,8 @@ import os from "node:os";
|
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
import process from "node:process";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { buildMcpServer, mcpServerName, resolveDevops } from "../flow/oris-flow-devops.mjs";
|
|
26
|
+
import { MCP_AGENTS, seedMcpServer } from "./oris-mcp.mjs";
|
|
25
27
|
|
|
26
28
|
const markerName = ".oris-skills-managed.json";
|
|
27
29
|
const installManifestName = ".oris-skills-install.json";
|
|
@@ -61,6 +63,7 @@ function parseArgs(argv) {
|
|
|
61
63
|
const options = {
|
|
62
64
|
dryRun: false,
|
|
63
65
|
force: false,
|
|
66
|
+
noMcp: false,
|
|
64
67
|
agents: [],
|
|
65
68
|
bundleRoot: paths.bundleRoot,
|
|
66
69
|
agentRoots: paths.agentRoots,
|
|
@@ -76,6 +79,7 @@ function parseArgs(argv) {
|
|
|
76
79
|
};
|
|
77
80
|
if (key === "--dry-run") options.dryRun = true;
|
|
78
81
|
else if (key === "--force") options.force = true;
|
|
82
|
+
else if (key === "--no-mcp") options.noMcp = true;
|
|
79
83
|
else if (key === "--agents" || key.startsWith("--agents=")) {
|
|
80
84
|
options.agents = value().split(",").map((name) => name.trim().toLowerCase()).filter(Boolean);
|
|
81
85
|
} else if (key === "--bundle-root" || key.startsWith("--bundle-root=")) options.bundleRoot = value();
|
|
@@ -256,6 +260,10 @@ function main() {
|
|
|
256
260
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
257
261
|
const manifest = readJson(path.join(repoRoot, ".cursor-plugin", "plugin.json"));
|
|
258
262
|
const skills = manifestSkills(manifest, repoRoot);
|
|
263
|
+
const devops = resolveDevops(null).devops;
|
|
264
|
+
const mcpServer = buildMcpServer(devops);
|
|
265
|
+
const mcpName = mcpServerName(devops);
|
|
266
|
+
const mcpAgents = options.noMcp ? [] : (options.agents.length > 0 ? options.agents : detectAgents(options.agentRoots)).filter((agent) => MCP_AGENTS.includes(agent));
|
|
259
267
|
const agents = options.agents.length > 0 ? options.agents : detectAgents(options.agentRoots);
|
|
260
268
|
if (agents.length === 0) {
|
|
261
269
|
fail("No agent detected (~/.cursor, ~/.claude, ~/.codex). Pass --agents cursor,claude,codex explicitly.");
|
|
@@ -273,6 +281,7 @@ function main() {
|
|
|
273
281
|
else log(`Would write ${agent} skills: ${path.join(options.agentRoots[agent], "skills")}`);
|
|
274
282
|
}
|
|
275
283
|
log(`Would write universal pointer: ${path.join(path.dirname(options.bundleRoot), "oris-flow.md")}`);
|
|
284
|
+
if (mcpAgents.length > 0) log(`Would seed MCP server '${mcpName}' for: ${mcpAgents.join(", ")}`);
|
|
276
285
|
return;
|
|
277
286
|
}
|
|
278
287
|
|
|
@@ -301,6 +310,11 @@ function main() {
|
|
|
301
310
|
}
|
|
302
311
|
installUniversalPointer(skills, options.bundleRoot);
|
|
303
312
|
|
|
313
|
+
for (const agent of mcpAgents) {
|
|
314
|
+
const result = seedMcpServer(agent, options.agentRoots, mcpName, mcpServer, { force: options.force });
|
|
315
|
+
log(`MCP ${mcpName} for ${agent}: ${result.status}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
304
318
|
writeJson(path.join(options.bundleRoot, installManifestName), {
|
|
305
319
|
package: manifest.name,
|
|
306
320
|
version: manifest.version,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
/** Agents that receive an MCP seed. Copilot has no home-scoped MCP target here. */
|
|
7
|
+
export const MCP_AGENTS = ["cursor", "claude", "codex"];
|
|
8
|
+
|
|
9
|
+
/** MCP config file + format per agent, derived from the (overridable) agent roots. */
|
|
10
|
+
export function mcpConfigTarget(agent, agentRoots) {
|
|
11
|
+
if (agent === "cursor") return { kind: "json", filePath: path.join(agentRoots.cursor, "mcp.json") };
|
|
12
|
+
if (agent === "claude") return { kind: "json", filePath: path.join(path.dirname(agentRoots.claude), ".claude.json") };
|
|
13
|
+
if (agent === "codex") return { kind: "toml", filePath: path.join(agentRoots.codex, "config.toml") };
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readText(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(filePath, "utf8");
|
|
20
|
+
} catch {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tomlBlock(name, server) {
|
|
26
|
+
const args = server.args.map((arg) => JSON.stringify(arg)).join(", ");
|
|
27
|
+
return `\n[mcp_servers.${name}]\ncommand = ${JSON.stringify(server.command)}\nargs = [${args}]\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Seed one MCP server into an agent's config, idempotently. Already present → skip unless
|
|
32
|
+
* `force`. `dryRun` reports the plan without writing. Returns a status per agent:
|
|
33
|
+
* "seeded" | "present" | "planned" | "skipped".
|
|
34
|
+
*/
|
|
35
|
+
export function seedMcpServer(agent, agentRoots, name, server, options = {}) {
|
|
36
|
+
const target = mcpConfigTarget(agent, agentRoots);
|
|
37
|
+
if (!target) return { status: "skipped", agent };
|
|
38
|
+
|
|
39
|
+
if (target.kind === "json") {
|
|
40
|
+
let data = {};
|
|
41
|
+
const existing = readText(target.filePath);
|
|
42
|
+
if (existing.trim()) {
|
|
43
|
+
try {
|
|
44
|
+
data = JSON.parse(existing);
|
|
45
|
+
} catch {
|
|
46
|
+
data = {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const servers = data.mcpServers && typeof data.mcpServers === "object" ? data.mcpServers : {};
|
|
50
|
+
const present = Object.prototype.hasOwnProperty.call(servers, name);
|
|
51
|
+
if (present && !options.force) return { status: "present", agent, filePath: target.filePath };
|
|
52
|
+
if (options.dryRun) return { status: "planned", agent, filePath: target.filePath };
|
|
53
|
+
servers[name] = server;
|
|
54
|
+
data.mcpServers = servers;
|
|
55
|
+
fs.mkdirSync(path.dirname(target.filePath), { recursive: true });
|
|
56
|
+
fs.writeFileSync(target.filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
57
|
+
return { status: "seeded", agent, filePath: target.filePath };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// TOML (Codex config.toml)
|
|
61
|
+
const text = readText(target.filePath);
|
|
62
|
+
const present = text.includes(`[mcp_servers.${name}]`);
|
|
63
|
+
if (present) return { status: "present", agent, filePath: target.filePath };
|
|
64
|
+
if (options.dryRun) return { status: "planned", agent, filePath: target.filePath };
|
|
65
|
+
fs.mkdirSync(path.dirname(target.filePath), { recursive: true });
|
|
66
|
+
fs.writeFileSync(target.filePath, `${text}${tomlBlock(name, server)}`, "utf8");
|
|
67
|
+
return { status: "seeded", agent, filePath: target.filePath };
|
|
68
|
+
}
|
|
@@ -14,6 +14,8 @@ const nodeTestFiles = [
|
|
|
14
14
|
"scripts/tests/test-skill-self-contained.mjs",
|
|
15
15
|
"scripts/tests/test-oris-1-0-cleanliness.mjs",
|
|
16
16
|
"scripts/tests/test-oris-flow-scan.mjs",
|
|
17
|
+
"scripts/tests/test-oris-flow-devops.mjs",
|
|
18
|
+
"scripts/tests/test-install-mcp.mjs",
|
|
17
19
|
"scripts/tests/test-routing-lifecycle.mjs",
|
|
18
20
|
"scripts/tests/test-skill-style.mjs",
|
|
19
21
|
"scripts/tests/test-oris-loop-stop.mjs",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import childProcess from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
11
|
+
|
|
12
|
+
function runCli(args, homeDir) {
|
|
13
|
+
return childProcess.spawnSync(process.execPath, ["scripts/oris-skills.mjs", ...args], {
|
|
14
|
+
cwd: root,
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir },
|
|
17
|
+
shell: false,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("install --dry-run plans the oris-devops MCP server", () => {
|
|
22
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "oris-mcp-dry-"));
|
|
23
|
+
try {
|
|
24
|
+
const result = runCli(["install", "--dry-run", "--agents", "claude,cursor,codex"], home);
|
|
25
|
+
assert.equal(result.status, 0, result.stderr);
|
|
26
|
+
assert.match(result.stdout, /Would seed MCP server 'oris-devops'/);
|
|
27
|
+
} finally {
|
|
28
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("--no-mcp skips seeding", () => {
|
|
33
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "oris-mcp-off-"));
|
|
34
|
+
try {
|
|
35
|
+
const result = runCli(["install", "--dry-run", "--agents", "claude", "--no-mcp"], home);
|
|
36
|
+
assert.equal(result.status, 0, result.stderr);
|
|
37
|
+
assert.doesNotMatch(result.stdout, /Would seed MCP/);
|
|
38
|
+
} finally {
|
|
39
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("real install seeds oris-devops into the Claude config, idempotently", () => {
|
|
44
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "oris-mcp-real-"));
|
|
45
|
+
try {
|
|
46
|
+
fs.mkdirSync(path.join(home, ".claude"), { recursive: true });
|
|
47
|
+
const first = runCli(["install", "--force", "--agents", "claude"], home);
|
|
48
|
+
assert.equal(first.status, 0, first.stderr);
|
|
49
|
+
|
|
50
|
+
const claudeConfig = path.join(home, ".claude.json");
|
|
51
|
+
assert.ok(fs.existsSync(claudeConfig), "~/.claude.json written");
|
|
52
|
+
const data = JSON.parse(fs.readFileSync(claudeConfig, "utf8"));
|
|
53
|
+
assert.ok(data.mcpServers["oris-devops"], "oris-devops seeded");
|
|
54
|
+
assert.deepEqual(
|
|
55
|
+
data.mcpServers["oris-devops"].args.slice(0, 3),
|
|
56
|
+
["-y", "@azure-devops/mcp@2.7.0", "orislineweb"],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const second = runCli(["install", "--force", "--agents", "claude"], home);
|
|
60
|
+
assert.equal(second.status, 0, second.stderr);
|
|
61
|
+
const after = JSON.parse(fs.readFileSync(claudeConfig, "utf8"));
|
|
62
|
+
assert.equal(Object.keys(after.mcpServers).length, 1, "no duplicate server on re-install");
|
|
63
|
+
} finally {
|
|
64
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { DEFAULT_DEVOPS, resolveDevops, buildMcpServer, mcpServerName } from "../flow/oris-flow-devops.mjs";
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
10
|
+
|
|
11
|
+
test("default provider is Azure DevOps with the oris-devops MCP and #{id} work items", () => {
|
|
12
|
+
const d = DEFAULT_DEVOPS.devops;
|
|
13
|
+
assert.equal(d.provider, "azure-devops");
|
|
14
|
+
assert.equal(d.organization, "orislineweb");
|
|
15
|
+
assert.equal(d.workItemSyntax, "#{id}");
|
|
16
|
+
assert.equal(d.commitLanguage, "en");
|
|
17
|
+
assert.equal(d.prLanguage, "en");
|
|
18
|
+
assert.equal(mcpServerName(d), "oris-devops");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("buildMcpServer produces the exact Azure DevOps command", () => {
|
|
22
|
+
const server = buildMcpServer(DEFAULT_DEVOPS.devops);
|
|
23
|
+
assert.deepEqual(server, {
|
|
24
|
+
command: "npx",
|
|
25
|
+
args: ["-y", "@azure-devops/mcp@2.7.0", "orislineweb", "-d", "core", "work", "work-items", "repositories"],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("buildMcpServer honours explicit args for a non-Azure provider", () => {
|
|
30
|
+
const server = buildMcpServer({ organization: "x", mcp: { command: "node", args: ["jira-mcp.js"] } });
|
|
31
|
+
assert.deepEqual(server, { command: "node", args: ["jira-mcp.js"] });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("resolution order is default -> org -> repo, each overriding the previous", () => {
|
|
35
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "oris-devops-home-"));
|
|
36
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), "oris-devops-repo-"));
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.join(home, ".oris"), { recursive: true });
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
path.join(home, ".oris", "devops.json"),
|
|
41
|
+
JSON.stringify({ version: 1, devops: { organization: "acme", workItemSyntax: "AB#{id}" } }),
|
|
42
|
+
);
|
|
43
|
+
fs.mkdirSync(path.join(repo, ".oris-flow"), { recursive: true });
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(repo, ".oris-flow", "devops.json"),
|
|
46
|
+
JSON.stringify({ version: 1, devops: { workItemSyntax: "JIRA-{id}" } }),
|
|
47
|
+
);
|
|
48
|
+
const resolved = resolveDevops(repo, { home }).devops;
|
|
49
|
+
assert.equal(resolved.provider, "azure-devops", "provider inherited from default");
|
|
50
|
+
assert.equal(resolved.organization, "acme", "organization from org layer");
|
|
51
|
+
assert.equal(resolved.workItemSyntax, "JIRA-{id}", "repo layer wins over org layer");
|
|
52
|
+
} finally {
|
|
53
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
54
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("devops schema is a versioned v1 contract", () => {
|
|
59
|
+
const schema = JSON.parse(fs.readFileSync(path.join(root, "references", "devops.schema.json"), "utf8"));
|
|
60
|
+
assert.equal(schema.$id, "https://oris.dev/schemas/oris-flow-devops-v1.json");
|
|
61
|
+
assert.equal(schema.properties.version.const, 1);
|
|
62
|
+
assert.deepEqual(schema.required, ["version", "devops"]);
|
|
63
|
+
assert.ok(schema.properties.devops.properties.provider);
|
|
64
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oris-flow
|
|
3
|
-
description: The one Oris skill - read the intent, pick ONE route, apply it in the same chat. Use for /oris-flow and whenever the user wants to set up a repo for agents, start a project, understand or define behavior, write acceptance criteria, plan, implement, fix a bug, verify criteria, handle a spec change, run a work loop (fix until green, repeat until verified), review architecture, update task docs, or asks how Oris works.
|
|
3
|
+
description: The one Oris skill - read the intent, pick ONE route, apply it in the same chat. Use for /oris-flow and whenever the user wants to set up a repo for agents, start a project, understand or define behavior, write acceptance criteria, plan, implement, fix a bug, verify criteria, handle a spec change, run a work loop (fix until green, repeat until verified), review architecture, update task docs, commit changes, open a pull request, or asks how Oris works.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Oris Flow — the router
|
|
@@ -32,6 +32,8 @@ Match the desired outcome to ONE route, then apply its reference:
|
|
|
32
32
|
| loop | repeat work until verified — fix until green, step through a plan | `skills/oris-flow/references/loop.md` |
|
|
33
33
|
| architecture | review architecture, find deepening/refactor opportunities | `skills/oris-flow/references/architecture.md` |
|
|
34
34
|
| docs | update task docs after a change | `skills/oris-flow/references/docs.md` |
|
|
35
|
+
| commit | commit changes with the Oris standard — map fresh, Conventional Commits, work item linked | `skills/oris-flow/references/commit.md` |
|
|
36
|
+
| pr | open or update a pull request, English title/description linked to the work item | `skills/oris-flow/references/pr.md` |
|
|
35
37
|
| help | how Oris works, install, update, troubleshooting | `skills/oris-flow/references/help.md` |
|
|
36
38
|
|
|
37
39
|
## Precedence (the calls that look ambiguous)
|
|
@@ -47,6 +49,8 @@ Match the desired outcome to ONE route, then apply its reference:
|
|
|
47
49
|
5. CHECK once → verify. Repeat-until-verified → loop.
|
|
48
50
|
6. EMPTY directory or from-scratch intent → new. Existing repo without
|
|
49
51
|
`.oris-flow/manifest.json` → recommend setup first; respect the user's choice.
|
|
52
|
+
7. COMMIT staged/working changes with the standard → commit. OPEN or update a pull request
|
|
53
|
+
→ pr. Neither writes product code — they package and publish what implement/fix produced.
|
|
50
54
|
|
|
51
55
|
## Menu (low confidence)
|
|
52
56
|
|
|
@@ -65,6 +69,8 @@ fallback in `references/conventions.md` `## Questions`), options:
|
|
|
65
69
|
- Loop: repeat until verified
|
|
66
70
|
- Architecture review
|
|
67
71
|
- Update docs
|
|
72
|
+
- Commit changes
|
|
73
|
+
- Open a pull request
|
|
68
74
|
- Help
|
|
69
75
|
- Explain the options / Spiega le opzioni
|
|
70
76
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Commit
|
|
2
|
+
|
|
3
|
+
Turn staged or working changes into one clean commit — map fresh, message in English,
|
|
4
|
+
work item linked. NEVER commit without showing the message and getting a yes.
|
|
5
|
+
|
|
6
|
+
RULES: `references/conventions.md`. DevOps config: `references/devops.md`.
|
|
7
|
+
|
|
8
|
+
## Before the commit
|
|
9
|
+
|
|
10
|
+
1. FRESHEN the map: `npx oris-skills flow scan --repository-root <repo> --check`. Stale →
|
|
11
|
+
refresh ONLY the changed areas (`--area <section>`) and stage `.oris-flow/` with the change.
|
|
12
|
+
2. READ the diff (`git status`, `git diff --staged`) to know what actually changed.
|
|
13
|
+
3. RESOLVE the work item id from context — current branch, the active task doc, or the
|
|
14
|
+
DevOps provider's active items. Unsure → ASK once for the id; never invent one.
|
|
15
|
+
|
|
16
|
+
## Compose (always English)
|
|
17
|
+
|
|
18
|
+
- TYPE from Conventional Commits: `feat` | `fix` | `chore` | `docs` | `refactor` | `test`
|
|
19
|
+
| `perf` | `build` | `ci`. Pick from the diff, not the chat.
|
|
20
|
+
- SUBJECT imperative, ≤72 chars, no trailing period. Optional `(scope)`.
|
|
21
|
+
- BODY only when it adds signal: what and why, not how.
|
|
22
|
+
- FOOTER: the work item via the provider `workItemSyntax` (default `#{id}`), on its own line.
|
|
23
|
+
|
|
24
|
+
## Gate (never skipped)
|
|
25
|
+
|
|
26
|
+
SHOW the full message + the files it will commit (code + refreshed map). ASK:
|
|
27
|
+
commit | edit message | cancel. ON commit → `git commit`; NEVER `--no-verify`.
|
|
28
|
+
|
|
29
|
+
## After
|
|
30
|
+
|
|
31
|
+
- SUMMARIZE the commit (hash, subject, work item).
|
|
32
|
+
- OFFER a pull request (`skills/oris-flow/references/pr.md`) or stop. NEVER auto-open one.
|
|
33
|
+
|
|
34
|
+
## Never
|
|
35
|
+
|
|
36
|
+
- Commit before the message and file list are shown and confirmed.
|
|
37
|
+
- Write the message in any language but English.
|
|
38
|
+
- Invent a work item id, or bypass hooks with `--no-verify`.
|
|
39
|
+
- Refresh confirmed map sections, or scan-rewrite areas the diff did not touch.
|
|
40
|
+
|
|
41
|
+
## Done when
|
|
42
|
+
|
|
43
|
+
- [ ] Map checked, and refreshed in place only where the diff touched.
|
|
44
|
+
- [ ] English Conventional message with the work item linked, confirmed at the gate.
|
|
45
|
+
- [ ] Commit made without `--no-verify`; next step offered, not auto-run.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Pull Request
|
|
2
|
+
|
|
3
|
+
Open or update a pull request whose title and description are English and linked to the
|
|
4
|
+
work item. NEVER open or update a PR without showing it and getting a yes.
|
|
5
|
+
|
|
6
|
+
RULES: `references/conventions.md`. DevOps config: `references/devops.md`.
|
|
7
|
+
|
|
8
|
+
## Before
|
|
9
|
+
|
|
10
|
+
1. ENSURE the branch is pushed and its commits follow the commit standard
|
|
11
|
+
(`skills/oris-flow/references/commit.md`); uncommitted work remains → route there first.
|
|
12
|
+
2. READ the commit range to summarize the actual change, not the intent.
|
|
13
|
+
3. RESOLVE the work item id (branch, task doc, or DevOps provider) — unsure → ASK once.
|
|
14
|
+
|
|
15
|
+
## Compose (always English)
|
|
16
|
+
|
|
17
|
+
- TITLE: one Conventional line, same grammar as a commit subject.
|
|
18
|
+
- DESCRIPTION: a short **Summary** (what changed and why), a **Verification** line
|
|
19
|
+
(how it was proven), and the work item via the provider `workItemSyntax` (default `#{id}`).
|
|
20
|
+
- KEEP secrets out — mask anything sensitive as `***` (`references/conventions.md` `## Gates`).
|
|
21
|
+
|
|
22
|
+
## Gate (never skipped)
|
|
23
|
+
|
|
24
|
+
SHOW the title + description + target branch. ASK: open PR | edit | cancel.
|
|
25
|
+
ON open → create via the provider (Azure DevOps MCP `oris-devops`, or `gh` for GitHub)
|
|
26
|
+
read from `references/devops.md`. Missing provider auth → say so; never guess credentials.
|
|
27
|
+
|
|
28
|
+
## After
|
|
29
|
+
|
|
30
|
+
- SUMMARIZE: PR link/number, target branch, work item.
|
|
31
|
+
- RECOMMEND verify (`skills/oris-flow/references/verify.md`) when criteria exist.
|
|
32
|
+
NEVER merge automatically.
|
|
33
|
+
|
|
34
|
+
## Never
|
|
35
|
+
|
|
36
|
+
- Open or update a PR before the title and description are shown and confirmed.
|
|
37
|
+
- Write the title or description in any language but English.
|
|
38
|
+
- Invent a work item id, store credentials, or merge without being asked.
|
|
39
|
+
|
|
40
|
+
## Done when
|
|
41
|
+
|
|
42
|
+
- [ ] Branch pushed and commits follow the commit standard.
|
|
43
|
+
- [ ] English title + description with the work item linked, confirmed at the gate.
|
|
44
|
+
- [ ] PR opened/updated via the configured provider; merge never automatic.
|