oris-skills 2.1.0 → 2.2.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 -16
- package/CHANGELOG.md +15 -1
- package/README.md +79 -32
- package/agents/oris-loop-debriefer.md +1 -0
- package/agents/oris-loop-executor.md +5 -2
- package/agents/oris-loop-verifier.md +3 -2
- package/docs/architecture.md +7 -6
- package/docs/distribution.md +1 -1
- package/docs/maintainer-guide.md +3 -3
- package/docs/user-guide.md +5 -4
- package/package.json +1 -1
- package/references/clean-code-checklist.md +1 -1
- package/references/conventions.md +28 -2
- package/references/loop-contract.md +1 -1
- package/references/repo-map.schema.json +4 -0
- package/references/settings.md +2 -1
- package/references/settings.schema.json +5 -0
- package/scripts/flow/oris-flow-scan.mjs +70 -0
- package/scripts/install/generate-agent-adapters.mjs +5 -15
- package/scripts/install/install-user-skills.mjs +54 -8
- package/scripts/install/uninstall-user-skills.mjs +29 -1
- package/scripts/loop/oris-loop-chat.mjs +2 -2
- package/scripts/loop/oris-loop-dry-run.mjs +1 -1
- package/scripts/loop/oris-loop-fixtures.mjs +1 -1
- package/scripts/loop/oris-loop-templates.mjs +3 -3
- package/scripts/oris-skills.mjs +1 -1
- package/scripts/tests/run-all-tests.mjs +1 -0
- package/scripts/tests/test-agent-adapters.mjs +52 -1
- package/scripts/tests/test-oris-flow-scan.mjs +43 -1
- package/scripts/tests/test-routing-lifecycle.mjs +28 -15
- package/scripts/tests/test-skill-style.mjs +64 -0
- package/skills/oris-flow/SKILL.md +48 -33
- package/skills/{oris-flow-architecture/SKILL.md → oris-flow/references/architecture.md} +37 -24
- package/skills/{oris-flow-change/SKILL.md → oris-flow/references/change.md} +21 -16
- package/skills/oris-flow/references/criteria.md +60 -0
- package/skills/oris-flow/references/discover.md +66 -0
- package/skills/{oris-flow-docs/SKILL.md → oris-flow/references/docs.md} +15 -13
- package/skills/oris-flow/references/fix.md +49 -0
- package/skills/{oris-help/SKILL.md → oris-flow/references/help.md} +18 -14
- package/skills/oris-flow/references/implement.md +48 -0
- package/skills/oris-flow/references/loop-craft.md +59 -0
- package/skills/oris-flow/references/loop-improve.md +32 -0
- package/skills/oris-flow/references/loop-run.md +47 -0
- package/skills/oris-flow/references/loop.md +56 -0
- package/skills/{oris-flow-new/SKILL.md → oris-flow/references/new.md} +25 -19
- package/skills/oris-flow/references/plan.md +58 -0
- package/skills/oris-flow/references/setup.md +100 -0
- package/skills/{oris-flow-verify/SKILL.md → oris-flow/references/verify.md} +24 -17
- package/skills/{oris-loop → oris-flow}/templates/debriefer.md +1 -0
- package/skills/{oris-loop → oris-flow}/templates/executor.md +5 -0
- package/skills/{oris-loop → oris-flow}/templates/verifier.md +2 -1
- package/skills/oris-flow-criteria/SKILL.md +0 -55
- package/skills/oris-flow-discover/SKILL.md +0 -63
- package/skills/oris-flow-fix/SKILL.md +0 -42
- package/skills/oris-flow-implement/SKILL.md +0 -43
- package/skills/oris-flow-merge/SKILL.md +0 -45
- package/skills/oris-flow-plan/SKILL.md +0 -55
- package/skills/oris-flow-setup/SKILL.md +0 -80
- package/skills/oris-help/agents/openai.yaml +0 -4
- package/skills/oris-loop/SKILL.md +0 -46
- package/skills/oris-loop/agents/openai.yaml +0 -4
- package/skills/oris-loop/references/craft.md +0 -49
- package/skills/oris-loop/references/improve.md +0 -23
- package/skills/oris-loop/references/run.md +0 -32
- /package/skills/{oris-loop → oris-flow}/templates/doctor.md +0 -0
- /package/skills/{oris-loop → oris-flow}/templates/orchestrator.md +0 -0
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* - Cursor skills: ~/.cursor/skills/<name>/
|
|
9
9
|
* - Claude Code skills: ~/.claude/skills/<name>/
|
|
10
10
|
* - Codex prompts: ~/.codex/prompts/<name>.md (pointer to the bundled SKILL.md)
|
|
11
|
+
* - Copilot prompts: VS Code user prompts dir /<name>.prompt.md (same pointer)
|
|
12
|
+
* - Any other agent: ~/.oris/oris-flow.md — a universal pointer any file-reading
|
|
13
|
+
* agent can be told to follow.
|
|
11
14
|
*
|
|
12
15
|
* Agents are auto-detected from existing home directories; override with
|
|
13
|
-
* --agents cursor,claude,codex.
|
|
16
|
+
* --agents cursor,claude,codex,copilot.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import childProcess from "node:child_process";
|
|
@@ -19,12 +22,18 @@ import os from "node:os";
|
|
|
19
22
|
import path from "node:path";
|
|
20
23
|
import process from "node:process";
|
|
21
24
|
import { fileURLToPath } from "node:url";
|
|
22
|
-
import { writeAgentAdapters } from "./generate-agent-adapters.mjs";
|
|
23
25
|
|
|
24
26
|
const markerName = ".oris-skills-managed.json";
|
|
25
27
|
const installManifestName = ".oris-skills-install.json";
|
|
26
28
|
const nodeDownloadUrl = "https://nodejs.org/en/download";
|
|
27
|
-
const KNOWN_AGENTS = ["cursor", "claude", "codex"];
|
|
29
|
+
const KNOWN_AGENTS = ["cursor", "claude", "codex", "copilot"];
|
|
30
|
+
|
|
31
|
+
/** VS Code user dir — where Copilot Chat reads user prompt files (chat.promptFiles). */
|
|
32
|
+
function vscodeUserDir(home) {
|
|
33
|
+
if (process.platform === "win32") return path.join(home, "AppData", "Roaming", "Code", "User");
|
|
34
|
+
if (process.platform === "darwin") return path.join(home, "Library", "Application Support", "Code", "User");
|
|
35
|
+
return path.join(home, ".config", "Code", "User");
|
|
36
|
+
}
|
|
28
37
|
|
|
29
38
|
function fail(message) {
|
|
30
39
|
throw new Error(message);
|
|
@@ -43,6 +52,7 @@ function defaultPaths() {
|
|
|
43
52
|
cursor: path.join(home, ".cursor"),
|
|
44
53
|
claude: path.join(home, ".claude"),
|
|
45
54
|
codex: path.join(home, ".codex"),
|
|
55
|
+
copilot: vscodeUserDir(home),
|
|
46
56
|
},
|
|
47
57
|
};
|
|
48
58
|
}
|
|
@@ -75,6 +85,7 @@ function parseArgs(argv) {
|
|
|
75
85
|
else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.agentRoots.cursor = value();
|
|
76
86
|
else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.agentRoots.claude = value();
|
|
77
87
|
else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.agentRoots.codex = value();
|
|
88
|
+
else if (key === "--copilot-root" || key.startsWith("--copilot-root=")) options.agentRoots.copilot = value();
|
|
78
89
|
else fail(`Unknown argument: ${raw}`);
|
|
79
90
|
}
|
|
80
91
|
for (const agent of options.agents) {
|
|
@@ -155,6 +166,7 @@ function defaultSettings() {
|
|
|
155
166
|
orisFlow: {
|
|
156
167
|
uiLanguage: "en",
|
|
157
168
|
artifactLanguage: "en",
|
|
169
|
+
responseStyle: "standard",
|
|
158
170
|
},
|
|
159
171
|
defaultProfiles: {
|
|
160
172
|
testing: "default",
|
|
@@ -205,6 +217,39 @@ User request: $ARGUMENTS
|
|
|
205
217
|
}
|
|
206
218
|
}
|
|
207
219
|
|
|
220
|
+
/** Copilot Chat user prompt files: /<name> in chat once chat.promptFiles is enabled. */
|
|
221
|
+
function installCopilotPrompts(skills, vscodeUserRoot, bundleRoot) {
|
|
222
|
+
const promptsRoot = path.join(vscodeUserRoot, "prompts");
|
|
223
|
+
fs.mkdirSync(promptsRoot, { recursive: true });
|
|
224
|
+
for (const skill of skills) {
|
|
225
|
+
const bundledSkill = path.join(bundleRoot, skill.relativePath, "SKILL.md").replace(/\\/g, "/");
|
|
226
|
+
const description = skillDescription(skill.sourceDir);
|
|
227
|
+
fs.writeFileSync(path.join(promptsRoot, `${skill.name}.prompt.md`), `---
|
|
228
|
+
description: ${description || "Oris entry point"}
|
|
229
|
+
---
|
|
230
|
+
<!-- ${markerName} -->
|
|
231
|
+
Read the Oris skill file at \`${bundledSkill}\` and follow it exactly.
|
|
232
|
+
Then handle the user's request in this conversation.
|
|
233
|
+
`, "utf8");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Universal pointer for any file-reading agent that has no install target of its own. */
|
|
238
|
+
function installUniversalPointer(skills, bundleRoot) {
|
|
239
|
+
const primary = skills[0];
|
|
240
|
+
const bundledSkill = path.join(bundleRoot, primary.relativePath, "SKILL.md").replace(/\\/g, "/");
|
|
241
|
+
fs.writeFileSync(path.join(path.dirname(bundleRoot), "oris-flow.md"), `<!-- ${markerName} -->
|
|
242
|
+
# Oris — universal entry point
|
|
243
|
+
|
|
244
|
+
Tell any agent that can read files:
|
|
245
|
+
|
|
246
|
+
> Read \`${bundledSkill}\` and follow it exactly.
|
|
247
|
+
|
|
248
|
+
That file routes every Oris flow (setup, discover, criteria, plan, implement, fix,
|
|
249
|
+
verify, change, loop, architecture, docs, help) in the same chat.
|
|
250
|
+
`, "utf8");
|
|
251
|
+
}
|
|
252
|
+
|
|
208
253
|
function main() {
|
|
209
254
|
requireNode();
|
|
210
255
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -225,9 +270,10 @@ function main() {
|
|
|
225
270
|
log(`Would write bundle: ${options.bundleRoot}`);
|
|
226
271
|
for (const agent of agents) {
|
|
227
272
|
if (agent === "codex") log(`Would write Codex prompts: ${path.join(options.agentRoots.codex, "prompts")}`);
|
|
273
|
+
else if (agent === "copilot") log(`Would write Copilot prompts: ${path.join(options.agentRoots.copilot, "prompts")}`);
|
|
228
274
|
else log(`Would write ${agent} skills: ${path.join(options.agentRoots[agent], "skills")}`);
|
|
229
|
-
log(`Would write ${agent} loop agents: ${path.join(options.agentRoots[agent], "agents")}`);
|
|
230
275
|
}
|
|
276
|
+
log(`Would write universal pointer: ${path.join(path.dirname(options.bundleRoot), "oris-flow.md")}`);
|
|
231
277
|
log(`Would create settings if missing: ${options.settingsPath}`);
|
|
232
278
|
return;
|
|
233
279
|
}
|
|
@@ -247,15 +293,15 @@ function main() {
|
|
|
247
293
|
if (agent === "codex") {
|
|
248
294
|
installCodexPrompts(skills, options.agentRoots.codex, options.bundleRoot);
|
|
249
295
|
log(`Installed ${skills.length} Codex prompts`);
|
|
296
|
+
} else if (agent === "copilot") {
|
|
297
|
+
installCopilotPrompts(skills, options.agentRoots.copilot, options.bundleRoot);
|
|
298
|
+
log(`Installed ${skills.length} Copilot prompts (enable VS Code setting chat.promptFiles)`);
|
|
250
299
|
} else {
|
|
251
300
|
installSkillDirs(skills, path.join(options.agentRoots[agent], "skills"), manifest, options.bundleRoot, options);
|
|
252
301
|
log(`Installed ${skills.length} ${agent} skills`);
|
|
253
302
|
}
|
|
254
303
|
}
|
|
255
|
-
|
|
256
|
-
const adapterTargets = Object.fromEntries(agents.map((agent) => [agent, options.agentRoots[agent]]));
|
|
257
|
-
const loopAgents = writeAgentAdapters(adapterTargets);
|
|
258
|
-
log(`Installed ${loopAgents.length} loop agent adapters (${agents.join(", ")})`);
|
|
304
|
+
installUniversalPointer(skills, options.bundleRoot);
|
|
259
305
|
|
|
260
306
|
if (!fs.existsSync(options.settingsPath)) writeJson(options.settingsPath, defaultSettings());
|
|
261
307
|
writeJson(path.join(options.bundleRoot, installManifestName), {
|
|
@@ -28,6 +28,11 @@ function parseArgs(argv) {
|
|
|
28
28
|
cursorRoot: path.join(home, ".cursor"),
|
|
29
29
|
claudeRoot: path.join(home, ".claude"),
|
|
30
30
|
codexRoot: path.join(home, ".codex"),
|
|
31
|
+
copilotRoot: process.platform === "win32"
|
|
32
|
+
? path.join(home, "AppData", "Roaming", "Code", "User")
|
|
33
|
+
: process.platform === "darwin"
|
|
34
|
+
? path.join(home, "Library", "Application Support", "Code", "User")
|
|
35
|
+
: path.join(home, ".config", "Code", "User"),
|
|
31
36
|
};
|
|
32
37
|
for (let i = 0; i < argv.length; i += 1) {
|
|
33
38
|
const raw = argv[i];
|
|
@@ -44,6 +49,7 @@ function parseArgs(argv) {
|
|
|
44
49
|
else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.cursorRoot = value();
|
|
45
50
|
else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.claudeRoot = value();
|
|
46
51
|
else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.codexRoot = value();
|
|
52
|
+
else if (key === "--copilot-root" || key.startsWith("--copilot-root=")) options.copilotRoot = value();
|
|
47
53
|
else fail(`Unknown argument: ${raw}`);
|
|
48
54
|
}
|
|
49
55
|
return options;
|
|
@@ -86,6 +92,23 @@ function managedCodexPrompts(codexRoot) {
|
|
|
86
92
|
.map((entry) => path.join(promptsRoot, entry.name));
|
|
87
93
|
}
|
|
88
94
|
|
|
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
|
+
function managedCopilotPrompts(copilotRoot) {
|
|
105
|
+
const promptsRoot = path.join(copilotRoot, "prompts");
|
|
106
|
+
if (!fs.existsSync(promptsRoot)) return [];
|
|
107
|
+
return fs.readdirSync(promptsRoot, { withFileTypes: true })
|
|
108
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().startsWith("oris-") && entry.name.endsWith(".prompt.md"))
|
|
109
|
+
.map((entry) => path.join(promptsRoot, entry.name));
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
function legacyOrisPaths(options) {
|
|
90
113
|
return [
|
|
91
114
|
path.join(options.cursorRoot, "oris-skills"),
|
|
@@ -104,6 +127,11 @@ function plannedRemovals(options) {
|
|
|
104
127
|
...managedSkillDirs(path.join(options.cursorRoot, "skills")),
|
|
105
128
|
...managedSkillDirs(path.join(options.claudeRoot, "skills")),
|
|
106
129
|
...managedCodexPrompts(options.codexRoot),
|
|
130
|
+
...managedCopilotPrompts(options.copilotRoot),
|
|
131
|
+
...managedAgentAdapters(options.cursorRoot, ".md"),
|
|
132
|
+
...managedAgentAdapters(options.claudeRoot, ".md"),
|
|
133
|
+
...managedAgentAdapters(options.codexRoot, ".toml"),
|
|
134
|
+
path.join(path.dirname(options.bundleRoot), "oris-flow.md"),
|
|
107
135
|
...legacyOrisPaths(options),
|
|
108
136
|
]);
|
|
109
137
|
return [...paths].map((target) => path.resolve(target)).filter(safeOrisPath).sort();
|
|
@@ -125,7 +153,7 @@ function removeOrisMcpServers(userMcpPath, dryRun) {
|
|
|
125
153
|
|
|
126
154
|
export function uninstallUserSkills(rawArgs = []) {
|
|
127
155
|
const options = parseArgs(rawArgs);
|
|
128
|
-
for (const target of [options.bundleRoot, options.settingsPath, options.cursorRoot, options.claudeRoot, options.codexRoot]) {
|
|
156
|
+
for (const target of [options.bundleRoot, options.settingsPath, options.cursorRoot, options.claudeRoot, options.codexRoot, options.copilotRoot]) {
|
|
129
157
|
assertInsideHome(target);
|
|
130
158
|
}
|
|
131
159
|
|
|
@@ -40,7 +40,7 @@ function parseArgs(argv) {
|
|
|
40
40
|
progress: "",
|
|
41
41
|
repositoryRoot: "",
|
|
42
42
|
runner: "chat",
|
|
43
|
-
maxIterations:
|
|
43
|
+
maxIterations: 10,
|
|
44
44
|
maxMinutes: 240,
|
|
45
45
|
profile: "",
|
|
46
46
|
settingsPath: ORIS_SKILLS_SETTINGS,
|
|
@@ -285,7 +285,7 @@ if (options.action === "start") {
|
|
|
285
285
|
}
|
|
286
286
|
const prompts = assessPromptFiles(path.dirname(loopPath), activeRoles(loopDocument.data));
|
|
287
287
|
if (prompts.missing.length > 0) {
|
|
288
|
-
fail(`Loop prompt files are missing or empty: ${prompts.missing.join(", ")}. Craft or copy them from skills/oris-
|
|
288
|
+
fail(`Loop prompt files are missing or empty: ${prompts.missing.join(", ")}. Craft or copy them from skills/oris-flow/templates/.`);
|
|
289
289
|
}
|
|
290
290
|
if (fs.existsSync(statePath)) {
|
|
291
291
|
const existing = normalizeChatState(JSON.parse(fs.readFileSync(statePath, "utf8")));
|
|
@@ -108,7 +108,7 @@ console.log(followup);
|
|
|
108
108
|
console.log("=== End of injected message ===");
|
|
109
109
|
console.log("");
|
|
110
110
|
if (prompts.missing.length > 0) {
|
|
111
|
-
console.log("NOT runnable yet — create the missing prompt files first (templates: skills/oris-
|
|
111
|
+
console.log("NOT runnable yet — create the missing prompt files first (templates: skills/oris-flow/templates/).");
|
|
112
112
|
process.exit(1);
|
|
113
113
|
}
|
|
114
114
|
console.log(`Runnable. Start it with: npx oris-skills loop chat --action start --loop ${metadata.name}`);
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Prompt templates live in skills/oris-
|
|
6
|
+
* Prompt templates live in skills/oris-flow/templates/*.md so users and
|
|
7
7
|
* agents can read and copy them. Scripts fall back to an embedded
|
|
8
8
|
* orchestrator prompt so the stop hook can never fail on a missing file.
|
|
9
9
|
*/
|
|
@@ -47,8 +47,8 @@ IF prompt files are missing or subagents cannot be spawned: \`set-state --state
|
|
|
47
47
|
export function resolveTemplatesDir(fromImportMetaUrl) {
|
|
48
48
|
const scriptsLoop = path.dirname(fileURLToPath(fromImportMetaUrl));
|
|
49
49
|
const candidates = [
|
|
50
|
-
path.resolve(scriptsLoop, "..", "..", "skills", "oris-
|
|
51
|
-
path.resolve(scriptsLoop, "..", "skills", "oris-
|
|
50
|
+
path.resolve(scriptsLoop, "..", "..", "skills", "oris-flow", "templates"),
|
|
51
|
+
path.resolve(scriptsLoop, "..", "skills", "oris-flow", "templates"),
|
|
52
52
|
];
|
|
53
53
|
for (const candidate of candidates) {
|
|
54
54
|
if (fs.existsSync(path.join(candidate, "orchestrator.md"))) return candidate;
|
package/scripts/oris-skills.mjs
CHANGED
|
@@ -32,7 +32,7 @@ function printHelp() {
|
|
|
32
32
|
console.log(`Oris Skills
|
|
33
33
|
|
|
34
34
|
Install for your agents (Cursor, Claude Code, Codex):
|
|
35
|
-
oris-skills install [--agents cursor,claude,codex] [--dry-run]
|
|
35
|
+
oris-skills install [--agents cursor,claude,codex,copilot] [--dry-run]
|
|
36
36
|
oris-skills uninstall [--dry-run]
|
|
37
37
|
oris-skills reinstall [--dry-run]
|
|
38
38
|
|
|
@@ -14,6 +14,7 @@ const nodeTestFiles = [
|
|
|
14
14
|
"scripts/tests/test-oris-1-0-cleanliness.mjs",
|
|
15
15
|
"scripts/tests/test-oris-flow-scan.mjs",
|
|
16
16
|
"scripts/tests/test-routing-lifecycle.mjs",
|
|
17
|
+
"scripts/tests/test-skill-style.mjs",
|
|
17
18
|
"scripts/tests/test-oris-loop-stop.mjs",
|
|
18
19
|
"scripts/tests/test-oris-loop-smoke.mjs",
|
|
19
20
|
"scripts/tests/test-oris-loop-bootstrap.mjs",
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import assert from "node:assert/strict";
|
|
6
|
+
import process from "node:process";
|
|
5
7
|
import test from "node:test";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
6
9
|
import { generateAgentAdapters } from "../install/generate-agent-adapters.mjs";
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
12
|
+
|
|
13
|
+
function runCli(args, homeDir) {
|
|
14
|
+
return childProcess.spawnSync(process.execPath, ["scripts/oris-skills.mjs", ...args], {
|
|
15
|
+
cwd: root,
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir },
|
|
18
|
+
shell: false,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("generates repo-local Cursor, Claude, and Codex agent adapters", () => {
|
|
9
23
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "oris-agent-adapters-"));
|
|
10
24
|
try {
|
|
11
25
|
const generated = generateAgentAdapters(temp);
|
|
@@ -17,3 +31,40 @@ test("generates Cursor, Claude, and Codex agent adapters", () => {
|
|
|
17
31
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
18
32
|
}
|
|
19
33
|
});
|
|
34
|
+
|
|
35
|
+
test("install never plans loop agent adapters in user agent homes", () => {
|
|
36
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-agent-install-"));
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.join(homeDir, ".claude"), { recursive: true });
|
|
39
|
+
fs.mkdirSync(path.join(homeDir, ".cursor"), { recursive: true });
|
|
40
|
+
const result = runCli(["install", "--dry-run"], homeDir);
|
|
41
|
+
assert.equal(result.status, 0, result.stderr);
|
|
42
|
+
assert.doesNotMatch(result.stdout, /loop agents/i);
|
|
43
|
+
assert.doesNotMatch(result.stdout, /agent adapters/i);
|
|
44
|
+
} finally {
|
|
45
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("uninstall removes legacy loop agent adapters but keeps user agents", () => {
|
|
50
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-agent-uninstall-"));
|
|
51
|
+
try {
|
|
52
|
+
const claudeAgents = path.join(homeDir, ".claude", "agents");
|
|
53
|
+
const cursorAgents = path.join(homeDir, ".cursor", "agents");
|
|
54
|
+
const codexAgents = path.join(homeDir, ".codex", "agents");
|
|
55
|
+
for (const dir of [claudeAgents, cursorAgents, codexAgents]) fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
fs.writeFileSync(path.join(claudeAgents, "oris-loop-executor.md"), "legacy adapter\n", "utf8");
|
|
57
|
+
fs.writeFileSync(path.join(claudeAgents, "my-agent.md"), "user agent\n", "utf8");
|
|
58
|
+
fs.writeFileSync(path.join(cursorAgents, "oris-loop-verifier.md"), "legacy adapter\n", "utf8");
|
|
59
|
+
fs.writeFileSync(path.join(codexAgents, "oris-loop-doctor.toml"), "legacy adapter\n", "utf8");
|
|
60
|
+
|
|
61
|
+
const result = runCli(["uninstall"], homeDir);
|
|
62
|
+
assert.equal(result.status, 0, result.stderr);
|
|
63
|
+
assert.equal(fs.existsSync(path.join(claudeAgents, "oris-loop-executor.md")), false);
|
|
64
|
+
assert.equal(fs.existsSync(path.join(cursorAgents, "oris-loop-verifier.md")), false);
|
|
65
|
+
assert.equal(fs.existsSync(path.join(codexAgents, "oris-loop-doctor.toml")), false);
|
|
66
|
+
assert.equal(fs.existsSync(path.join(claudeAgents, "my-agent.md")), true, "non-Oris agents must survive");
|
|
67
|
+
} finally {
|
|
68
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -3,7 +3,8 @@ 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
|
|
6
|
+
import childProcess from "node:child_process";
|
|
7
|
+
import { checkFreshness, scanRepository, writeScanResult } from "../flow/oris-flow-scan.mjs";
|
|
7
8
|
import { ORIS_FLOW_MANIFEST } from "../flow/oris-flow-layout.mjs";
|
|
8
9
|
|
|
9
10
|
function write(filePath, content) {
|
|
@@ -113,3 +114,44 @@ test("confirmed section without fresh evidence is marked stale, not deleted", ()
|
|
|
113
114
|
}
|
|
114
115
|
});
|
|
115
116
|
|
|
117
|
+
|
|
118
|
+
test("scan anchors the map to HEAD; --check answers freshness from the SHA + changed files", () => {
|
|
119
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
120
|
+
const git = (...args) => childProcess.execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
121
|
+
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
|
+
const scan = scanRepository(root);
|
|
150
|
+
assert.equal(scan.manifest.headSha, null);
|
|
151
|
+
writeScanResult(scan);
|
|
152
|
+
const check = checkFreshness(root);
|
|
153
|
+
assert.equal(check.fresh, false, "no anchor → refresh advised, not an error");
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
@@ -28,7 +28,7 @@ function runCli(args, homeDir) {
|
|
|
28
28
|
|
|
29
29
|
test("plugin manifest skills all exist and match front-matter names", () => {
|
|
30
30
|
const manifest = JSON.parse(read(".cursor-plugin/plugin.json"));
|
|
31
|
-
assert.
|
|
31
|
+
assert.equal(manifest.skills.length, 1, "one user-facing skill");
|
|
32
32
|
for (const entry of manifest.skills) {
|
|
33
33
|
const relative = entry.replace(/^[.\\/]+/, "");
|
|
34
34
|
const skillFile = path.join(relative, "SKILL.md");
|
|
@@ -40,8 +40,8 @@ test("plugin manifest skills all exist and match front-matter names", () => {
|
|
|
40
40
|
|
|
41
41
|
test("oris-flow route table destinations exist and routing stays in-chat", () => {
|
|
42
42
|
const text = read("skills/oris-flow/SKILL.md");
|
|
43
|
-
const destinations = [...text.matchAll(/`(skills\/[a-z-]
|
|
44
|
-
assert.ok(destinations.length >=
|
|
43
|
+
const destinations = [...text.matchAll(/`(skills\/oris-flow\/references\/[a-z-]+\.md)`/g)].map((match) => match[1]);
|
|
44
|
+
assert.ok(destinations.length >= 13, "route table must list at least 13 destinations");
|
|
45
45
|
for (const destination of destinations) assert.equal(exists(destination), true, destination);
|
|
46
46
|
assert.deepEqual(destinations, [...new Set(destinations)], "route destinations must be unique");
|
|
47
47
|
assert.match(text, /NEVER tell the user to type another command/i);
|
|
@@ -73,10 +73,10 @@ test("every skill points at conventions instead of repeating shared rules", () =
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
test("loop skill covers craft, demo, run, audit, improve and editable prompt files", () => {
|
|
76
|
-
const skill = read("skills/oris-loop
|
|
77
|
-
assert.match(skill, /references\/craft\.md/);
|
|
78
|
-
assert.match(skill, /references\/run\.md/);
|
|
79
|
-
assert.match(skill, /references\/improve\.md/);
|
|
76
|
+
const skill = read("skills/oris-flow/references/loop.md");
|
|
77
|
+
assert.match(skill, /references\/loop-craft\.md/);
|
|
78
|
+
assert.match(skill, /references\/loop-run\.md/);
|
|
79
|
+
assert.match(skill, /references\/loop-improve\.md/);
|
|
80
80
|
assert.match(skill, /oris-skills loop demo/);
|
|
81
81
|
assert.match(skill, /oris-skills loop dry-run/);
|
|
82
82
|
assert.match(skill, /oris-skills loop list --json/);
|
|
@@ -84,10 +84,10 @@ test("loop skill covers craft, demo, run, audit, improve and editable prompt fil
|
|
|
84
84
|
assert.match(skill, /improve\.mode: auto/);
|
|
85
85
|
assert.match(skill, /Cursor|Claude Code|Codex/);
|
|
86
86
|
for (const reference of ["craft", "run", "improve"]) {
|
|
87
|
-
assert.equal(exists(`skills/oris-
|
|
87
|
+
assert.equal(exists(`skills/oris-flow/references/loop-${reference}.md`), true, reference);
|
|
88
88
|
}
|
|
89
89
|
for (const template of ["orchestrator", "executor", "verifier", "doctor", "debriefer"]) {
|
|
90
|
-
assert.equal(exists(`skills/oris-
|
|
90
|
+
assert.equal(exists(`skills/oris-flow/templates/${template}.md`), true, template);
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -99,26 +99,26 @@ test("loop contract stays subagent-first with per-role editable prompts", () =>
|
|
|
99
99
|
assert.match(contract, /improve/);
|
|
100
100
|
assert.match(contract, /inherit/);
|
|
101
101
|
assert.match(contract, /NEVER success/i);
|
|
102
|
-
const run = read("skills/oris-
|
|
102
|
+
const run = read("skills/oris-flow/references/loop-run.md");
|
|
103
103
|
assert.match(run, /oris-skills loop chat --action start --loop <slug>/);
|
|
104
104
|
assert.match(run, /NEVER do executor\/verifier\/doctor work in the main chat/i);
|
|
105
105
|
assert.match(run, /set-state blocked/);
|
|
106
106
|
assert.match(run, /oris-skills loop run --loop <slug> --agent codex\|claude/);
|
|
107
|
-
const craft = read("skills/oris-
|
|
108
|
-
assert.match(craft, /skills\/oris-
|
|
107
|
+
const craft = read("skills/oris-flow/references/loop-craft.md");
|
|
108
|
+
assert.match(craft, /skills\/oris-flow\/templates\//);
|
|
109
109
|
assert.match(craft, /write the loop \| edit verifier \| edit executor \| edit doctor/);
|
|
110
110
|
assert.match(contract, /ORIS LOOP ACTIVE/, "the contract defines the executor gate literal");
|
|
111
|
-
for (const text of [contract, run, craft, read("skills/oris-loop
|
|
111
|
+
for (const text of [contract, run, craft, read("skills/oris-flow/references/loop.md")]) {
|
|
112
112
|
assert.doesNotMatch(text, /node scripts\/(?:flow|loop)\//);
|
|
113
113
|
assert.doesNotMatch(text, /composer-2\.5/);
|
|
114
114
|
}
|
|
115
|
-
for (const text of [run, craft, read("skills/oris-loop
|
|
115
|
+
for (const text of [run, craft, read("skills/oris-flow/references/loop.md")]) {
|
|
116
116
|
assert.doesNotMatch(text, /ORIS LOOP ACTIVE/, "the gate literal lives in the contract and executor prompts only");
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
test("setup and repo-map keep the installed CLI as the command surface", () => {
|
|
121
|
-
const setup = read("skills/oris-flow
|
|
121
|
+
const setup = read("skills/oris-flow/references/setup.md");
|
|
122
122
|
assert.match(setup, /oris-skills flow scan --repository-root <repo> --json/);
|
|
123
123
|
assert.match(setup, /oris-skills loop bootstrap --repository-root <repo>/);
|
|
124
124
|
assert.match(setup, /oris-skills flow clean-runtime --repository-root <repo> --dry-run/);
|
|
@@ -181,3 +181,16 @@ test("CLI exposes install, loop learnability, and flow helper commands", () => {
|
|
|
181
181
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
182
182
|
}
|
|
183
183
|
});
|
|
184
|
+
|
|
185
|
+
test("copilot install is planned alongside the universal pointer", () => {
|
|
186
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-copilot-"));
|
|
187
|
+
try {
|
|
188
|
+
const result = runCli(["install", "--dry-run", "--agents", "copilot"], homeDir);
|
|
189
|
+
assert.equal(result.status, 0, result.stderr);
|
|
190
|
+
assert.match(result.stdout, /Copilot prompts/);
|
|
191
|
+
assert.match(result.stdout, /universal pointer/);
|
|
192
|
+
assert.match(result.stdout, /oris-flow\.md/);
|
|
193
|
+
} finally {
|
|
194
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
// The Oris skill style, enforced: front matter that routes, leading-word moves,
|
|
8
|
+
// an explicit Never block, a Done when checklist, and a hard line budget.
|
|
9
|
+
// One user-facing skill exists; every route is one of its references.
|
|
10
|
+
|
|
11
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
12
|
+
const skillsRoot = path.join(repoRoot, "skills");
|
|
13
|
+
const MAX_LINES = 110;
|
|
14
|
+
|
|
15
|
+
const skillDirs = fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
16
|
+
.filter((entry) => entry.isDirectory())
|
|
17
|
+
.map((entry) => entry.name);
|
|
18
|
+
|
|
19
|
+
function resolveHandles(text, label, localRoot) {
|
|
20
|
+
for (const match of text.matchAll(/`(?:~\/\.oris\/oris-skills\/)?((?:skills|references)\/[A-Za-z0-9/._-]+\.md)`/g)) {
|
|
21
|
+
const target = path.join(repoRoot, match[1]);
|
|
22
|
+
const local = path.join(localRoot, match[1]);
|
|
23
|
+
assert.ok(fs.existsSync(target) || fs.existsSync(local), `${label}: routing handle resolves: ${match[1]}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("exactly one user-facing skill: oris-flow", () => {
|
|
28
|
+
assert.deepEqual(skillDirs, ["oris-flow"], "users see one skill; routes are its references");
|
|
29
|
+
assert.ok(fs.existsSync(path.join(skillsRoot, "oris-flow", "SKILL.md")));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("skill style: oris-flow", () => {
|
|
33
|
+
const text = fs.readFileSync(path.join(skillsRoot, "oris-flow", "SKILL.md"), "utf8");
|
|
34
|
+
const front = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
35
|
+
assert.ok(front, "has front matter");
|
|
36
|
+
assert.match(front[1], /^name: oris-flow$/m, "front matter name matches the folder");
|
|
37
|
+
const description = front[1].match(/^description: (.+)$/m)?.[1];
|
|
38
|
+
assert.ok(description, "front matter has description");
|
|
39
|
+
assert.match(description, /Use for/, "description names its triggers");
|
|
40
|
+
assert.doesNotMatch(front[1], /disable-model-invocation/, "the single entry point must stay model-invocable");
|
|
41
|
+
assert.ok(text.includes("## Never"), "carries an explicit Never block");
|
|
42
|
+
assert.ok(text.includes("## Done when"), "ends with a Done when checklist");
|
|
43
|
+
const lines = text.split(/\r?\n/).length;
|
|
44
|
+
assert.ok(lines <= MAX_LINES, `SKILL.md is ${lines} lines (max ${MAX_LINES}) — move detail to references/`);
|
|
45
|
+
resolveHandles(text, "SKILL.md", path.join(skillsRoot, "oris-flow"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("every route reference carries the style and resolves its handles", () => {
|
|
49
|
+
const skillText = fs.readFileSync(path.join(skillsRoot, "oris-flow", "SKILL.md"), "utf8");
|
|
50
|
+
const routes = [...skillText.matchAll(/`skills\/oris-flow\/references\/([a-z-]+\.md)`/g)].map((match) => match[1]);
|
|
51
|
+
assert.ok(routes.length >= 13, `router table lists all routes (found ${routes.length})`);
|
|
52
|
+
assert.deepEqual(routes, [...new Set(routes)], "route destinations are unique");
|
|
53
|
+
const referencesDir = path.join(skillsRoot, "oris-flow", "references");
|
|
54
|
+
for (const file of routes) {
|
|
55
|
+
const text = fs.readFileSync(path.join(referencesDir, file), "utf8");
|
|
56
|
+
assert.doesNotMatch(text, /^---\r?\n/, `${file}: references carry no front matter`);
|
|
57
|
+
assert.ok(text.includes("## Never"), `${file}: carries an explicit Never block`);
|
|
58
|
+
assert.ok(text.includes("## Done when"), `${file}: ends with a Done when checklist`);
|
|
59
|
+
}
|
|
60
|
+
for (const file of fs.readdirSync(referencesDir)) {
|
|
61
|
+
const text = fs.readFileSync(path.join(referencesDir, file), "utf8");
|
|
62
|
+
resolveHandles(text, file, path.join(skillsRoot, "oris-flow"));
|
|
63
|
+
}
|
|
64
|
+
});
|