oris-skills 2.2.2 → 3.0.1
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/.cursor-plugin/plugin.json +2 -3
- package/CHANGELOG.md +24 -6
- package/README.md +23 -59
- package/docs/architecture.md +15 -11
- package/docs/distribution.md +3 -3
- package/docs/maintainer-guide.md +4 -4
- package/docs/user-guide.md +12 -20
- package/package.json +3 -6
- package/references/conventions.md +23 -13
- package/references/devops.md +58 -0
- package/references/devops.schema.json +54 -0
- package/references/doc-policy.md +5 -5
- package/references/research.md +45 -0
- package/references/settings.md +11 -5
- package/references/settings.schema.json +10 -0
- package/scripts/flow/oris-flow-devops.mjs +77 -0
- package/scripts/flow/oris-flow-layout.mjs +0 -16
- package/scripts/flow/oris-flow-scan.mjs +32 -317
- package/scripts/flow/oris-gitignore.mjs +1 -5
- package/scripts/install/install-user-skills.mjs +17 -3
- package/scripts/install/oris-mcp.mjs +68 -0
- package/scripts/install/uninstall-user-skills.mjs +2 -29
- package/scripts/oris-skills.mjs +0 -47
- package/scripts/tests/run-all-tests.mjs +3 -9
- package/scripts/tests/test-cleanliness.mjs +54 -0
- package/scripts/tests/test-install-mcp.mjs +66 -0
- package/scripts/tests/test-oris-flow-devops.mjs +64 -0
- package/scripts/tests/test-oris-flow-scan.mjs +21 -91
- package/scripts/tests/test-routing-lifecycle.mjs +22 -53
- package/scripts/tests/test-schemas.mjs +17 -31
- package/scripts/tests/test-skill-style.mjs +5 -2
- package/skills/oris-flow/SKILL.md +10 -6
- package/skills/oris-flow/references/architecture.md +1 -1
- package/skills/oris-flow/references/commit.md +46 -0
- package/skills/oris-flow/references/criteria.md +2 -2
- package/skills/oris-flow/references/discover.md +2 -2
- package/skills/oris-flow/references/fix.md +7 -3
- package/skills/oris-flow/references/help.md +1 -6
- package/skills/oris-flow/references/implement.md +5 -3
- package/skills/oris-flow/references/new.md +6 -6
- package/skills/oris-flow/references/plan.md +7 -2
- package/skills/oris-flow/references/pr.md +44 -0
- package/skills/oris-flow/references/setup.md +28 -35
- package/skills/oris-flow/references/verify.md +4 -5
- package/agents/oris-loop-debriefer.md +0 -30
- package/agents/oris-loop-doctor.md +0 -32
- package/agents/oris-loop-executor.md +0 -35
- package/agents/oris-loop-verifier.md +0 -35
- package/references/loop-adapter.schema.json +0 -93
- package/references/loop-contract.md +0 -126
- package/references/loop.schema.json +0 -156
- package/references/repo-map.md +0 -51
- package/references/repo-map.schema.json +0 -213
- package/scripts/flow/oris-flow-clean-runtime.mjs +0 -182
- package/scripts/install/generate-agent-adapters.mjs +0 -81
- package/scripts/loop/oris-loop-bootstrap.mjs +0 -383
- package/scripts/loop/oris-loop-bundle.mjs +0 -22
- package/scripts/loop/oris-loop-chat.mjs +0 -396
- package/scripts/loop/oris-loop-demo.mjs +0 -171
- package/scripts/loop/oris-loop-document.mjs +0 -196
- package/scripts/loop/oris-loop-dry-run.mjs +0 -114
- package/scripts/loop/oris-loop-fixtures.mjs +0 -149
- package/scripts/loop/oris-loop-list.mjs +0 -81
- package/scripts/loop/oris-loop-paths.mjs +0 -232
- package/scripts/loop/oris-loop-run.mjs +0 -301
- package/scripts/loop/oris-loop-stop.mjs +0 -358
- package/scripts/loop/oris-loop-templates.mjs +0 -80
- package/scripts/loop/oris-loop-verify.mjs +0 -205
- package/scripts/tests/test-agent-adapters.mjs +0 -70
- package/scripts/tests/test-oris-1-0-cleanliness.mjs +0 -58
- package/scripts/tests/test-oris-flow-clean-runtime.mjs +0 -75
- package/scripts/tests/test-oris-loop-bootstrap.mjs +0 -94
- package/scripts/tests/test-oris-loop-document.mjs +0 -148
- package/scripts/tests/test-oris-loop-list.mjs +0 -74
- package/scripts/tests/test-oris-loop-run.mjs +0 -71
- package/scripts/tests/test-oris-loop-smoke.mjs +0 -120
- package/scripts/tests/test-oris-loop-stop.mjs +0 -432
- package/skills/oris-flow/references/loop-craft.md +0 -59
- package/skills/oris-flow/references/loop-improve.md +0 -32
- package/skills/oris-flow/references/loop-run.md +0 -47
- package/skills/oris-flow/references/loop.md +0 -56
- package/skills/oris-flow/templates/debriefer.md +0 -19
- package/skills/oris-flow/templates/doctor.md +0 -22
- package/skills/oris-flow/templates/executor.md +0 -25
- package/skills/oris-flow/templates/orchestrator.md +0 -36
- package/skills/oris-flow/templates/verifier.md +0 -24
|
@@ -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
|
+
}
|
|
@@ -24,7 +24,6 @@ function parseArgs(argv) {
|
|
|
24
24
|
const options = {
|
|
25
25
|
dryRun: false,
|
|
26
26
|
bundleRoot: path.join(home, ".oris", "oris-skills"),
|
|
27
|
-
settingsPath: path.join(home, ".oris", "settings.json"),
|
|
28
27
|
cursorRoot: path.join(home, ".cursor"),
|
|
29
28
|
claudeRoot: path.join(home, ".claude"),
|
|
30
29
|
codexRoot: path.join(home, ".codex"),
|
|
@@ -45,7 +44,6 @@ function parseArgs(argv) {
|
|
|
45
44
|
};
|
|
46
45
|
if (key === "--dry-run") options.dryRun = true;
|
|
47
46
|
else if (key === "--bundle-root" || key.startsWith("--bundle-root=")) options.bundleRoot = value();
|
|
48
|
-
else if (key === "--settings" || key.startsWith("--settings=")) options.settingsPath = value();
|
|
49
47
|
else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.cursorRoot = value();
|
|
50
48
|
else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.claudeRoot = value();
|
|
51
49
|
else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.codexRoot = value();
|
|
@@ -92,15 +90,6 @@ function managedCodexPrompts(codexRoot) {
|
|
|
92
90
|
.map((entry) => path.join(promptsRoot, entry.name));
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
/** Loop agent adapters that pre-2.2.1 installers wrote into <agent home>/agents/. */
|
|
96
|
-
function managedAgentAdapters(agentRoot, extension) {
|
|
97
|
-
const agentsRoot = path.join(agentRoot, "agents");
|
|
98
|
-
if (!fs.existsSync(agentsRoot)) return [];
|
|
99
|
-
return fs.readdirSync(agentsRoot, { withFileTypes: true })
|
|
100
|
-
.filter((entry) => entry.isFile() && entry.name.toLowerCase().startsWith("oris-") && entry.name.endsWith(extension))
|
|
101
|
-
.map((entry) => path.join(agentsRoot, entry.name));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
93
|
function managedCopilotPrompts(copilotRoot) {
|
|
105
94
|
const promptsRoot = path.join(copilotRoot, "prompts");
|
|
106
95
|
if (!fs.existsSync(promptsRoot)) return [];
|
|
@@ -109,18 +98,6 @@ function managedCopilotPrompts(copilotRoot) {
|
|
|
109
98
|
.map((entry) => path.join(promptsRoot, entry.name));
|
|
110
99
|
}
|
|
111
100
|
|
|
112
|
-
function legacyOrisPaths(options) {
|
|
113
|
-
return [
|
|
114
|
-
path.join(options.cursorRoot, "oris-skills"),
|
|
115
|
-
path.join(options.cursorRoot, "oris-flow"),
|
|
116
|
-
path.join(options.cursorRoot, "oris-loop"),
|
|
117
|
-
path.join(options.cursorRoot, "oris-skills.settings.json"),
|
|
118
|
-
path.join(options.cursorRoot, ".cursor-plugin-install-backups"),
|
|
119
|
-
path.join(options.cursorRoot, "plugins", "oris-skills"),
|
|
120
|
-
options.settingsPath,
|
|
121
|
-
];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
101
|
function plannedRemovals(options) {
|
|
125
102
|
const paths = new Set([
|
|
126
103
|
options.bundleRoot,
|
|
@@ -128,11 +105,7 @@ function plannedRemovals(options) {
|
|
|
128
105
|
...managedSkillDirs(path.join(options.claudeRoot, "skills")),
|
|
129
106
|
...managedCodexPrompts(options.codexRoot),
|
|
130
107
|
...managedCopilotPrompts(options.copilotRoot),
|
|
131
|
-
...managedAgentAdapters(options.cursorRoot, ".md"),
|
|
132
|
-
...managedAgentAdapters(options.claudeRoot, ".md"),
|
|
133
|
-
...managedAgentAdapters(options.codexRoot, ".toml"),
|
|
134
108
|
path.join(path.dirname(options.bundleRoot), "oris-flow.md"),
|
|
135
|
-
...legacyOrisPaths(options),
|
|
136
109
|
]);
|
|
137
110
|
return [...paths].map((target) => path.resolve(target)).filter(safeOrisPath).sort();
|
|
138
111
|
}
|
|
@@ -153,7 +126,7 @@ function removeOrisMcpServers(userMcpPath, dryRun) {
|
|
|
153
126
|
|
|
154
127
|
export function uninstallUserSkills(rawArgs = []) {
|
|
155
128
|
const options = parseArgs(rawArgs);
|
|
156
|
-
for (const target of [options.bundleRoot, options.
|
|
129
|
+
for (const target of [options.bundleRoot, options.cursorRoot, options.claudeRoot, options.codexRoot, options.copilotRoot]) {
|
|
157
130
|
assertInsideHome(target);
|
|
158
131
|
}
|
|
159
132
|
|
|
@@ -161,7 +134,7 @@ export function uninstallUserSkills(rawArgs = []) {
|
|
|
161
134
|
log(options.dryRun ? "Dry run uninstall plan:" : "Uninstalling Oris-owned assets:");
|
|
162
135
|
for (const target of removals) log(`${options.dryRun ? "Would remove" : "Remove"} ${target}`);
|
|
163
136
|
|
|
164
|
-
//
|
|
137
|
+
// The installer seeds the Oris MCP server into Cursor's user config; remove it with the rest.
|
|
165
138
|
const mcpResult = removeOrisMcpServers(path.join(options.cursorRoot, "mcp.json"), options.dryRun);
|
|
166
139
|
if (mcpResult.changed) log(`${options.dryRun ? "Would update" : "Updated"} ${mcpResult.path}`);
|
|
167
140
|
|
package/scripts/oris-skills.mjs
CHANGED
|
@@ -4,7 +4,6 @@ import childProcess from "node:child_process";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { bootstrapLoop } from "./loop/oris-loop-bootstrap.mjs";
|
|
8
7
|
|
|
9
8
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
9
|
|
|
@@ -19,15 +18,6 @@ function runNode(script, args, options = {}) {
|
|
|
19
18
|
process.exit(status);
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
function runGitRoot() {
|
|
23
|
-
const result = childProcess.spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
24
|
-
cwd: process.cwd(),
|
|
25
|
-
shell: process.platform === "win32",
|
|
26
|
-
encoding: "utf8",
|
|
27
|
-
});
|
|
28
|
-
return result.status === 0 && result.stdout.trim() ? path.resolve(result.stdout.trim()) : process.cwd();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
21
|
function printHelp() {
|
|
32
22
|
console.log(`Oris Skills
|
|
33
23
|
|
|
@@ -36,24 +26,10 @@ Install for your agents (Cursor, Claude Code, Codex):
|
|
|
36
26
|
oris-skills uninstall [--dry-run]
|
|
37
27
|
oris-skills reinstall [--dry-run]
|
|
38
28
|
|
|
39
|
-
Learn and test loops:
|
|
40
|
-
oris-skills loop demo create a safe tutorial loop
|
|
41
|
-
oris-skills loop dry-run --loop <slug> preview the next pass, run nothing
|
|
42
|
-
oris-skills loop verify --temp self-check the loop runtime
|
|
43
|
-
|
|
44
|
-
Run and manage loops:
|
|
45
|
-
oris-skills loop list [--json]
|
|
46
|
-
oris-skills loop bootstrap [--platforms cursor,claude]
|
|
47
|
-
oris-skills loop chat --action start --loop <slug>
|
|
48
|
-
oris-skills loop chat --action status|set-state|repair|stop [...]
|
|
49
|
-
oris-skills loop run --loop <slug> --agent codex|claude headless runner
|
|
50
|
-
|
|
51
29
|
Repository tools:
|
|
52
|
-
oris-skills doctor
|
|
53
30
|
oris-skills validate
|
|
54
31
|
oris-skills flow scan [...]
|
|
55
32
|
oris-skills flow version-control [...]
|
|
56
|
-
oris-skills flow clean-runtime [...]
|
|
57
33
|
`);
|
|
58
34
|
}
|
|
59
35
|
|
|
@@ -79,37 +55,14 @@ if (command === "reinstall") {
|
|
|
79
55
|
runNode("install/install-user-skills.mjs", ["--force", ...commandArgs]);
|
|
80
56
|
}
|
|
81
57
|
|
|
82
|
-
if (command === "doctor") {
|
|
83
|
-
const root = runGitRoot();
|
|
84
|
-
const result = bootstrapLoop(root, { dryRun: true });
|
|
85
|
-
console.log(JSON.stringify({
|
|
86
|
-
repositoryRoot: root,
|
|
87
|
-
ready: result.ready,
|
|
88
|
-
missing: result.before.missing,
|
|
89
|
-
plannedActions: result.actions,
|
|
90
|
-
}, null, 2));
|
|
91
|
-
process.exit(result.ready ? 0 : 1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
58
|
if (command === "validate") {
|
|
95
59
|
// Kept as an alias of the full test suite for older docs and muscle memory.
|
|
96
60
|
runNode("tests/run-all-tests.mjs", []);
|
|
97
61
|
}
|
|
98
62
|
|
|
99
|
-
if (command === "loop") {
|
|
100
|
-
if (subcommand === "list") runNode("loop/oris-loop-list.mjs", rest);
|
|
101
|
-
if (subcommand === "bootstrap") runNode("loop/oris-loop-bootstrap.mjs", rest);
|
|
102
|
-
if (subcommand === "chat") runNode("loop/oris-loop-chat.mjs", rest);
|
|
103
|
-
if (subcommand === "verify") runNode("loop/oris-loop-verify.mjs", rest);
|
|
104
|
-
if (subcommand === "demo") runNode("loop/oris-loop-demo.mjs", rest);
|
|
105
|
-
if (subcommand === "dry-run") runNode("loop/oris-loop-dry-run.mjs", rest);
|
|
106
|
-
if (subcommand === "run") runNode("loop/oris-loop-run.mjs", rest);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
63
|
if (command === "flow") {
|
|
110
64
|
if (subcommand === "scan") runNode("flow/oris-flow-scan.mjs", rest);
|
|
111
65
|
if (subcommand === "version-control") runNode("flow/oris-flow-version-control.mjs", rest);
|
|
112
|
-
if (subcommand === "clean-runtime") runNode("flow/oris-flow-clean-runtime.mjs", rest);
|
|
113
66
|
}
|
|
114
67
|
|
|
115
68
|
console.error(`Unknown command: ${[command, subcommand].filter(Boolean).join(" ")}`);
|
|
@@ -9,19 +9,13 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".
|
|
|
9
9
|
|
|
10
10
|
const nodeTestFiles = [
|
|
11
11
|
"scripts/tests/test-schemas.mjs",
|
|
12
|
-
"scripts/tests/test-oris-loop-document.mjs",
|
|
13
|
-
"scripts/tests/test-agent-adapters.mjs",
|
|
14
12
|
"scripts/tests/test-skill-self-contained.mjs",
|
|
15
|
-
"scripts/tests/test-
|
|
13
|
+
"scripts/tests/test-cleanliness.mjs",
|
|
16
14
|
"scripts/tests/test-oris-flow-scan.mjs",
|
|
15
|
+
"scripts/tests/test-oris-flow-devops.mjs",
|
|
16
|
+
"scripts/tests/test-install-mcp.mjs",
|
|
17
17
|
"scripts/tests/test-routing-lifecycle.mjs",
|
|
18
18
|
"scripts/tests/test-skill-style.mjs",
|
|
19
|
-
"scripts/tests/test-oris-loop-stop.mjs",
|
|
20
|
-
"scripts/tests/test-oris-loop-smoke.mjs",
|
|
21
|
-
"scripts/tests/test-oris-loop-bootstrap.mjs",
|
|
22
|
-
"scripts/tests/test-oris-flow-clean-runtime.mjs",
|
|
23
|
-
"scripts/tests/test-oris-loop-list.mjs",
|
|
24
|
-
"scripts/tests/test-oris-loop-run.mjs",
|
|
25
19
|
"scripts/tests/test-oris-gitignore.mjs",
|
|
26
20
|
];
|
|
27
21
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
|
+
const ignoredDirs = new Set([".git", "node_modules", ".cursor", ".claude", ".codex", "dist", ".cursor-plugin-install-backups", ".oris-flow"]);
|
|
9
|
+
|
|
10
|
+
function walk(dir, matches = []) {
|
|
11
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
12
|
+
if (ignoredDirs.has(entry.name)) continue;
|
|
13
|
+
const full = path.join(dir, entry.name);
|
|
14
|
+
if (entry.isDirectory()) walk(full, matches);
|
|
15
|
+
else matches.push(full);
|
|
16
|
+
}
|
|
17
|
+
return matches;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function repoFiles() {
|
|
21
|
+
return walk(repoRoot).map((filePath) => ({
|
|
22
|
+
filePath,
|
|
23
|
+
relative: path.relative(repoRoot, filePath).replace(/\\/g, "/"),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("repository contains no PowerShell scripts", () => {
|
|
28
|
+
const psFiles = repoFiles().filter((entry) => entry.relative.endsWith(".ps1"));
|
|
29
|
+
assert.deepEqual(psFiles.map((entry) => entry.relative), []);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Machinery removed in 3.0: the loop runtime and the manifest/maps knowledge store.
|
|
33
|
+
// The agent-facing surface (skills, references, docs, README) must never point at it;
|
|
34
|
+
// CHANGELOG keeps the history, and the uninstaller/gitignore keep legacy-cleanup paths.
|
|
35
|
+
const removedMachinery = /oris-skills loop |scripts\/loop\/|\.oris-flow\/(?:loops|runtime|manifest\.json|maps|adapter\.json)|loop-contract|references\/repo-map|flow scan[^\n]*--(?:check|write|area)|flow clean-runtime/;
|
|
36
|
+
|
|
37
|
+
test("agent-facing text never references machinery removed in 3.0", () => {
|
|
38
|
+
const offenders = repoFiles()
|
|
39
|
+
.filter((entry) => /^(skills|references|docs)\/.*\.md$|^(README|AGENTS|CONTEXT)\.md$/.test(entry.relative))
|
|
40
|
+
.filter((entry) => removedMachinery.test(fs.readFileSync(entry.filePath, "utf8")))
|
|
41
|
+
.map((entry) => entry.relative);
|
|
42
|
+
assert.deepEqual(offenders, []);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("production scripts never invoke machinery removed in 3.0", () => {
|
|
46
|
+
const offenders = repoFiles()
|
|
47
|
+
// scripts/tests assert absences; the uninstaller/gitignore clean legacy paths — both must name them.
|
|
48
|
+
.filter((entry) => /^scripts\/(flow|install)\/.*\.mjs$|^scripts\/[^/]+\.mjs$/.test(entry.relative))
|
|
49
|
+
.filter((entry) => entry.relative !== "scripts/install/uninstall-user-skills.mjs")
|
|
50
|
+
.filter((entry) => entry.relative !== "scripts/flow/oris-gitignore.mjs")
|
|
51
|
+
.filter((entry) => removedMachinery.test(fs.readFileSync(entry.filePath, "utf8")))
|
|
52
|
+
.map((entry) => entry.relative);
|
|
53
|
+
assert.deepEqual(offenders, []);
|
|
54
|
+
});
|
|
@@ -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,18 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
-
import
|
|
7
|
-
import { checkFreshness, scanRepository, writeScanResult } from "../flow/oris-flow-scan.mjs";
|
|
8
|
-
import { ORIS_FLOW_MANIFEST } from "../flow/oris-flow-layout.mjs";
|
|
6
|
+
import { scanRepository } from "../flow/oris-flow-scan.mjs";
|
|
9
7
|
|
|
10
8
|
function write(filePath, content) {
|
|
11
9
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
12
10
|
fs.writeFileSync(filePath, content, "utf8");
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
test("scanner
|
|
13
|
+
test("scanner reports only evidence-backed areas and writes nothing", () => {
|
|
16
14
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
17
15
|
try {
|
|
18
16
|
write(path.join(root, "README.md"), "# Demo\n");
|
|
@@ -20,17 +18,13 @@ test("scanner creates only evidence-backed map sections", () => {
|
|
|
20
18
|
write(path.join(root, "src", "index.js"), "export const value = 1;\n");
|
|
21
19
|
|
|
22
20
|
const scan = scanRepository(root);
|
|
23
|
-
assert.ok(scan.
|
|
24
|
-
assert.ok(scan.
|
|
25
|
-
assert.ok(scan.
|
|
26
|
-
assert.ok(scan.
|
|
27
|
-
assert.equal(scan.
|
|
28
|
-
assert.equal(scan.manifest["known" + "Gaps"], undefined);
|
|
21
|
+
assert.ok(scan.areas.docs);
|
|
22
|
+
assert.ok(scan.areas.code);
|
|
23
|
+
assert.ok(scan.stacks.some((stack) => stack.id === "node"));
|
|
24
|
+
assert.ok(scan.commands.some((command) => command.command === "npm run test"));
|
|
25
|
+
assert.equal(scan.areas.tests, undefined, "no test evidence → no tests area");
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
assert.equal(written.manifestPath, ORIS_FLOW_MANIFEST);
|
|
32
|
-
assert.ok(fs.existsSync(path.join(root, ORIS_FLOW_MANIFEST)));
|
|
33
|
-
assert.ok(fs.existsSync(path.join(root, ".oris-flow", "maps", "stack.md")));
|
|
27
|
+
assert.equal(fs.existsSync(path.join(root, ".oris-flow")), false, "scan never writes");
|
|
34
28
|
} finally {
|
|
35
29
|
fs.rmSync(root, { recursive: true, force: true });
|
|
36
30
|
}
|
|
@@ -41,8 +35,9 @@ test("scanner supports documentation-only repositories", () => {
|
|
|
41
35
|
try {
|
|
42
36
|
write(path.join(root, "docs", "guide.md"), "# Guide\n");
|
|
43
37
|
const scan = scanRepository(root);
|
|
44
|
-
assert.deepEqual(Object.keys(scan.
|
|
38
|
+
assert.deepEqual(Object.keys(scan.areas), ["docs"]);
|
|
45
39
|
assert.deepEqual(scan.stacks, []);
|
|
40
|
+
assert.deepEqual(scan.commands, []);
|
|
46
41
|
} finally {
|
|
47
42
|
fs.rmSync(root, { recursive: true, force: true });
|
|
48
43
|
}
|
|
@@ -61,96 +56,31 @@ test("scanner detects project type and derives inferred stack commands", () => {
|
|
|
61
56
|
const dotnetTest = scan.commands.find((command) => command.command.startsWith("dotnet test"));
|
|
62
57
|
assert.ok(dotnetTest, "expected an inferred dotnet test command");
|
|
63
58
|
assert.equal(dotnetTest.confidence, "inferred");
|
|
64
|
-
assert.equal(scan.manifest.setup.tasksRoot, ".oris-flow/tasks");
|
|
65
59
|
} finally {
|
|
66
60
|
fs.rmSync(root, { recursive: true, force: true });
|
|
67
61
|
}
|
|
68
62
|
});
|
|
69
63
|
|
|
70
|
-
test("
|
|
64
|
+
test("scanner flags truncation instead of silently under-reporting", () => {
|
|
71
65
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
72
66
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
first.manifest.sections.docs.confidence = "confirmed";
|
|
80
|
-
first.manifest.commands[0].confidence = "confirmed";
|
|
81
|
-
writeScanResult(first);
|
|
82
|
-
const enriched = "# Docs\n\nUser-confirmed enrichment that must survive refresh.\n";
|
|
83
|
-
write(path.join(root, ".oris-flow", "maps", "docs.md"), enriched);
|
|
84
|
-
|
|
85
|
-
const second = scanRepository(root);
|
|
86
|
-
assert.equal(second.manifest.setup.tasksRoot, "docs/tasks");
|
|
87
|
-
assert.deepEqual(second.manifest.setup.confirmedDecisions, ["tests live under scripts/tests"]);
|
|
88
|
-
assert.equal(second.manifest.sections.docs.confidence, "confirmed");
|
|
89
|
-
assert.equal(second.manifest.sections.docs.stale, false);
|
|
90
|
-
assert.equal(second.manifest.commands[0].confidence, "confirmed");
|
|
91
|
-
assert.ok(second.preservedSections.has("docs"));
|
|
92
|
-
|
|
93
|
-
writeScanResult(second);
|
|
94
|
-
assert.equal(fs.readFileSync(path.join(root, ".oris-flow", "maps", "docs.md"), "utf8"), enriched);
|
|
67
|
+
for (let index = 0; index < 12; index += 1) {
|
|
68
|
+
write(path.join(root, "src", `file-${index}.js`), "export {};\n");
|
|
69
|
+
}
|
|
70
|
+
const scan = scanRepository(root, { maxFiles: 5 });
|
|
71
|
+
assert.equal(scan.truncated, true);
|
|
72
|
+
assert.ok(scan.fileCount <= 5);
|
|
95
73
|
} finally {
|
|
96
74
|
fs.rmSync(root, { recursive: true, force: true });
|
|
97
75
|
}
|
|
98
76
|
});
|
|
99
77
|
|
|
100
|
-
test("
|
|
101
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
102
|
-
try {
|
|
103
|
-
write(path.join(root, "README.md"), "# Demo\n");
|
|
104
|
-
const first = scanRepository(root);
|
|
105
|
-
first.manifest.sections.docs.confidence = "confirmed";
|
|
106
|
-
writeScanResult(first);
|
|
107
|
-
|
|
108
|
-
fs.rmSync(path.join(root, "README.md"));
|
|
109
|
-
const second = scanRepository(root);
|
|
110
|
-
assert.equal(second.manifest.sections.docs.confidence, "confirmed");
|
|
111
|
-
assert.equal(second.manifest.sections.docs.stale, true);
|
|
112
|
-
} finally {
|
|
113
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
test("scan anchors the map to HEAD; --check answers freshness from the SHA + changed files", () => {
|
|
78
|
+
test("empty repository classifies as empty", () => {
|
|
119
79
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
120
|
-
const git = (...args) => childProcess.execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
121
80
|
try {
|
|
122
|
-
write(path.join(root, "README.md"), "# Demo\n");
|
|
123
|
-
write(path.join(root, "package.json"), `${JSON.stringify({ scripts: { test: "node --test" } })}\n`);
|
|
124
|
-
git("init", "-q");
|
|
125
|
-
git("-c", "user.email=t@t", "-c", "user.name=t", "add", ".");
|
|
126
|
-
git("-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "init");
|
|
127
|
-
|
|
128
|
-
const scan = scanRepository(root);
|
|
129
|
-
assert.equal(typeof scan.manifest.headSha, "string", "scan records the HEAD anchor");
|
|
130
|
-
writeScanResult(scan);
|
|
131
|
-
|
|
132
|
-
const fresh = checkFreshness(root);
|
|
133
|
-
assert.equal(fresh.fresh, true, "same SHA + clean tree → fresh, zero reads");
|
|
134
|
-
assert.deepEqual(fresh.changedFiles, []);
|
|
135
|
-
|
|
136
|
-
write(path.join(root, "src", "new.js"), "export const later = 1;\n");
|
|
137
|
-
const stale = checkFreshness(root);
|
|
138
|
-
assert.equal(stale.fresh, false, "working-tree change → stale");
|
|
139
|
-
assert.ok(stale.changedFiles.some((file) => file.includes("new.js")), "changed files name the delta");
|
|
140
|
-
} finally {
|
|
141
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("outside git the map still works — no anchor, never an error", () => {
|
|
146
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
147
|
-
try {
|
|
148
|
-
write(path.join(root, "README.md"), "# Demo\n");
|
|
149
81
|
const scan = scanRepository(root);
|
|
150
|
-
assert.equal(scan.
|
|
151
|
-
|
|
152
|
-
const check = checkFreshness(root);
|
|
153
|
-
assert.equal(check.fresh, false, "no anchor → refresh advised, not an error");
|
|
82
|
+
assert.equal(scan.projectType.id, "empty");
|
|
83
|
+
assert.deepEqual(scan.areas, {});
|
|
154
84
|
} finally {
|
|
155
85
|
fs.rmSync(root, { recursive: true, force: true });
|
|
156
86
|
}
|