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.
Files changed (66) hide show
  1. package/.cursor-plugin/plugin.json +2 -16
  2. package/CHANGELOG.md +15 -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/distribution.md +1 -1
  9. package/docs/maintainer-guide.md +3 -3
  10. package/docs/user-guide.md +5 -4
  11. package/package.json +1 -1
  12. package/references/clean-code-checklist.md +1 -1
  13. package/references/conventions.md +28 -2
  14. package/references/loop-contract.md +1 -1
  15. package/references/repo-map.schema.json +4 -0
  16. package/references/settings.md +2 -1
  17. package/references/settings.schema.json +5 -0
  18. package/scripts/flow/oris-flow-scan.mjs +70 -0
  19. package/scripts/install/generate-agent-adapters.mjs +5 -15
  20. package/scripts/install/install-user-skills.mjs +54 -8
  21. package/scripts/install/uninstall-user-skills.mjs +29 -1
  22. package/scripts/loop/oris-loop-chat.mjs +2 -2
  23. package/scripts/loop/oris-loop-dry-run.mjs +1 -1
  24. package/scripts/loop/oris-loop-fixtures.mjs +1 -1
  25. package/scripts/loop/oris-loop-templates.mjs +3 -3
  26. package/scripts/oris-skills.mjs +1 -1
  27. package/scripts/tests/run-all-tests.mjs +1 -0
  28. package/scripts/tests/test-agent-adapters.mjs +52 -1
  29. package/scripts/tests/test-oris-flow-scan.mjs +43 -1
  30. package/scripts/tests/test-routing-lifecycle.mjs +28 -15
  31. package/scripts/tests/test-skill-style.mjs +64 -0
  32. package/skills/oris-flow/SKILL.md +48 -33
  33. package/skills/{oris-flow-architecture/SKILL.md → oris-flow/references/architecture.md} +37 -24
  34. package/skills/{oris-flow-change/SKILL.md → oris-flow/references/change.md} +21 -16
  35. package/skills/oris-flow/references/criteria.md +60 -0
  36. package/skills/oris-flow/references/discover.md +66 -0
  37. package/skills/{oris-flow-docs/SKILL.md → oris-flow/references/docs.md} +15 -13
  38. package/skills/oris-flow/references/fix.md +49 -0
  39. package/skills/{oris-help/SKILL.md → oris-flow/references/help.md} +18 -14
  40. package/skills/oris-flow/references/implement.md +48 -0
  41. package/skills/oris-flow/references/loop-craft.md +59 -0
  42. package/skills/oris-flow/references/loop-improve.md +32 -0
  43. package/skills/oris-flow/references/loop-run.md +47 -0
  44. package/skills/oris-flow/references/loop.md +56 -0
  45. package/skills/{oris-flow-new/SKILL.md → oris-flow/references/new.md} +25 -19
  46. package/skills/oris-flow/references/plan.md +58 -0
  47. package/skills/oris-flow/references/setup.md +100 -0
  48. package/skills/{oris-flow-verify/SKILL.md → oris-flow/references/verify.md} +24 -17
  49. package/skills/{oris-loop → oris-flow}/templates/debriefer.md +1 -0
  50. package/skills/{oris-loop → oris-flow}/templates/executor.md +5 -0
  51. package/skills/{oris-loop → oris-flow}/templates/verifier.md +2 -1
  52. package/skills/oris-flow-criteria/SKILL.md +0 -55
  53. package/skills/oris-flow-discover/SKILL.md +0 -63
  54. package/skills/oris-flow-fix/SKILL.md +0 -42
  55. package/skills/oris-flow-implement/SKILL.md +0 -43
  56. package/skills/oris-flow-merge/SKILL.md +0 -45
  57. package/skills/oris-flow-plan/SKILL.md +0 -55
  58. package/skills/oris-flow-setup/SKILL.md +0 -80
  59. package/skills/oris-help/agents/openai.yaml +0 -4
  60. package/skills/oris-loop/SKILL.md +0 -46
  61. package/skills/oris-loop/agents/openai.yaml +0 -4
  62. package/skills/oris-loop/references/craft.md +0 -49
  63. package/skills/oris-loop/references/improve.md +0 -23
  64. package/skills/oris-loop/references/run.md +0 -32
  65. /package/skills/{oris-loop → oris-flow}/templates/doctor.md +0 -0
  66. /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: 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",
@@ -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
- test("generates Cursor, Claude, and Codex agent adapters", () => {
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 { 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
+ });