oris-skills 3.0.2 → 3.0.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "oris-skills",
3
3
  "description": "Oris Skills: one entry point (/oris-flow) and a guided discovery-to-implementation flow. Cross-model: Cursor, Claude Code, Codex.",
4
- "version": "3.0.1",
4
+ "version": "3.0.3",
5
5
  "author": {
6
6
  "name": "Davide Baldassarre"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.3 - 2026-07-14
4
+
5
+ - **Native route questions restored.** Ambiguous `/oris-flow` requests now traverse a
6
+ bounded, outcome-based navigator through Cursor `AskQuestion` or Claude
7
+ `AskUserQuestion`; text fallback is used only when no question tool is available.
8
+ - **Question capability is detected, not inferred from mode.** Cursor Agent Mode can use
9
+ its native question UI when the runtime exposes it.
10
+ - **Release drift is blocked.** Package and Cursor plugin versions must match, and real
11
+ temporary Cursor and Claude installs verify self-contained references and version
12
+ markers.
13
+
3
14
  ## 3.0.1 - 2026-07-10
4
15
 
5
16
  The lean core: one skill, one durable store, evidence on both sides of the repo boundary.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oris-skills",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "Oris Skills: one entry point (/oris-flow) and a guided discovery-to-implementation flow for Cursor, Claude Code, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Davide Baldassarre",
@@ -27,15 +27,14 @@ The `oris-skills` CLI is NOT installed globally. ALWAYS invoke it as `npx oris-s
27
27
  ## Questions
28
28
 
29
29
  - ASK through the platform's question tool — Claude `AskUserQuestion`, Cursor `AskQuestion`
30
- (Plan Mode). A question turn = ONE call to it. Chat text is a fallback, NOT a choice: use
31
- it ONLY when no such tool is available (e.g. Cursor Agent Mode) or the question cannot fit
32
- the tool's schema (open-ended, or more options than it allows) then present the options
33
- as a lettered list.
30
+ whenever it is present in the current tool list. Never infer availability from the
31
+ platform mode. A question turn = ONE call to it. Chat text is a fallback, NOT a choice:
32
+ use it ONLY when no question tool is present or the question is open-ended.
34
33
  - Oris gates and interview questions ALWAYS qualify for the tool: the answer decides the
35
34
  next step. NEVER skip the tool because an answer "looks like a sensible default".
36
- - FIT the schema: question tools cap the options (typically 4, meta-options included).
37
- MORE options → split into a two-level question (category first) or use the deliberate
38
- text fallback above.
35
+ - FIT both platforms: every question has 2–4 explicit options, including meta-options.
36
+ MORE choices → split them into successive native questions; never flatten them to chat
37
+ text while the tool is present.
39
38
  - ONE question per turn. Recommended answer FIRST.
40
39
  - VISIBILITY: assume the user sees ONLY what the question tool itself renders — chat text
41
40
  emitted in the same turn as the tool call can be hidden by the platform UI. Therefore
@@ -17,6 +17,22 @@ function exists(relativePath) {
17
17
  return fs.existsSync(path.join(root, relativePath));
18
18
  }
19
19
 
20
+ function routeDestinations(text) {
21
+ return [...text.matchAll(/^\|\s*[a-z-]+\s*\|[^\n]*\|\s*`(skills\/oris-flow\/references\/[a-z-]+\.md)`\s*\|$/gm)]
22
+ .map((match) => match[1]);
23
+ }
24
+
25
+ function navigatorNodes(text) {
26
+ const nodes = new Map();
27
+ const matches = [...text.matchAll(/^### `([a-z-]+)`\r?\n([\s\S]*?)(?=^### `|^## |(?![\s\S]))/gm)];
28
+ for (const match of matches) {
29
+ const options = [...match[2].matchAll(/^- `[^`]+` → `(node|route|explain):([a-z-]+)`/gm)]
30
+ .map((option) => ({ type: option[1], target: option[2] }));
31
+ nodes.set(match[1], options);
32
+ }
33
+ return nodes;
34
+ }
35
+
20
36
  function runCli(args, homeDir) {
21
37
  return childProcess.spawnSync(process.execPath, ["scripts/oris-skills.mjs", ...args], {
22
38
  cwd: root,
@@ -40,18 +56,46 @@ test("plugin manifest skills all exist and match front-matter names", () => {
40
56
 
41
57
  test("oris-flow route table destinations exist and routing stays in-chat", () => {
42
58
  const text = read("skills/oris-flow/SKILL.md");
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");
59
+ const destinations = routeDestinations(text);
60
+ assert.equal(destinations.length, 16, "route table must list all 16 destinations");
45
61
  for (const destination of destinations) assert.equal(exists(destination), true, destination);
46
62
  assert.deepEqual(destinations, [...new Set(destinations)], "route destinations must be unique");
47
63
  assert.match(text, /NEVER tell the user to type another command/i);
48
64
  assert.match(text, /route to the interview owner/i);
49
65
  assert.match(text, /references\/conventions\.md/);
66
+ assert.match(text, /references\/question-navigator\.md/);
67
+ assert.doesNotMatch(text, /plain-text lettered list|present it as a plain-text/i);
68
+ });
69
+
70
+ test("question navigator covers every route within the portable option cap", () => {
71
+ const skill = read("skills/oris-flow/SKILL.md");
72
+ const navigator = read("skills/oris-flow/references/question-navigator.md");
73
+ const routes = routeDestinations(skill).map((destination) => path.basename(destination, ".md")).sort();
74
+ const nodes = navigatorNodes(navigator);
75
+ assert.ok(nodes.has("root"), "navigator starts at root");
76
+
77
+ const leaves = [];
78
+ for (const [name, options] of nodes) {
79
+ assert.ok(options.length >= 2 && options.length <= 4, `${name} has 2-4 options`);
80
+ assert.deepEqual(
81
+ options.filter((option) => option.type === "explain"),
82
+ [{ type: "explain", target: name }],
83
+ `${name} has one explain option returning to itself`,
84
+ );
85
+ for (const option of options) {
86
+ if (option.type === "node") assert.ok(nodes.has(option.target), `${name} targets existing node ${option.target}`);
87
+ if (option.type === "route") leaves.push(option.target);
88
+ }
89
+ }
90
+ assert.deepEqual(leaves.sort(), routes, "every route appears exactly once as a navigator leaf");
50
91
  });
51
92
 
52
93
  test("shared conventions centralize question, language, and gate rules", () => {
53
94
  const conventions = read("references/conventions.md");
54
95
  assert.match(conventions, /ONE question per turn/i);
96
+ assert.match(conventions, /whenever it is present in the current tool list/i);
97
+ assert.match(conventions, /2–4 explicit options/i);
98
+ assert.doesNotMatch(conventions, /Cursor Agent Mode|Cursor `AskQuestion`\s*\(Plan Mode\)/i);
55
99
  assert.match(conventions, /Explain the options.*Spiega le opzioni/s);
56
100
  assert.match(conventions, /NEVER write documents, code, settings, Git, or DevOps without explicit user confirmation/i);
57
101
  assert.match(conventions, /`Basta domande — procedi`/);
@@ -64,6 +108,12 @@ test("shared conventions centralize question, language, and gate rules", () => {
64
108
  assert.equal(exists("references/questions.md"), false, "questions.md was merged into conventions.md");
65
109
  });
66
110
 
111
+ test("package and plugin expose one release version", () => {
112
+ const packageJson = JSON.parse(read("package.json"));
113
+ const plugin = JSON.parse(read(".cursor-plugin/plugin.json"));
114
+ assert.equal(plugin.version, packageJson.version);
115
+ });
116
+
67
117
  test("every skill points at conventions instead of repeating shared rules", () => {
68
118
  const manifest = JSON.parse(read(".cursor-plugin/plugin.json"));
69
119
  for (const entry of manifest.skills) {
@@ -28,38 +28,56 @@ function markdownFiles(dir) {
28
28
  // A standalone skill (Claude/Cursor) has no bundle root above it, so every reference it
29
29
  // points to must resolve inside the skill folder — the regression that broke
30
30
  // `references/conventions.md` at runtime.
31
- test("installed Claude skill is self-contained", () => {
31
+ test("installed Cursor and Claude skills are self-contained and version-aligned", () => {
32
32
  const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-self-contained-"));
33
33
  try {
34
+ fs.mkdirSync(path.join(homeDir, ".cursor"), { recursive: true });
34
35
  fs.mkdirSync(path.join(homeDir, ".claude"), { recursive: true });
35
- const result = runCli(["install", "--force"], homeDir);
36
+ const result = runCli(["install", "--force", "--no-mcp", "--agents", "cursor,claude"], homeDir);
36
37
  assert.equal(result.status, 0, result.stderr);
37
38
 
38
- const skill = path.join(homeDir, ".claude", "skills", "oris-flow");
39
- assert.ok(fs.existsSync(skill), "oris-flow skill was installed");
39
+ const packageVersion = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version;
40
+ const installManifest = JSON.parse(
41
+ fs.readFileSync(path.join(homeDir, ".oris", "oris-skills", ".oris-skills-install.json"), "utf8"),
42
+ );
43
+ assert.equal(installManifest.version, packageVersion, "bundle manifest matches package version");
40
44
 
41
- // The shared references that used to dangle must now be present.
42
- for (const name of ["conventions.md", "doc-policy.md"]) {
43
- assert.ok(
44
- fs.existsSync(path.join(skill, "references", name)),
45
- `shared reference ${name} is bundled into the skill`,
46
- );
47
- }
45
+ for (const agent of ["cursor", "claude"]) {
46
+ const skill = path.join(homeDir, `.${agent}`, "skills", "oris-flow");
47
+ assert.ok(fs.existsSync(skill), `${agent} oris-flow skill was installed`);
48
+
49
+ for (const name of ["conventions.md", "doc-policy.md", "question-navigator.md"]) {
50
+ assert.ok(
51
+ fs.existsSync(path.join(skill, "references", name)),
52
+ `${agent} bundles ${name}`,
53
+ );
54
+ }
48
55
 
49
- const files = markdownFiles(skill);
50
- const pathToken = /(?:references|templates|agents)\/[A-Za-z0-9._/-]+\.(?:md|json|toml)/g;
51
- const missing = [];
52
- let bundlePrefixLeaks = 0;
53
- for (const file of files) {
54
- const text = fs.readFileSync(file, "utf8");
55
- // No bundle-root-relative pointer may survive in a standalone skill.
56
- if (text.includes("skills/oris-flow/")) bundlePrefixLeaks += 1;
57
- for (const cited of text.match(pathToken) ?? []) {
58
- if (!fs.existsSync(path.join(skill, cited))) missing.push(`${path.basename(file)} → ${cited}`);
56
+ const files = markdownFiles(skill);
57
+ const pathToken = /(?:references|templates|agents)\/[A-Za-z0-9._/-]+\.(?:md|json|toml)/g;
58
+ const missing = [];
59
+ let bundlePrefixLeaks = 0;
60
+ for (const file of files) {
61
+ const text = fs.readFileSync(file, "utf8");
62
+ if (text.includes("skills/oris-flow/")) bundlePrefixLeaks += 1;
63
+ for (const cited of text.match(pathToken) ?? []) {
64
+ if (!fs.existsSync(path.join(skill, cited))) missing.push(`${path.basename(file)} ${cited}`);
65
+ }
59
66
  }
67
+ assert.equal(bundlePrefixLeaks, 0, `${agent} has no bundle-root path leaks`);
68
+ assert.deepEqual(missing, [], `${agent} cited references resolve`);
69
+
70
+ const navigator = fs.readFileSync(path.join(skill, "references", "question-navigator.md"), "utf8");
71
+ const leaves = [...navigator.matchAll(/`route:([a-z-]+)`/g)].map((match) => match[1]);
72
+ assert.equal(leaves.length, 16, `${agent} navigator keeps all route leaves`);
73
+ for (const match of navigator.matchAll(/^### `([a-z-]+)`\r?\n([\s\S]*?)(?=^### `|^## |(?![\s\S]))/gm)) {
74
+ const options = match[2].match(/^- `[^`]+` → `/gm) ?? [];
75
+ assert.ok(options.length >= 2 && options.length <= 4, `${agent} ${match[1]} keeps 2-4 options`);
76
+ }
77
+
78
+ const marker = JSON.parse(fs.readFileSync(path.join(skill, ".oris-skills-managed.json"), "utf8"));
79
+ assert.equal(marker.version, packageVersion, `${agent} marker matches package version`);
60
80
  }
61
- assert.equal(bundlePrefixLeaks, 0, "no `skills/oris-flow/` paths remain in the standalone skill");
62
- assert.deepEqual(missing, [], "every cited reference resolves inside the skill");
63
81
  } finally {
64
82
  fs.rmSync(homeDir, { recursive: true, force: true });
65
83
  }
@@ -50,8 +50,9 @@ test("skill style: oris-flow", () => {
50
50
 
51
51
  test("every route reference carries the style and resolves its handles", () => {
52
52
  const skillText = fs.readFileSync(path.join(skillsRoot, "oris-flow", "SKILL.md"), "utf8");
53
- const routes = [...skillText.matchAll(/`skills\/oris-flow\/references\/([a-z-]+\.md)`/g)].map((match) => match[1]);
54
- assert.ok(routes.length >= 13, `router table lists all routes (found ${routes.length})`);
53
+ const routes = [...skillText.matchAll(/^\|\s*[a-z-]+\s*\|[^\n]*\|\s*`skills\/oris-flow\/references\/([a-z-]+\.md)`\s*\|$/gm)]
54
+ .map((match) => match[1]);
55
+ assert.equal(routes.length, 16, `router table lists all routes (found ${routes.length})`);
55
56
  assert.deepEqual(routes, [...new Set(routes)], "route destinations are unique");
56
57
  const referencesDir = path.join(skillsRoot, "oris-flow", "references");
57
58
  for (const file of routes) {
@@ -47,8 +47,7 @@ Match the desired outcome to ONE route, then apply its reference:
47
47
  3. CHANGED spec on a feature that already has task docs or code → change, not discover —
48
48
  change owns the delta and the history. Spec unchanged but code/reality moved on and the
49
49
  task docs lag behind → docs, not change.
50
- 4. BROKEN behavior → fix. NEW planned work → implement. The menu answer "Implement or
51
- fix" gets exactly one follow-up question to split them.
50
+ 4. BROKEN behavior → fix. NEW planned work → implement.
52
51
  5. EMPTY directory or from-scratch intent → new. Existing repo never touched by Oris
53
52
  (no `AGENTS.md`, no `.oris-flow/`) → recommend setup first; respect the user's choice.
54
53
  6. COMMIT staged/working changes with the standard → commit. OPEN or update a pull request
@@ -58,34 +57,11 @@ Match the desired outcome to ONE route, then apply its reference:
58
57
  8. CONTINUE elsewhere — session ending, context full, "pass this to another agent" →
59
58
  handoff.
60
59
 
61
- ## Menu (low confidence)
60
+ ## Low-confidence navigator
62
61
 
63
- ASK "What do you want to do?" in the user's language. This menu exceeds every
64
- question tool's option cap present it as a plain-text lettered list (the deliberate
65
- fallback in `references/conventions.md` `## Questions`), options:
66
-
67
- - Setup repository
68
- - New project from scratch
69
- - Capture an idea or bug as a work item (triage)
70
- - Understand / define behavior (discovery)
71
- - Acceptance criteria
72
- - Technical plan
73
- - Implement or fix
74
- - Verify acceptance criteria
75
- - Spec changed: update feature docs
76
- - Architecture review
77
- - Update docs
78
- - Commit changes
79
- - Open a pull request
80
- - Hand off this session (continue elsewhere)
81
- - Help
82
- - Explain the options / Spiega le opzioni
83
-
84
- IF "Explain the options" → ONE message that contains BOTH: one explanation line per
85
- option, THEN the full lettered list again below the explanations. Re-asking without the
86
- explanations visible in that same message is the failure this rule exists to prevent.
87
- Every other label maps to its route in the table above; "Implement or fix" is the one
88
- label that splits, with exactly one follow-up question (Precedence 4).
62
+ IF intent is unclear READ `skills/oris-flow/references/question-navigator.md` and APPLY
63
+ it in the same chat. It owns the native, outcome-based route selection; the router does
64
+ not carry or flatten its menu.
89
65
 
90
66
  ## Never
91
67
 
@@ -93,9 +69,9 @@ label that splits, with exactly one follow-up question (Precedence 4).
93
69
  - Write anything — the route's reference owns every gate.
94
70
  - Hand the user a command or skill name to type — enter the route yourself.
95
71
  - Route to implement while the plan itself is in question.
96
- - Keep interviewing here: one menu question maximum, then enter.
72
+ - Keep interviewing here: unclear intent belongs to the question navigator.
97
73
 
98
74
  ## Done when
99
75
 
100
- - [ ] One route entered and applied in the same chat, or the menu was asked exactly once.
76
+ - [ ] One route entered and applied in the same chat.
101
77
  - [ ] No writes happened before the route's own gate.
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Oris Flow"
3
- short_description: "Start the Oris workflow menu"
4
- default_prompt: "Use $oris-flow to enter the right Oris workflow."
3
+ short_description: "Route, build, and verify with Oris"
4
+ default_prompt: "Use $oris-flow to enter the right Oris route."
@@ -0,0 +1,82 @@
1
+ # Question Navigator
2
+
3
+ Choose one route when the user's intent is unclear. This is a deep routing module, not
4
+ a route: selecting a leaf enters that route in the same chat.
5
+
6
+ RULES: `references/conventions.md`.
7
+
8
+ ## Protocol
9
+
10
+ START at `root`. ASK exactly the options of the current node through the platform's
11
+ question tool. A `node:` target opens that node; a `route:` target enters the referenced
12
+ route immediately.
13
+
14
+ IF no question tool is present → show only the current node as a lettered list, accept
15
+ one answer, then continue through the same tree. Never flatten multiple nodes.
16
+
17
+ IF `explain` is selected → re-ask the same node with every option's description and
18
+ consequence visible inside the question call.
19
+
20
+ ## Tree
21
+
22
+ ### `root`
23
+ - `Understand or decide` → `node:understand` — Clarify behavior, code, or planned work.
24
+ - `Build or verify` → `node:build` — Implement, repair, or check real behavior.
25
+ - `Manage or deliver` → `node:manage` — Start, document, publish, continue, or get help.
26
+ - `Explain the options` → `explain:root` — Describe these groups, then re-ask this node.
27
+
28
+ ### `understand`
29
+ - `Understand behavior` → `node:behavior` — Define, test, or change product behavior.
30
+ - `Decide the solution` → `node:solution` — Plan implementation or review architecture.
31
+ - `Capture work for later` → `route:triage` — Create an agent-ready work item without doing it.
32
+ - `Explain the options` → `explain:understand` — Describe these outcomes, then re-ask this node.
33
+
34
+ ### `behavior`
35
+ - `Define desired behavior` → `route:discover` — Interview and document product behavior.
36
+ - `Define acceptance criteria` → `route:criteria` — Write testable business scenarios.
37
+ - `Update a changed specification` → `route:change` — Apply a confirmed behavior delta.
38
+ - `Explain the options` → `explain:behavior` — Describe these routes, then re-ask this node.
39
+
40
+ ### `solution`
41
+ - `Create a technical plan` → `route:plan` — Produce an execution-ready implementation plan.
42
+ - `Review architecture` → `route:architecture` — Find deepening and refactor opportunities.
43
+ - `Explain the options` → `explain:solution` — Describe these routes, then re-ask this node.
44
+
45
+ ### `build`
46
+ - `Implement planned work` → `route:implement` — Build from a plan or confirmed scope.
47
+ - `Fix broken behavior` → `route:fix` — Diagnose and repair a regression or failed check.
48
+ - `Verify acceptance criteria` → `route:verify` — Check criteria against the real product.
49
+ - `Explain the options` → `explain:build` — Describe these routes, then re-ask this node.
50
+
51
+ ### `manage`
52
+ - `Start or prepare a project` → `node:start` — Bootstrap or map a repository.
53
+ - `Document or deliver work` → `node:deliver` — Update docs, commit, or open a pull request.
54
+ - `Continue or get help` → `node:continue` — Transfer context or troubleshoot Oris.
55
+ - `Explain the options` → `explain:manage` — Describe these outcomes, then re-ask this node.
56
+
57
+ ### `start`
58
+ - `New project from scratch` → `route:new` — Bootstrap an empty or scaffold-only repository.
59
+ - `Set up an existing repository` → `route:setup` — Map an existing repository for Oris.
60
+ - `Explain the options` → `explain:start` — Describe these routes, then re-ask this node.
61
+
62
+ ### `deliver`
63
+ - `Update task documentation` → `route:docs` — Align task docs with current reality.
64
+ - `Commit changes` → `route:commit` — Package changes with the Oris Git standard.
65
+ - `Open or update a pull request` → `route:pr` — Publish the branch for review.
66
+ - `Explain the options` → `explain:deliver` — Describe these routes, then re-ask this node.
67
+
68
+ ### `continue`
69
+ - `Hand off this session` → `route:handoff` — Package state for another session or agent.
70
+ - `Get Oris help` → `route:help` — Explain installation, usage, or troubleshooting.
71
+ - `Explain the options` → `explain:continue` — Describe these routes, then re-ask this node.
72
+
73
+ ## Never
74
+
75
+ - Ask about a route the user's clear intent already selected.
76
+ - Present options from more than one node in a single question.
77
+ - Enter two routes, or stop between a leaf selection and its route.
78
+
79
+ ## Done when
80
+
81
+ - [ ] One leaf selected through native questions, or the same node-by-node text fallback.
82
+ - [ ] The selected route entered in the same chat.