oris-skills 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +2 -16
- package/CHANGELOG.md +11 -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/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/install-user-skills.mjs +56 -4
- package/scripts/install/uninstall-user-skills.mjs +17 -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-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";
|
|
@@ -24,7 +27,14 @@ import { writeAgentAdapters } from "./generate-agent-adapters.mjs";
|
|
|
24
27
|
const markerName = ".oris-skills-managed.json";
|
|
25
28
|
const installManifestName = ".oris-skills-install.json";
|
|
26
29
|
const nodeDownloadUrl = "https://nodejs.org/en/download";
|
|
27
|
-
const KNOWN_AGENTS = ["cursor", "claude", "codex"];
|
|
30
|
+
const KNOWN_AGENTS = ["cursor", "claude", "codex", "copilot"];
|
|
31
|
+
|
|
32
|
+
/** VS Code user dir — where Copilot Chat reads user prompt files (chat.promptFiles). */
|
|
33
|
+
function vscodeUserDir(home) {
|
|
34
|
+
if (process.platform === "win32") return path.join(home, "AppData", "Roaming", "Code", "User");
|
|
35
|
+
if (process.platform === "darwin") return path.join(home, "Library", "Application Support", "Code", "User");
|
|
36
|
+
return path.join(home, ".config", "Code", "User");
|
|
37
|
+
}
|
|
28
38
|
|
|
29
39
|
function fail(message) {
|
|
30
40
|
throw new Error(message);
|
|
@@ -43,6 +53,7 @@ function defaultPaths() {
|
|
|
43
53
|
cursor: path.join(home, ".cursor"),
|
|
44
54
|
claude: path.join(home, ".claude"),
|
|
45
55
|
codex: path.join(home, ".codex"),
|
|
56
|
+
copilot: vscodeUserDir(home),
|
|
46
57
|
},
|
|
47
58
|
};
|
|
48
59
|
}
|
|
@@ -75,6 +86,7 @@ function parseArgs(argv) {
|
|
|
75
86
|
else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.agentRoots.cursor = value();
|
|
76
87
|
else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.agentRoots.claude = value();
|
|
77
88
|
else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.agentRoots.codex = value();
|
|
89
|
+
else if (key === "--copilot-root" || key.startsWith("--copilot-root=")) options.agentRoots.copilot = value();
|
|
78
90
|
else fail(`Unknown argument: ${raw}`);
|
|
79
91
|
}
|
|
80
92
|
for (const agent of options.agents) {
|
|
@@ -155,6 +167,7 @@ function defaultSettings() {
|
|
|
155
167
|
orisFlow: {
|
|
156
168
|
uiLanguage: "en",
|
|
157
169
|
artifactLanguage: "en",
|
|
170
|
+
responseStyle: "standard",
|
|
158
171
|
},
|
|
159
172
|
defaultProfiles: {
|
|
160
173
|
testing: "default",
|
|
@@ -205,6 +218,39 @@ User request: $ARGUMENTS
|
|
|
205
218
|
}
|
|
206
219
|
}
|
|
207
220
|
|
|
221
|
+
/** Copilot Chat user prompt files: /<name> in chat once chat.promptFiles is enabled. */
|
|
222
|
+
function installCopilotPrompts(skills, vscodeUserRoot, bundleRoot) {
|
|
223
|
+
const promptsRoot = path.join(vscodeUserRoot, "prompts");
|
|
224
|
+
fs.mkdirSync(promptsRoot, { recursive: true });
|
|
225
|
+
for (const skill of skills) {
|
|
226
|
+
const bundledSkill = path.join(bundleRoot, skill.relativePath, "SKILL.md").replace(/\\/g, "/");
|
|
227
|
+
const description = skillDescription(skill.sourceDir);
|
|
228
|
+
fs.writeFileSync(path.join(promptsRoot, `${skill.name}.prompt.md`), `---
|
|
229
|
+
description: ${description || "Oris entry point"}
|
|
230
|
+
---
|
|
231
|
+
<!-- ${markerName} -->
|
|
232
|
+
Read the Oris skill file at \`${bundledSkill}\` and follow it exactly.
|
|
233
|
+
Then handle the user's request in this conversation.
|
|
234
|
+
`, "utf8");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Universal pointer for any file-reading agent that has no install target of its own. */
|
|
239
|
+
function installUniversalPointer(skills, bundleRoot) {
|
|
240
|
+
const primary = skills[0];
|
|
241
|
+
const bundledSkill = path.join(bundleRoot, primary.relativePath, "SKILL.md").replace(/\\/g, "/");
|
|
242
|
+
fs.writeFileSync(path.join(path.dirname(bundleRoot), "oris-flow.md"), `<!-- ${markerName} -->
|
|
243
|
+
# Oris — universal entry point
|
|
244
|
+
|
|
245
|
+
Tell any agent that can read files:
|
|
246
|
+
|
|
247
|
+
> Read \`${bundledSkill}\` and follow it exactly.
|
|
248
|
+
|
|
249
|
+
That file routes every Oris flow (setup, discover, criteria, plan, implement, fix,
|
|
250
|
+
verify, change, loop, architecture, docs, help) in the same chat.
|
|
251
|
+
`, "utf8");
|
|
252
|
+
}
|
|
253
|
+
|
|
208
254
|
function main() {
|
|
209
255
|
requireNode();
|
|
210
256
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -225,9 +271,11 @@ function main() {
|
|
|
225
271
|
log(`Would write bundle: ${options.bundleRoot}`);
|
|
226
272
|
for (const agent of agents) {
|
|
227
273
|
if (agent === "codex") log(`Would write Codex prompts: ${path.join(options.agentRoots.codex, "prompts")}`);
|
|
274
|
+
else if (agent === "copilot") log(`Would write Copilot prompts: ${path.join(options.agentRoots.copilot, "prompts")}`);
|
|
228
275
|
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")}`);
|
|
276
|
+
if (agent !== "copilot") log(`Would write ${agent} loop agents: ${path.join(options.agentRoots[agent], "agents")}`);
|
|
230
277
|
}
|
|
278
|
+
log(`Would write universal pointer: ${path.join(path.dirname(options.bundleRoot), "oris-flow.md")}`);
|
|
231
279
|
log(`Would create settings if missing: ${options.settingsPath}`);
|
|
232
280
|
return;
|
|
233
281
|
}
|
|
@@ -247,13 +295,17 @@ function main() {
|
|
|
247
295
|
if (agent === "codex") {
|
|
248
296
|
installCodexPrompts(skills, options.agentRoots.codex, options.bundleRoot);
|
|
249
297
|
log(`Installed ${skills.length} Codex prompts`);
|
|
298
|
+
} else if (agent === "copilot") {
|
|
299
|
+
installCopilotPrompts(skills, options.agentRoots.copilot, options.bundleRoot);
|
|
300
|
+
log(`Installed ${skills.length} Copilot prompts (enable VS Code setting chat.promptFiles)`);
|
|
250
301
|
} else {
|
|
251
302
|
installSkillDirs(skills, path.join(options.agentRoots[agent], "skills"), manifest, options.bundleRoot, options);
|
|
252
303
|
log(`Installed ${skills.length} ${agent} skills`);
|
|
253
304
|
}
|
|
254
305
|
}
|
|
306
|
+
installUniversalPointer(skills, options.bundleRoot);
|
|
255
307
|
|
|
256
|
-
const adapterTargets = Object.fromEntries(agents.map((agent) => [agent, options.agentRoots[agent]]));
|
|
308
|
+
const adapterTargets = Object.fromEntries(agents.filter((agent) => agent !== "copilot").map((agent) => [agent, options.agentRoots[agent]]));
|
|
257
309
|
const loopAgents = writeAgentAdapters(adapterTargets);
|
|
258
310
|
log(`Installed ${loopAgents.length} loop agent adapters (${agents.join(", ")})`);
|
|
259
311
|
|
|
@@ -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,14 @@ function managedCodexPrompts(codexRoot) {
|
|
|
86
92
|
.map((entry) => path.join(promptsRoot, entry.name));
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
function managedCopilotPrompts(copilotRoot) {
|
|
96
|
+
const promptsRoot = path.join(copilotRoot, "prompts");
|
|
97
|
+
if (!fs.existsSync(promptsRoot)) return [];
|
|
98
|
+
return fs.readdirSync(promptsRoot, { withFileTypes: true })
|
|
99
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().startsWith("oris-") && entry.name.endsWith(".prompt.md"))
|
|
100
|
+
.map((entry) => path.join(promptsRoot, entry.name));
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
function legacyOrisPaths(options) {
|
|
90
104
|
return [
|
|
91
105
|
path.join(options.cursorRoot, "oris-skills"),
|
|
@@ -104,6 +118,8 @@ function plannedRemovals(options) {
|
|
|
104
118
|
...managedSkillDirs(path.join(options.cursorRoot, "skills")),
|
|
105
119
|
...managedSkillDirs(path.join(options.claudeRoot, "skills")),
|
|
106
120
|
...managedCodexPrompts(options.codexRoot),
|
|
121
|
+
...managedCopilotPrompts(options.copilotRoot),
|
|
122
|
+
path.join(path.dirname(options.bundleRoot), "oris-flow.md"),
|
|
107
123
|
...legacyOrisPaths(options),
|
|
108
124
|
]);
|
|
109
125
|
return [...paths].map((target) => path.resolve(target)).filter(safeOrisPath).sort();
|
|
@@ -125,7 +141,7 @@ function removeOrisMcpServers(userMcpPath, dryRun) {
|
|
|
125
141
|
|
|
126
142
|
export function uninstallUserSkills(rawArgs = []) {
|
|
127
143
|
const options = parseArgs(rawArgs);
|
|
128
|
-
for (const target of [options.bundleRoot, options.settingsPath, options.cursorRoot, options.claudeRoot, options.codexRoot]) {
|
|
144
|
+
for (const target of [options.bundleRoot, options.settingsPath, options.cursorRoot, options.claudeRoot, options.codexRoot, options.copilotRoot]) {
|
|
129
145
|
assertInsideHome(target);
|
|
130
146
|
}
|
|
131
147
|
|
|
@@ -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",
|
|
@@ -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
|
+
});
|
|
@@ -1,43 +1,52 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oris-flow
|
|
3
|
-
description: Oris
|
|
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Oris Flow —
|
|
6
|
+
# Oris Flow — the router
|
|
7
7
|
|
|
8
|
-
The only
|
|
8
|
+
The only skill users see. READ the intent, PICK one route, say one line
|
|
9
|
+
("Entering <route>: <why>"), then READ the route's reference file and APPLY it top to
|
|
10
|
+
bottom in the same chat — its Never and Done when bind the whole route.
|
|
11
|
+
NEVER tell the user to type another command; NEVER narrate the reading
|
|
12
|
+
(`references/conventions.md` `## Silence`).
|
|
9
13
|
|
|
10
|
-
RULES:
|
|
14
|
+
RULES: `references/conventions.md`. This router NEVER writes documents, code, Git, or
|
|
15
|
+
DevOps — the route's own reference owns its gates.
|
|
11
16
|
|
|
12
17
|
## Route
|
|
13
18
|
|
|
14
|
-
Match the desired outcome to ONE route:
|
|
19
|
+
Match the desired outcome to ONE route, then apply its reference:
|
|
15
20
|
|
|
16
|
-
| Route | When the user wants… |
|
|
21
|
+
| Route | When the user wants… | Apply |
|
|
17
22
|
|-------|----------------------|-------|
|
|
18
|
-
| setup | prepare / map / refresh
|
|
19
|
-
| new | bootstrap a
|
|
20
|
-
| discover | understand or define product behavior | `skills/oris-flow
|
|
21
|
-
| criteria | acceptance criteria
|
|
22
|
-
| plan | a technical implementation plan | `skills/oris-flow
|
|
23
|
-
| implement | build from a plan or confirmed scope | `skills/oris-flow
|
|
24
|
-
| fix |
|
|
25
|
-
| verify | check acceptance criteria against the real product, once | `skills/oris-flow
|
|
26
|
-
| change | the spec changed — update analysis, criteria, plan, and history together | `skills/oris-flow
|
|
27
|
-
| loop | repeat work until verified
|
|
28
|
-
| architecture | review architecture, find deepening/refactor opportunities | `skills/oris-flow
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
1.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
| setup | prepare / map / refresh an existing repo for Oris and agents | `skills/oris-flow/references/setup.md` |
|
|
24
|
+
| new | bootstrap a project from scratch, greenfield, empty directory | `skills/oris-flow/references/new.md` |
|
|
25
|
+
| discover | understand or define product behavior, functional analysis | `skills/oris-flow/references/discover.md` |
|
|
26
|
+
| criteria | acceptance criteria, QA checks, testable scenarios | `skills/oris-flow/references/criteria.md` |
|
|
27
|
+
| plan | a technical implementation plan before coding | `skills/oris-flow/references/plan.md` |
|
|
28
|
+
| implement | build from a plan or confirmed scope | `skills/oris-flow/references/implement.md` |
|
|
29
|
+
| fix | broken behavior, a regression, a failed QA check | `skills/oris-flow/references/fix.md` |
|
|
30
|
+
| verify | check acceptance criteria against the real product, once | `skills/oris-flow/references/verify.md` |
|
|
31
|
+
| change | the spec changed — update analysis, criteria, plan, and history together | `skills/oris-flow/references/change.md` |
|
|
32
|
+
| loop | repeat work until verified — fix until green, step through a plan | `skills/oris-flow/references/loop.md` |
|
|
33
|
+
| architecture | review architecture, find deepening/refactor opportunities | `skills/oris-flow/references/architecture.md` |
|
|
34
|
+
| docs | update task docs after a change | `skills/oris-flow/references/docs.md` |
|
|
35
|
+
| help | how Oris works, install, update, troubleshooting | `skills/oris-flow/references/help.md` |
|
|
36
|
+
|
|
37
|
+
## Precedence (the calls that look ambiguous)
|
|
38
|
+
|
|
39
|
+
1. INTENT clear → enter the route NOW. Unclear → ONE menu question (below); NEVER guess
|
|
40
|
+
between two routes and never ask twice.
|
|
41
|
+
2. RETHINK beats build: analyze, question, or reshape existing work →
|
|
42
|
+
route to the interview owner (discover / plan / change), NEVER straight to implement.
|
|
43
|
+
3. CHANGED spec on a feature that already has task docs or code → change, not discover —
|
|
44
|
+
change owns the delta and the history.
|
|
45
|
+
4. BROKEN behavior → fix. NEW planned work → implement. The menu answer "Implement or
|
|
46
|
+
fix" gets exactly one follow-up question to split them.
|
|
47
|
+
5. CHECK once → verify. Repeat-until-verified → loop.
|
|
48
|
+
6. EMPTY directory or from-scratch intent → new. Existing repo without
|
|
49
|
+
`.oris-flow/manifest.json` → recommend setup first; respect the user's choice.
|
|
41
50
|
|
|
42
51
|
## Menu (low confidence)
|
|
43
52
|
|
|
@@ -53,16 +62,22 @@ ASK "What do you want to do?" localized to `uiLanguage`, options:
|
|
|
53
62
|
- Spec changed: update feature docs
|
|
54
63
|
- Loop: repeat until verified
|
|
55
64
|
- Architecture review
|
|
56
|
-
- Resolve merge conflicts
|
|
57
65
|
- Update docs
|
|
58
66
|
- Help
|
|
59
67
|
- Explain the options / Spiega le opzioni
|
|
60
68
|
|
|
61
|
-
IF "Implement or fix" → one follow-up: new planned work (implement) or broken behavior (fix)?
|
|
62
69
|
IF "Explain the options" → explain briefly, re-ask the same question.
|
|
63
70
|
Every other label maps 1:1 to its route in the table above.
|
|
64
71
|
|
|
72
|
+
## Never
|
|
73
|
+
|
|
74
|
+
- Force a route onto a plain question — no flow intent means no menu: just answer.
|
|
75
|
+
- Write anything — the route's reference owns every gate.
|
|
76
|
+
- Hand the user a command or skill name to type — enter the route yourself.
|
|
77
|
+
- Route to implement while the plan itself is in question.
|
|
78
|
+
- Keep interviewing here: one menu question maximum, then enter.
|
|
79
|
+
|
|
65
80
|
## Done when
|
|
66
81
|
|
|
67
|
-
- [ ] One route entered in the same chat, or the menu was asked.
|
|
68
|
-
- [ ] No writes happened before the
|
|
82
|
+
- [ ] One route entered and applied in the same chat, or the menu was asked exactly once.
|
|
83
|
+
- [ ] No writes happened before the route's own gate.
|