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.
Files changed (63) hide show
  1. package/.cursor-plugin/plugin.json +2 -16
  2. package/CHANGELOG.md +11 -1
  3. package/README.md +79 -32
  4. package/agents/oris-loop-debriefer.md +1 -0
  5. package/agents/oris-loop-executor.md +5 -2
  6. package/agents/oris-loop-verifier.md +3 -2
  7. package/docs/architecture.md +7 -6
  8. package/docs/maintainer-guide.md +3 -3
  9. package/docs/user-guide.md +5 -4
  10. package/package.json +1 -1
  11. package/references/clean-code-checklist.md +1 -1
  12. package/references/conventions.md +28 -2
  13. package/references/loop-contract.md +1 -1
  14. package/references/repo-map.schema.json +4 -0
  15. package/references/settings.md +2 -1
  16. package/references/settings.schema.json +5 -0
  17. package/scripts/flow/oris-flow-scan.mjs +70 -0
  18. package/scripts/install/install-user-skills.mjs +56 -4
  19. package/scripts/install/uninstall-user-skills.mjs +17 -1
  20. package/scripts/loop/oris-loop-chat.mjs +2 -2
  21. package/scripts/loop/oris-loop-dry-run.mjs +1 -1
  22. package/scripts/loop/oris-loop-fixtures.mjs +1 -1
  23. package/scripts/loop/oris-loop-templates.mjs +3 -3
  24. package/scripts/oris-skills.mjs +1 -1
  25. package/scripts/tests/run-all-tests.mjs +1 -0
  26. package/scripts/tests/test-oris-flow-scan.mjs +43 -1
  27. package/scripts/tests/test-routing-lifecycle.mjs +28 -15
  28. package/scripts/tests/test-skill-style.mjs +64 -0
  29. package/skills/oris-flow/SKILL.md +48 -33
  30. package/skills/{oris-flow-architecture/SKILL.md → oris-flow/references/architecture.md} +37 -24
  31. package/skills/{oris-flow-change/SKILL.md → oris-flow/references/change.md} +21 -16
  32. package/skills/oris-flow/references/criteria.md +60 -0
  33. package/skills/oris-flow/references/discover.md +66 -0
  34. package/skills/{oris-flow-docs/SKILL.md → oris-flow/references/docs.md} +15 -13
  35. package/skills/oris-flow/references/fix.md +49 -0
  36. package/skills/{oris-help/SKILL.md → oris-flow/references/help.md} +18 -14
  37. package/skills/oris-flow/references/implement.md +48 -0
  38. package/skills/oris-flow/references/loop-craft.md +59 -0
  39. package/skills/oris-flow/references/loop-improve.md +32 -0
  40. package/skills/oris-flow/references/loop-run.md +47 -0
  41. package/skills/oris-flow/references/loop.md +56 -0
  42. package/skills/{oris-flow-new/SKILL.md → oris-flow/references/new.md} +25 -19
  43. package/skills/oris-flow/references/plan.md +58 -0
  44. package/skills/oris-flow/references/setup.md +100 -0
  45. package/skills/{oris-flow-verify/SKILL.md → oris-flow/references/verify.md} +24 -17
  46. package/skills/{oris-loop → oris-flow}/templates/debriefer.md +1 -0
  47. package/skills/{oris-loop → oris-flow}/templates/executor.md +5 -0
  48. package/skills/{oris-loop → oris-flow}/templates/verifier.md +2 -1
  49. package/skills/oris-flow-criteria/SKILL.md +0 -55
  50. package/skills/oris-flow-discover/SKILL.md +0 -63
  51. package/skills/oris-flow-fix/SKILL.md +0 -42
  52. package/skills/oris-flow-implement/SKILL.md +0 -43
  53. package/skills/oris-flow-merge/SKILL.md +0 -45
  54. package/skills/oris-flow-plan/SKILL.md +0 -55
  55. package/skills/oris-flow-setup/SKILL.md +0 -80
  56. package/skills/oris-help/agents/openai.yaml +0 -4
  57. package/skills/oris-loop/SKILL.md +0 -46
  58. package/skills/oris-loop/agents/openai.yaml +0 -4
  59. package/skills/oris-loop/references/craft.md +0 -49
  60. package/skills/oris-loop/references/improve.md +0 -23
  61. package/skills/oris-loop/references/run.md +0 -32
  62. /package/skills/{oris-loop → oris-flow}/templates/doctor.md +0 -0
  63. /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: 25,
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-loop/templates/.`);
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-loop/templates/).");
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}`);
@@ -40,7 +40,7 @@ approved: true
40
40
  phases: [${phases.join(", ")}]
41
41
  roles: [${roles.join(", ")}]
42
42
  limits:
43
- maxIterations: 25
43
+ maxIterations: 10
44
44
  maxNoProgress: 2
45
45
  maxMinutes: 240
46
46
  ${modelLines.join("\n")}
@@ -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-loop/templates/*.md so users and
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-loop", "templates"),
51
- path.resolve(scriptsLoop, "..", "skills", "oris-loop", "templates"),
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;
@@ -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 { scanRepository, writeScanResult } from "../flow/oris-flow-scan.mjs";
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.ok(manifest.skills.length >= 10);
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-]+\/SKILL\.md)`/g)].map((match) => match[1]);
44
- assert.ok(destinations.length >= 9, "route table must list at least 9 destinations");
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/SKILL.md");
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-loop/references/${reference}.md`), true, reference);
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-loop/templates/${template}.md`), true, template);
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-loop/references/run.md");
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-loop/references/craft.md");
108
- assert.match(craft, /skills\/oris-loop\/templates\//);
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/SKILL.md")]) {
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/SKILL.md")]) {
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-setup/SKILL.md");
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 entry point. Use when the user types /oris-flow, picks from its menu, or describes any software-flow goal (analyze, define criteria, plan, implement, fix, loop, docs). Routes to the right Oris skill in the same chat.
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 — Entry Point
6
+ # Oris Flow — the router
7
7
 
8
- The only command users must remember. READ the intent, ROUTE to one skill, ENTER it in the same chat.
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: follow `references/conventions.md`. This router NEVER writes documents, code, Git, or DevOps — the destination skill owns its own gates.
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… | Enter |
21
+ | Route | When the user wants… | Apply |
17
22
  |-------|----------------------|-------|
18
- | setup | prepare / map / refresh the repo for Oris | `skills/oris-flow-setup/SKILL.md` |
19
- | new | bootstrap a new AI-driven project from scratch | `skills/oris-flow-new/SKILL.md` |
20
- | discover | understand or define product behavior | `skills/oris-flow-discover/SKILL.md` |
21
- | criteria | acceptance criteria / QA checks | `skills/oris-flow-criteria/SKILL.md` |
22
- | plan | a technical implementation plan | `skills/oris-flow-plan/SKILL.md` |
23
- | implement | build from a plan or confirmed scope | `skills/oris-flow-implement/SKILL.md` |
24
- | fix | fix a bug or regression | `skills/oris-flow-fix/SKILL.md` |
25
- | verify | check acceptance criteria against the real product, once | `skills/oris-flow-verify/SKILL.md` |
26
- | change | the spec changed — update analysis, criteria, plan, and history together | `skills/oris-flow-change/SKILL.md` |
27
- | loop | repeat work until verified (design, demo, run, tune) | `skills/oris-loop/SKILL.md` |
28
- | architecture | review architecture, find deepening/refactor opportunities | `skills/oris-flow-architecture/SKILL.md` |
29
- | merge | resolve an in-progress merge/rebase conflict | `skills/oris-flow-merge/SKILL.md` |
30
- | docs | update task docs after a change | `skills/oris-flow-docs/SKILL.md` |
31
- | help | how Oris works, install, troubleshooting | `skills/oris-help/SKILL.md` |
32
-
33
- ROUTING:
34
- 1. IF intent is clear → say one line ("Entering <route>: <why>"), then READ and APPLY the destination skill immediately. NEVER tell the user to type another command.
35
- 2. IF intent is unclear ask ONE menu question (below).
36
- 3. IF the user wants to analyze, rethink, or change an existing plan route to the interview owner (discover / plan / change), NEVER straight to implement.
37
- 4. IF requirements changed on a feature that already has task docs or code → change, not discover (change owns the delta + history).
38
- 5. IF `.oris-flow/manifest.json` is missing recommend setup first; respect the user's choice.
39
- IF the target directory is empty or the user starts a project from scratch → new, not setup.
40
- 6. Destination skills marked `disable-model-invocation` may still be entered from here that flag only blocks unsolicited invocation from generic chat.
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 destination skill's own gate.
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.