oris-skills 2.2.0 → 2.2.2

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), guided discovery-to-implementation flow, and self-tuning verified loops. Cross-model: Cursor, Claude Code, Codex.",
4
- "version": "2.2.0",
4
+ "version": "2.2.1",
5
5
  "author": {
6
6
  "name": "Davide Baldassarre"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - **Settings are per-project and tiny**: moved out of the user-global `~/.oris/settings.json` into `.oris-flow/settings.json`, holding only `orisFlow.responseStyle` (`standard`/`caveman`) and `orisFlow.artifactLanguage` (document language, default `en`). The installer no longer creates a settings file.
6
+ - **Test profiles removed**: the whole profile/secret subsystem is gone — `defaultProfiles`, `profiles`, the adapter `profile` block and its env mappings, and the loop's profile-gating. Every project drives its own e2e/CI tooling; Oris stores no credentials.
7
+ - **No UI-language setting**: chat, menus, and question labels follow the language the user writes (deduced from context); only generated documents use `artifactLanguage`.
8
+ - **No refresh-policy setting**: map freshness is answered by the `headSha` anchor (`flow scan --check`); the advisory `refreshPolicy` field was dropped from the manifest and schema.
9
+ - **Questions & subagents fire reliably**: conventions now bind a platform question call (Claude `AskUserQuestion`) as a tool call rather than optional chat text, and treat entering an Oris route as the explicit request to spawn scan subagents — so gates open their UI and setup fans out by default.
10
+
11
+ ## 2.2.1 - 2026-07-07
12
+
13
+ - **Loop roles are invisible to users**: the installer no longer writes `oris-loop-{executor,verifier,doctor,debriefer}` into `~/.cursor/agents/`, `~/.claude/agents/`, or `~/.codex/agents/` — they showed up as manually invocable subagents, but the loop orchestrator never spawns them by name (it passes each loop's `prompts/<role>.md` to a generic subagent). `agents/` stays in the repo as the canonical role definitions for development. Uninstall (and therefore reinstall/upgrade) now also removes adapters left behind by earlier versions.
14
+
3
15
  ## 2.2.0 - 2026-07-07
4
16
 
5
17
  - **One skill**: users see only `/oris-flow`. Every former skill (setup, new, discover, criteria, plan, implement, fix, verify, change, architecture, merge, docs, help, loop) is now a route reference inside `skills/oris-flow/references/`; the router reads the intent, picks ONE route, and applies its reference in the same chat. Loop prompt templates moved to `skills/oris-flow/templates/`.
package/README.md CHANGED
@@ -91,9 +91,10 @@ Stop anything, anytime: `npx oris-skills loop chat --action stop`.
91
91
 
92
92
  ## Tuning
93
93
 
94
- Settings live in `~/.oris/settings.json` (`/oris-flow` → Setup language & profiles):
95
- chat and document language (`en`/`it`), test profiles, and `responseStyle: "caveman"`
96
- chat shrinks to maximum token density while documents and safety gates stay complete.
94
+ Settings live in `.oris-flow/settings.json` (`/oris-flow` → Setup): document language
95
+ (`artifactLanguage`, default `en`) and `responseStyle` (`standard`/`caveman`, where
96
+ caveman shrinks chat to maximum token density while documents and safety gates stay
97
+ complete). Chat language is deduced from your messages, not configured.
97
98
 
98
99
  ## Repository layout
99
100
 
@@ -0,0 +1,5 @@
1
+ # Architecture Decision Records
2
+
3
+ Decisions agents must not re-litigate. Format: `NNNN-short-title.md`.
4
+
5
+ Each ADR: context, decision, consequences. See `docs/architecture.md` for current principles.
@@ -7,7 +7,7 @@ Read this before explaining or changing the project.
7
7
  ```text
8
8
  skills/oris-flow/ the ONE user-facing skill: SKILL.md routes; references/ hold every route (setup…loop); templates/ hold the loop role prompts
9
9
  references/ shared contracts (conventions, loop-contract, settings, repo-map, doc-policy, clean-code-checklist) + JSON schemas
10
- agents/ canonical subagent definitions; adapters generated per platform
10
+ agents/ canonical loop-role definitions (internal); never installed into user agent homes — roles reach subagents via each loop's prompts/<role>.md
11
11
  scripts/ Node CLI (oris-skills.mjs), loop runtime, installer, tests
12
12
  docs/ this file, user-guide, maintainer-guide, distribution
13
13
  ```
@@ -14,19 +14,18 @@ The installer detects Cursor / Claude Code / Codex from `~/.cursor`, `~/.claude`
14
14
 
15
15
  ```text
16
16
  ~/.oris/oris-skills/ bundle: scripts, skills, references, agents, docs
17
- ~/.oris/settings.json user settings (created only if missing)
18
17
  ~/.cursor/skills/<name>/ Cursor skills (marker-managed)
19
18
  ~/.claude/skills/<name>/ Claude Code skills (marker-managed)
20
19
  ~/.codex/prompts/<name>.md Codex prompts pointing to the bundled SKILL.md
21
20
  ```
22
21
 
23
- Project assets live only in each repository's `.oris-flow/` (adapter, loops, runtime) plus thin hook wrappers in `.cursor/hooks/` and `.claude/hooks/` written by `npx oris-skills loop bootstrap`.
22
+ Project assets live only in each repository's `.oris-flow/` (adapter, settings, loops, runtime) plus thin hook wrappers in `.cursor/hooks/` and `.claude/hooks/` written by `npx oris-skills loop bootstrap`.
24
23
 
25
24
  ## Uninstall / reinstall
26
25
 
27
26
  ```bash
28
27
  npx oris-skills uninstall --dry-run # preview
29
- npx oris-skills uninstall # removes Oris-owned paths incl. legacy ~/.cursor locations
28
+ npx oris-skills uninstall # removes Oris-owned paths incl. legacy ~/.cursor locations and pre-2.2.1 loop agent adapters (~/.cursor|.claude|.codex/agents/oris-*)
30
29
  npx oris-skills reinstall
31
30
  ```
32
31
 
@@ -42,4 +42,4 @@ npx oris-skills reinstall
42
42
  ```
43
43
 
44
44
  Uninstall removes only Oris-owned paths (including legacy locations); preview with `--dry-run`.
45
- Settings (language, test profiles, `responseStyle: caveman` for ultra-short chat): `~/.oris/settings.json`, edited via `/oris-flow` → Setup.
45
+ Settings (`artifactLanguage` for document language, `responseStyle: caveman` for ultra-short chat): `.oris-flow/settings.json`, edited via `/oris-flow` → Setup. Chat language is deduced from context, not configured.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oris-skills",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Oris Skills: one entry point (/oris-flow), guided discovery-to-implementation flow, and self-tuning verified loops for Cursor, Claude Code, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Davide Baldassarre",
@@ -26,9 +26,18 @@ The `oris-skills` CLI is NOT installed globally. ALWAYS invoke it as `npx oris-s
26
26
 
27
27
  ## Questions
28
28
 
29
- - ASK with the platform question UI (Cursor `AskQuestion`, Claude `AskUserQuestion`); ELSE ask plainly in chat.
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.
34
+ - Oris gates and interview questions ALWAYS qualify for the tool: the answer decides the
35
+ 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.
30
39
  - ONE question per turn. Recommended answer FIRST.
31
- - ALWAYS include, localized to `uiLanguage`:
40
+ - ALWAYS include, in the user's language:
32
41
  - `Explain the options` / `Spiega le opzioni` — explain, then re-ask the same question.
33
42
  - During interviews: `Enough questions — proceed` / `Basta domande — procedi` (early exit → go to the generation gate; NOT approval to write).
34
43
  - NEVER ask what the workspace, docs, code, or setup map already answer. Explore first.
@@ -41,12 +50,15 @@ The `oris-skills` CLI is NOT installed globally. ALWAYS invoke it as `npx oris-s
41
50
 
42
51
  ## Language
43
52
 
44
- - READ `references/settings.md` (user settings: `~/.oris/settings.json`).
45
- - Chat + questions: `uiLanguage`. Generated documents: `artifactLanguage`. Default both: `en`.
53
+ - CHAT, menus, and question labels: the language the user writes — DEDUCED from context,
54
+ no setting. NEVER ask which language to use when the conversation already shows it.
55
+ - GENERATED documents: `orisFlow.artifactLanguage` from `.oris-flow/settings.json`
56
+ (default `en`).
57
+ - Skill instructions and code stay English.
46
58
 
47
59
  ## Response style
48
60
 
49
- READ `orisFlow.responseStyle` (`standard` | `caveman`, default `standard`).
61
+ READ `orisFlow.responseStyle` from `.oris-flow/settings.json` (`standard` | `caveman`, default `standard`).
50
62
 
51
63
  `caveman` = the user pays per token; chat goes maximum density. The contract, in its own voice:
52
64
 
@@ -75,5 +87,11 @@ labels, receipts, error reports, and anything another session must read cold.
75
87
 
76
88
  ## Subagents
77
89
 
78
- - USE parallel subagents for independent scans when the platform has them (Cursor subagents, Claude Task tool).
90
+ - USE parallel subagents for independent scans when the platform's agent tool is in the
91
+ tool list (Claude `Agent` tool — formerly Task; Cursor subagents).
92
+ - The user invoking an Oris route IS the explicit request to spawn: when a route step
93
+ names subagent scans, platform guidance like "only spawn when the user asks" is
94
+ satisfied — do not skip on that ground.
95
+ - WHEN the platform has no agent tool (or spawning is blocked) → run the same scans
96
+ inline, sequentially, same coverage. Coverage never shrinks; only parallelism does.
79
97
  - KEEP subagent output compact: summaries and pointers, not raw logs.
@@ -32,7 +32,7 @@ NEVER split one feature's docs across unrelated directories.
32
32
 
33
33
  ## Rules
34
34
 
35
- 1. Language: documents in `artifactLanguage`; skill instructions stay English.
35
+ 1. Language: documents in `artifactLanguage` (default `en`); chat/UI language is deduced from context. Skill instructions and code stay English.
36
36
  2. Steps are independent: any document may be created without the previous one. State the missing source once, use the strongest confirmed source, record the gap in the document ("not available" localized). Never block on a missing predecessor.
37
37
  3. INSPECT the task folder + setup map before asking the user or writing.
38
38
  4. Every document ends with a change-history table: date | source | change | reason. Later changes rewrite the affected sections AND add a history row — no inline "changed!" notes.
@@ -4,7 +4,7 @@
4
4
  "title": "Oris Loop Repository Adapter",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
- "required": ["$schema", "schemaVersion", "repository", "paths", "commands", "profile", "preflight"],
7
+ "required": ["$schema", "schemaVersion", "repository", "paths", "commands", "preflight"],
8
8
  "properties": {
9
9
  "$schema": {
10
10
  "const": "https://oris.dev/schemas/oris-loop-adapter-v1.json"
@@ -54,22 +54,6 @@
54
54
  "propertyNames": { "pattern": "^[a-z][a-z0-9-]*$" },
55
55
  "additionalProperties": { "type": "string", "minLength": 1 }
56
56
  },
57
- "profile": {
58
- "type": "object",
59
- "additionalProperties": false,
60
- "required": ["required", "defaultRole", "mappings"],
61
- "properties": {
62
- "required": { "type": "boolean" },
63
- "defaultRole": {
64
- "type": "string",
65
- "pattern": "^[a-z][a-zA-Z0-9]*$"
66
- },
67
- "mappings": {
68
- "type": "array",
69
- "items": { "$ref": "#/$defs/mapping" }
70
- }
71
- }
72
- },
73
57
  "preflight": {
74
58
  "type": "object",
75
59
  "additionalProperties": false,
@@ -104,23 +88,6 @@
104
88
  "minimum": 0
105
89
  }
106
90
  }
107
- },
108
- "mapping": {
109
- "type": "object",
110
- "additionalProperties": false,
111
- "required": ["env", "from", "required", "secret"],
112
- "properties": {
113
- "env": {
114
- "type": "string",
115
- "pattern": "^[A-Z_][A-Z0-9_]*$"
116
- },
117
- "from": {
118
- "type": "string",
119
- "pattern": "^[A-Za-z][A-Za-z0-9]*(\\.[A-Za-z][A-Za-z0-9]*)*$"
120
- },
121
- "required": { "type": "boolean" },
122
- "secret": { "type": "boolean" }
123
- }
124
91
  }
125
92
  }
126
93
  }
@@ -6,10 +6,10 @@ Setup map = the default project context for every Oris skill and loop. It exists
6
6
 
7
7
  | File | Scope | Purpose |
8
8
  |------|-------|---------|
9
- | `.oris-flow/manifest.json` | repo | compact index: section paths, freshness, confidence, refresh policy |
9
+ | `.oris-flow/manifest.json` | repo | compact index: section paths, freshness, confidence |
10
10
  | `.oris-flow/maps/**` | repo | short markdown maps: docs, stack, commands, code areas, tests |
11
11
  | `.oris-flow/adapter.json` | repo | loop adapter: paths, commands, model aliases |
12
- | `~/.oris/settings.json` | user | language + test profiles |
12
+ | `.oris-flow/settings.json` | repo | preferences: `responseStyle` + `artifactLanguage` (non-secret) |
13
13
  | `{tasksRoot}/{task}/…` | task | business artifacts only — `setup.tasksRoot` in the manifest, default `.oris-flow/tasks` (see `references/doc-policy.md`) |
14
14
 
15
15
  Manifest and maps are never task authority and NEVER hold secrets.
@@ -24,7 +24,6 @@ Manifest and maps are never task authority and NEVER hold secrets.
24
24
  "generatedAt": "",
25
25
  "updatedAt": "",
26
26
  "setup": { "versionControl": { "orisFlowMap": "commit" } },
27
- "refreshPolicy": {},
28
27
  "sections": {}
29
28
  }
30
29
  ```
@@ -46,7 +45,6 @@ Each section: purpose, authoritative paths, confirmed decisions, key commands/co
46
45
  2. VERIFY stale or high-risk facts against the repository before consequential actions.
47
46
  3. Loops: the orchestrator passes relevant setup-map facts into role prompts at craft time; users never repeat project facts per loop.
48
47
  4. SUGGEST `/oris-flow` → Setup refresh only when a stale gap materially affects the task.
49
- 5. Refresh policy is advisory — never a scheduler.
50
48
 
51
49
  ## Command surface
52
50
 
@@ -49,9 +49,6 @@
49
49
  "setup": {
50
50
  "$ref": "#/$defs/setup"
51
51
  },
52
- "refreshPolicy": {
53
- "$ref": "#/$defs/refreshPolicy"
54
- },
55
52
  "sections": {
56
53
  "$ref": "#/$defs/sections"
57
54
  },
@@ -106,29 +103,6 @@
106
103
  "pattern": "^[a-z][a-zA-Z0-9-]*$"
107
104
  }
108
105
  },
109
- "refreshPolicy": {
110
- "type": "object",
111
- "additionalProperties": false,
112
- "required": ["mode"],
113
- "properties": {
114
- "mode": {
115
- "type": "string",
116
- "enum": ["manual", "monthly", "after-major-refactor", "after-test-layout-change"]
117
- },
118
- "suggestAfterDays": {
119
- "type": "integer",
120
- "minimum": 1
121
- },
122
- "triggers": {
123
- "type": "array",
124
- "items": {
125
- "type": "string",
126
- "enum": ["after-major-refactor", "after-test-layout-change"]
127
- },
128
- "uniqueItems": true
129
- }
130
- }
131
- },
132
106
  "setup": {
133
107
  "type": "object",
134
108
  "additionalProperties": false,
@@ -1,36 +1,24 @@
1
1
  # Oris Settings
2
2
 
3
- User-local settings, never bundle content. Path: `~/.oris/settings.json` (all platforms).
4
- The installer creates it when missing; it NEVER overwrites an existing file.
3
+ Per-project preferences. Path: `.oris-flow/settings.json`. Small and non-secret —
4
+ chat verbosity and document language, nothing else. Missing file defaults below.
5
+ Created/edited via `/oris-flow` → Setup, or by hand.
5
6
 
6
7
  ## Shape (v1)
7
8
 
8
9
  ```json
9
10
  {
10
11
  "version": 1,
11
- "orisFlow": { "uiLanguage": "en", "artifactLanguage": "en", "responseStyle": "standard" },
12
- "defaultProfiles": { "testing": "default" },
13
- "profiles": {
14
- "default": {
15
- "environment": "local",
16
- "values": { "baseUrl": "" },
17
- "secrets": { "username": "", "password": "" },
18
- "flags": { "allowDataMutation": false }
19
- }
20
- }
12
+ "orisFlow": { "responseStyle": "standard", "artifactLanguage": "en" }
21
13
  }
22
14
  ```
23
15
 
24
- - `uiLanguage` → chat, menus, question labels, recaps (`en` | `it`, default `en`).
25
- - `artifactLanguage` → generated documents (`en` | `it`, default `en`).
26
16
  - `responseStyle` → chat verbosity (`standard` | `caveman`, default `standard`). Caveman = maximum token density in chat; contract in `references/conventions.md` `## Response style`. Documents and gates are never compressed.
27
- - `profiles` → repository-neutral test logins/config. The repo adapter maps profile fields to roles.
28
- - `flags.allowDataMutation` → data mutation allowed ONLY with `environment: local` + loopback URLs + this flag true.
17
+ - `artifactLanguage` → language for generated documents (ISO code, default `en`). Chat/UI language is NOT configured here it is deduced from the conversation.
29
18
 
30
19
  ## Rules
31
20
 
32
- 1. READ settings before the first workflow question; missing/invalid → defaults above.
21
+ 1. READ settings before the first workflow decision; missing/invalid → defaults above.
33
22
  2. Current user instructions override settings for the turn.
34
- 3. Passwords: only in this file. NEVER committed, printed, put in docs, loop state, receipts, or CLI args. Mask as `***` everywhere.
35
- 4. CONFIRM before writing settings. Update via `/oris-flow` → Setup → language & profiles.
36
- 5. In an approved loop scope, use only the profile/commands/tools the loop declares; otherwise `blocked`.
23
+ 3. Oris stores no credentials. Test/CI secrets belong to each project's own tooling — never Oris files, docs, loop state, receipts, or logs.
24
+ 4. CONFIRM before writing settings. Update via `/oris-flow` → Setup.
@@ -1,80 +1,30 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "title": "Oris Flow Settings",
4
- "type": "object",
5
- "$id": "https://oris.dev/schemas/oris-flow-settings-v1.json",
6
- "required": ["version", "orisFlow", "defaultProfiles", "profiles"],
7
- "additionalProperties": false,
8
- "properties": {
9
- "version": {
10
- "type": "integer",
11
- "const": 1
12
- },
13
- "orisFlow": {
14
- "type": "object",
15
- "required": ["uiLanguage", "artifactLanguage"],
16
- "additionalProperties": false,
17
- "properties": {
18
- "uiLanguage": {
19
- "type": "string",
20
- "enum": ["en", "it"]
21
- },
22
- "artifactLanguage": {
23
- "type": "string",
24
- "enum": ["it", "en"]
25
- },
26
- "responseStyle": {
27
- "type": "string",
28
- "enum": ["standard", "caveman"],
29
- "description": "Chat verbosity: standard prose or caveman (maximum token density; documents and gates never compressed)."
30
- }
31
- }
32
- },
33
- "defaultProfiles": {
34
- "type": "object",
35
- "minProperties": 1,
36
- "additionalProperties": {
37
- "type": "string",
38
- "minLength": 1
39
- }
40
- },
41
- "profiles": {
42
- "type": "object",
43
- "minProperties": 1,
44
- "additionalProperties": {
45
- "$ref": "#/$defs/profile"
46
- }
47
- }
48
- },
49
- "$defs": {
50
- "profile": {
51
- "type": "object",
52
- "additionalProperties": false,
53
- "required": ["environment", "values", "secrets", "flags"],
54
- "properties": {
55
- "environment": {
56
- "type": "string",
57
- "minLength": 1
58
- },
59
- "values": {
60
- "type": "object",
61
- "additionalProperties": {
62
- "type": ["string", "number", "boolean"]
63
- }
64
- },
65
- "secrets": {
66
- "type": "object",
67
- "additionalProperties": {
68
- "type": "string"
69
- }
70
- },
71
- "flags": {
72
- "type": "object",
73
- "additionalProperties": {
74
- "type": "boolean"
75
- }
76
- }
77
- }
78
- }
79
- }
80
- }
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Oris Flow Settings",
4
+ "$id": "https://oris.dev/schemas/oris-flow-settings-v1.json",
5
+ "type": "object",
6
+ "required": ["version", "orisFlow"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "version": {
10
+ "type": "integer",
11
+ "const": 1
12
+ },
13
+ "orisFlow": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "responseStyle": {
18
+ "type": "string",
19
+ "enum": ["standard", "caveman"],
20
+ "description": "Chat verbosity: standard prose or caveman (maximum token density; documents and gates never compressed). Default standard."
21
+ },
22
+ "artifactLanguage": {
23
+ "type": "string",
24
+ "minLength": 2,
25
+ "description": "Language for generated documents (ISO code, e.g. en, it). Default en. Chat language is deduced from the conversation, not configured here."
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
@@ -8,9 +8,9 @@ export const ORIS_FLOW_TASKS_ROOT = ".oris-flow/tasks";
8
8
  export const ORIS_FLOW_ADAPTER = ".oris-flow/adapter.json";
9
9
  export const ORIS_FLOW_LOOPS_ROOT = ".oris-flow/loops";
10
10
  export const ORIS_FLOW_RUNTIME = ".oris-flow/runtime";
11
+ export const ORIS_FLOW_SETTINGS = ".oris-flow/settings.json";
11
12
  export const ORIS_HOME = path.join(os.homedir(), ".oris");
12
13
  export const ORIS_BUNDLE_ROOT = path.join(ORIS_HOME, "oris-skills");
13
- export const ORIS_SKILLS_SETTINGS = path.join(ORIS_HOME, "settings.json");
14
14
  export const ORIS_LOOP_SCHEMA = "https://oris.dev/schemas/oris-loop-adapter-v1.json";
15
15
  export const ORIS_LOOP_SCHEMA_VERSION = 1;
16
16
  export const ORIS_LOOP_DOCUMENT_VERSION = 2;
@@ -433,9 +433,9 @@ function resolveTasksRoot(root, existingManifest) {
433
433
 
434
434
  /**
435
435
  * Refresh keeps user truth: confirmed decisions, tasksRoot, version control,
436
- * refresh policy, confirmed commands, and confirmed sections (whose map files
437
- * are also left untouched). A confirmed section the scan no longer sees is
438
- * marked stale, never deleted.
436
+ * confirmed commands, and confirmed sections (whose map files are also left
437
+ * untouched). A confirmed section the scan no longer sees is marked stale,
438
+ * never deleted.
439
439
  */
440
440
  function mergeWithExisting(scan, existingManifest) {
441
441
  if (!existingManifest || typeof existingManifest !== "object") return scan;
@@ -447,9 +447,6 @@ function mergeWithExisting(scan, existingManifest) {
447
447
  if (existingManifest.setup?.versionControl?.orisFlowMap) {
448
448
  merged.manifest.setup.versionControl.orisFlowMap = existingManifest.setup.versionControl.orisFlowMap;
449
449
  }
450
- if (existingManifest.refreshPolicy && typeof existingManifest.refreshPolicy === "object") {
451
- merged.manifest.refreshPolicy = existingManifest.refreshPolicy;
452
- }
453
450
  if (existingManifest.projectType?.confidence === "confirmed") {
454
451
  merged.manifest.projectType = existingManifest.projectType;
455
452
  }
@@ -507,9 +504,6 @@ export function scanRepository(repositoryRoot, options = {}) {
507
504
  orisFlowMap: options.versionControl ?? "commit",
508
505
  },
509
506
  },
510
- refreshPolicy: {
511
- mode: "manual",
512
- },
513
507
  stacks,
514
508
  commands,
515
509
  sections: sectionPointers,
@@ -58,7 +58,11 @@ function readAgents() {
58
58
  .map((name) => ({ name, ...readAgent(path.join(sourceRoot, name)) }));
59
59
  }
60
60
 
61
- /** Write adapters into repo-local .cursor/.claude/.codex dirs under root. */
61
+ /**
62
+ * Write adapters into repo-local .cursor/.claude/.codex dirs under root.
63
+ * Development-only: the installer never copies these into user agent homes —
64
+ * loop roles are internal and reach subagents via per-loop prompts/<role>.md files.
65
+ */
62
66
  export function generateAgentAdapters(root = repoRoot) {
63
67
  const agents = readAgents();
64
68
  for (const agent of agents) {
@@ -69,20 +73,6 @@ export function generateAgentAdapters(root = repoRoot) {
69
73
  return agents.map((agent) => agent.frontMatter.name);
70
74
  }
71
75
 
72
- /**
73
- * Write adapters into explicit agent homes, e.g. { cursor: "~/.cursor", codex: "~/.codex" }.
74
- * Used by the installer so the generated adapters actually reach the user's agents.
75
- */
76
- export function writeAgentAdapters(targets) {
77
- const agents = readAgents();
78
- for (const agent of agents) {
79
- if (targets.cursor) writeFile(path.join(targets.cursor, "agents", agent.name), renderMarkdownAgent(agent));
80
- if (targets.claude) writeFile(path.join(targets.claude, "agents", agent.name), renderMarkdownAgent(agent));
81
- if (targets.codex) writeFile(path.join(targets.codex, "agents", agent.name.replace(/\.md$/, ".toml")), renderCodexAgent(agent));
82
- }
83
- return agents.map((agent) => agent.frontMatter.name);
84
- }
85
-
86
76
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
87
77
  const rootArg = process.argv.find((arg) => arg.startsWith("--root="));
88
78
  const root = rootArg ? path.resolve(rootArg.slice("--root=".length)) : repoRoot;
@@ -22,7 +22,6 @@ import os from "node:os";
22
22
  import path from "node:path";
23
23
  import process from "node:process";
24
24
  import { fileURLToPath } from "node:url";
25
- import { writeAgentAdapters } from "./generate-agent-adapters.mjs";
26
25
 
27
26
  const markerName = ".oris-skills-managed.json";
28
27
  const installManifestName = ".oris-skills-install.json";
@@ -48,7 +47,6 @@ function defaultPaths() {
48
47
  const home = os.homedir();
49
48
  return {
50
49
  bundleRoot: path.join(home, ".oris", "oris-skills"),
51
- settingsPath: path.join(home, ".oris", "settings.json"),
52
50
  agentRoots: {
53
51
  cursor: path.join(home, ".cursor"),
54
52
  claude: path.join(home, ".claude"),
@@ -65,7 +63,6 @@ function parseArgs(argv) {
65
63
  force: false,
66
64
  agents: [],
67
65
  bundleRoot: paths.bundleRoot,
68
- settingsPath: paths.settingsPath,
69
66
  agentRoots: paths.agentRoots,
70
67
  };
71
68
  for (let i = 0; i < argv.length; i += 1) {
@@ -82,7 +79,6 @@ function parseArgs(argv) {
82
79
  else if (key === "--agents" || key.startsWith("--agents=")) {
83
80
  options.agents = value().split(",").map((name) => name.trim().toLowerCase()).filter(Boolean);
84
81
  } else if (key === "--bundle-root" || key.startsWith("--bundle-root=")) options.bundleRoot = value();
85
- else if (key === "--settings" || key.startsWith("--settings=")) options.settingsPath = value();
86
82
  else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.agentRoots.cursor = value();
87
83
  else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.agentRoots.claude = value();
88
84
  else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.agentRoots.codex = value();
@@ -161,28 +157,24 @@ function manifestSkills(manifest, repoRoot) {
161
157
  });
162
158
  }
163
159
 
164
- function defaultSettings() {
165
- return {
166
- version: 1,
167
- orisFlow: {
168
- uiLanguage: "en",
169
- artifactLanguage: "en",
170
- responseStyle: "standard",
171
- },
172
- defaultProfiles: {
173
- testing: "default",
174
- },
175
- profiles: {
176
- default: {
177
- environment: "local",
178
- values: {},
179
- secrets: {},
180
- flags: {
181
- allowDataMutation: false,
182
- },
183
- },
184
- },
185
- };
160
+ /**
161
+ * Rewrite bundle-root-relative Oris paths to skill-root-relative in a standalone skill
162
+ * copy. The source keeps bundle-relative paths (`skills/<name>/references/…`) so the bundle
163
+ * and the Codex/Copilot pointers resolve; a standalone Claude/Cursor skill has no bundle
164
+ * root above it, so its own `.md` files must point at `references/…` and `templates/…`.
165
+ */
166
+ function rewriteBundlePaths(skillDir, relativePath) {
167
+ // In a standalone skill the skill folder IS the root, so any `skills/<name>/…` pointer
168
+ // (SKILL.md, references/…, templates/…, agents/…) becomes root-relative once the prefix
169
+ // is stripped.
170
+ const prefix = `${relativePath.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
171
+ const entries = fs.readdirSync(skillDir, { recursive: true, withFileTypes: true });
172
+ for (const entry of entries) {
173
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
174
+ const filePath = path.join(entry.parentPath ?? entry.path, entry.name);
175
+ const text = fs.readFileSync(filePath, "utf8");
176
+ if (text.includes(prefix)) fs.writeFileSync(filePath, text.split(prefix).join(""), "utf8");
177
+ }
186
178
  }
187
179
 
188
180
  function installSkillDirs(skills, skillsRoot, manifest, bundleRoot, options) {
@@ -194,6 +186,13 @@ function installSkillDirs(skills, skillsRoot, manifest, bundleRoot, options) {
194
186
  }
195
187
  fs.rmSync(target, { recursive: true, force: true });
196
188
  copy(skill.sourceDir, target);
189
+ // Make the standalone skill self-contained: the shared references it points to
190
+ // (conventions.md, doc-policy.md, …) live in the bundle root, not inside the skill
191
+ // folder, so they were never copied — the whole reason `references/conventions.md`
192
+ // dangled at runtime. Merge them in, then rewrite the paths to skill-relative.
193
+ const bundleReferences = path.join(bundleRoot, "references");
194
+ if (fs.existsSync(bundleReferences)) copy(bundleReferences, path.join(target, "references"));
195
+ rewriteBundlePaths(target, skill.relativePath);
197
196
  writeJson(path.join(target, markerName), {
198
197
  package: manifest.name,
199
198
  version: manifest.version,
@@ -263,7 +262,6 @@ function main() {
263
262
  }
264
263
 
265
264
  assertInsideHome(options.bundleRoot);
266
- assertInsideHome(options.settingsPath);
267
265
  for (const agent of agents) assertInsideHome(options.agentRoots[agent]);
268
266
 
269
267
  if (options.dryRun) {
@@ -273,10 +271,8 @@ function main() {
273
271
  if (agent === "codex") log(`Would write Codex prompts: ${path.join(options.agentRoots.codex, "prompts")}`);
274
272
  else if (agent === "copilot") log(`Would write Copilot prompts: ${path.join(options.agentRoots.copilot, "prompts")}`);
275
273
  else log(`Would write ${agent} skills: ${path.join(options.agentRoots[agent], "skills")}`);
276
- if (agent !== "copilot") log(`Would write ${agent} loop agents: ${path.join(options.agentRoots[agent], "agents")}`);
277
274
  }
278
275
  log(`Would write universal pointer: ${path.join(path.dirname(options.bundleRoot), "oris-flow.md")}`);
279
- log(`Would create settings if missing: ${options.settingsPath}`);
280
276
  return;
281
277
  }
282
278
 
@@ -305,11 +301,6 @@ function main() {
305
301
  }
306
302
  installUniversalPointer(skills, options.bundleRoot);
307
303
 
308
- const adapterTargets = Object.fromEntries(agents.filter((agent) => agent !== "copilot").map((agent) => [agent, options.agentRoots[agent]]));
309
- const loopAgents = writeAgentAdapters(adapterTargets);
310
- log(`Installed ${loopAgents.length} loop agent adapters (${agents.join(", ")})`);
311
-
312
- if (!fs.existsSync(options.settingsPath)) writeJson(options.settingsPath, defaultSettings());
313
304
  writeJson(path.join(options.bundleRoot, installManifestName), {
314
305
  package: manifest.name,
315
306
  version: manifest.version,
@@ -92,6 +92,15 @@ function managedCodexPrompts(codexRoot) {
92
92
  .map((entry) => path.join(promptsRoot, entry.name));
93
93
  }
94
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
+
95
104
  function managedCopilotPrompts(copilotRoot) {
96
105
  const promptsRoot = path.join(copilotRoot, "prompts");
97
106
  if (!fs.existsSync(promptsRoot)) return [];
@@ -119,6 +128,9 @@ function plannedRemovals(options) {
119
128
  ...managedSkillDirs(path.join(options.claudeRoot, "skills")),
120
129
  ...managedCodexPrompts(options.codexRoot),
121
130
  ...managedCopilotPrompts(options.copilotRoot),
131
+ ...managedAgentAdapters(options.cursorRoot, ".md"),
132
+ ...managedAgentAdapters(options.claudeRoot, ".md"),
133
+ ...managedAgentAdapters(options.codexRoot, ".toml"),
122
134
  path.join(path.dirname(options.bundleRoot), "oris-flow.md"),
123
135
  ...legacyOrisPaths(options),
124
136
  ]);
@@ -168,11 +168,6 @@ export function buildAdapterDraft(repositoryRoot, hints = {}) {
168
168
  },
169
169
  commands,
170
170
  models: {},
171
- profile: {
172
- required: false,
173
- defaultRole: "testing",
174
- mappings: [],
175
- },
176
171
  preflight: {
177
172
  minFreeDiskGiB: 5,
178
173
  },
@@ -15,7 +15,6 @@ import {
15
15
  ORIS_FLOW_ADAPTER,
16
16
  ORIS_LOOP_SCHEMA,
17
17
  ORIS_LOOP_SCHEMA_VERSION,
18
- ORIS_SKILLS_SETTINGS,
19
18
  } from "../flow/oris-flow-layout.mjs";
20
19
  import {
21
20
  activeRoles,
@@ -42,8 +41,6 @@ function parseArgs(argv) {
42
41
  runner: "chat",
43
42
  maxIterations: 10,
44
43
  maxMinutes: 240,
45
- profile: "",
46
- settingsPath: ORIS_SKILLS_SETTINGS,
47
44
  };
48
45
  const readValue = (index, raw) => {
49
46
  if (raw.includes("=")) return [index, raw.slice(raw.indexOf("=") + 1)];
@@ -60,8 +57,6 @@ function parseArgs(argv) {
60
57
  else if (key === "--progress" || key.startsWith("--progress=")) [i, options.progress] = readValue(i, raw);
61
58
  else if (key === "--repository-root" || key.startsWith("--repository-root=")) [i, options.repositoryRoot] = readValue(i, raw);
62
59
  else if (key === "--runner" || key.startsWith("--runner=")) [i, options.runner] = readValue(i, raw);
63
- else if (key === "--profile" || key.startsWith("--profile=")) [i, options.profile] = readValue(i, raw);
64
- else if (key === "--settings-path" || key.startsWith("--settings-path=")) [i, options.settingsPath] = readValue(i, raw);
65
60
  else if (key === "--max-iterations" || key.startsWith("--max-iterations=")) {
66
61
  const parsed = readValue(i, raw);
67
62
  i = parsed[0];
@@ -143,30 +138,6 @@ function reconcileContext(root, state) {
143
138
  fs.renameSync(temporary, contextPath);
144
139
  }
145
140
 
146
- function getPathValue(value, dottedPath) {
147
- return dottedPath.split(".").filter(Boolean).reduce((current, key) => (
148
- current != null && typeof current === "object" ? current[key] : undefined
149
- ), value);
150
- }
151
-
152
- function validateRequiredProfile(options, adapter) {
153
- if (adapter.profile?.required !== true) return;
154
- const notArmed = "Oris chat loop was not armed; the stop hook will not resume this chat. Fix the profile, rerun start, then end the turn.";
155
- if (!fs.existsSync(options.settingsPath)) fail(`Required Oris settings are missing: ${options.settingsPath}. ${notArmed}`);
156
- const settings = JSON.parse(fs.readFileSync(options.settingsPath, "utf8"));
157
- if (settings.version !== 1) fail(`Oris settings must use schema v1. ${notArmed}`);
158
- const role = adapter.profile.defaultRole || "testing";
159
- const profileName = options.profile || settings.defaultProfiles?.[role];
160
- const profile = settings.profiles?.[profileName];
161
- if (!profile) fail(`Required Oris profile '${profileName || "(none)"}' was not found. ${notArmed}`);
162
- for (const mapping of adapter.profile.mappings ?? []) {
163
- const value = getPathValue(profile, mapping.from);
164
- if (mapping.required === true && (value == null || String(value).trim() === "")) {
165
- fail(`Profile '${profileName}' is missing required field '${mapping.from}'. ${notArmed}`);
166
- }
167
- }
168
- }
169
-
170
141
  /** A same-chat loop needs at least one armed stop hook: Cursor or Claude Code. */
171
142
  function assessStopHooks(root) {
172
143
  const registered = [];
@@ -267,7 +238,6 @@ if (options.action === "start") {
267
238
  fail("No Oris stop hook is registered (.cursor/hooks.json or .claude/settings.json). Run: oris-skills loop bootstrap");
268
239
  }
269
240
  }
270
- validateRequiredProfile(options, adapter);
271
241
  const minimumFreeDiskGiB = Number(adapter.preflight?.minFreeDiskGiB ?? 0);
272
242
  if (typeof fs.statfsSync === "function") {
273
243
  const disk = fs.statfsSync(root);
@@ -11,6 +11,7 @@ const nodeTestFiles = [
11
11
  "scripts/tests/test-schemas.mjs",
12
12
  "scripts/tests/test-oris-loop-document.mjs",
13
13
  "scripts/tests/test-agent-adapters.mjs",
14
+ "scripts/tests/test-skill-self-contained.mjs",
14
15
  "scripts/tests/test-oris-1-0-cleanliness.mjs",
15
16
  "scripts/tests/test-oris-flow-scan.mjs",
16
17
  "scripts/tests/test-routing-lifecycle.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
+ });
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import test from "node:test";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { initSmokeRepository } from "../loop/oris-loop-fixtures.mjs";
9
- import { ORIS_FLOW_ADAPTER, ORIS_FLOW_RUNTIME } from "../flow/oris-flow-layout.mjs";
9
+ import { ORIS_FLOW_RUNTIME } from "../flow/oris-flow-layout.mjs";
10
10
 
11
11
  const scriptsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "loop");
12
12
 
@@ -118,46 +118,3 @@ test("same-chat start accepts an existing loop slug", () => {
118
118
  }
119
119
  });
120
120
 
121
- test("missing required profile leaves same-chat loop unarmed", () => {
122
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-profile-"));
123
- try {
124
- initSmokeRepository(root, {
125
- taskSlug: "profile-smoke",
126
- program: "profile-smoke",
127
- });
128
- const adapterPath = path.join(root, ORIS_FLOW_ADAPTER);
129
- const adapter = JSON.parse(fs.readFileSync(adapterPath, "utf8"));
130
- adapter.profile = {
131
- required: true,
132
- defaultRole: "testing",
133
- mappings: [
134
- { env: "BASE_URL", from: "values.baseUrl", required: true, secret: false },
135
- ],
136
- };
137
- fs.writeFileSync(adapterPath, `${JSON.stringify(adapter, null, 2)}\n`, "utf8");
138
- const settingsPath = path.join(root, "settings.json");
139
- fs.writeFileSync(settingsPath, `${JSON.stringify({
140
- version: 1,
141
- orisFlow: { uiLanguage: "en", artifactLanguage: "en" },
142
- defaultProfiles: { testing: "default" },
143
- profiles: {
144
- default: { environment: "local", values: {}, secrets: {}, flags: {} },
145
- },
146
- }, null, 2)}\n`, "utf8");
147
-
148
- const chat = path.join(scriptsRoot, "oris-loop-chat.mjs");
149
- const start = spawnSync(process.execPath, [
150
- chat,
151
- "--action", "start",
152
- "--loop", "profile-smoke",
153
- "--repository-root", root,
154
- "--settings-path", settingsPath,
155
- ], { encoding: "utf8" });
156
- assert.notEqual(start.status, 0);
157
- assert.match(start.stderr, /missing required field 'values\.baseUrl'/);
158
- assert.match(start.stderr, /Oris chat loop was not armed; the stop hook will not resume this chat/);
159
- assert.equal(fs.existsSync(path.join(root, ORIS_FLOW_RUNTIME, "chat-active.json")), false);
160
- } finally {
161
- fs.rmSync(root, { recursive: true, force: true });
162
- }
163
- });
@@ -12,26 +12,27 @@ function read(relativePath) {
12
12
  return JSON.parse(fs.readFileSync(path.join(root, relativePath), "utf8"));
13
13
  }
14
14
 
15
- test("settings schema is the generic v1 contract", () => {
15
+ test("settings schema is the minimal v1 contract: only responseStyle and artifactLanguage", () => {
16
16
  const schema = read("references/settings.schema.json");
17
17
  assert.equal(schema.properties.version.const, 1);
18
- assert.deepEqual(schema.required, ["version", "orisFlow", "defaultProfiles", "profiles"]);
19
- assert.ok(schema.$defs.profile.properties.values);
20
- assert.ok(schema.$defs.profile.properties.secrets);
21
- assert.ok(schema.$defs.profile.properties.flags);
18
+ assert.deepEqual(schema.required, ["version", "orisFlow"]);
19
+ assert.ok(schema.properties.orisFlow.properties.responseStyle);
20
+ assert.ok(schema.properties.orisFlow.properties.artifactLanguage);
21
+ assert.ok(!schema.properties.defaultProfiles, "test profiles were removed from settings");
22
+ assert.ok(!schema.properties.profiles, "test profiles were removed from settings");
22
23
  });
23
24
 
24
- test("adapter schema v1 declares paths, commands, model aliases, profile, preflight", () => {
25
+ test("adapter schema v1 declares paths, commands, model aliases, preflight (no test profiles)", () => {
25
26
  const schema = read("references/loop-adapter.schema.json");
26
27
  assert.equal(schema.$id, "https://oris.dev/schemas/oris-loop-adapter-v1.json");
27
28
  assert.equal(schema.properties.schemaVersion.const, 1);
28
29
  assert.deepEqual(
29
30
  schema.required,
30
- ["$schema", "schemaVersion", "repository", "paths", "commands", "profile", "preflight"],
31
+ ["$schema", "schemaVersion", "repository", "paths", "commands", "preflight"],
31
32
  );
32
33
  assert.ok(schema.properties.paths.properties.loops);
33
34
  assert.ok(schema.properties.models, "adapter schema must allow a model alias map");
34
- assert.equal(schema.$defs.mapping.properties.secret.type, "boolean");
35
+ assert.ok(!schema.properties.profile, "test profiles were removed from the adapter");
35
36
  });
36
37
 
37
38
  test("loop document schema is v2 with approved gate, per-role models, and improve mode", () => {
@@ -0,0 +1,66 @@
1
+ import childProcess from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import assert from "node:assert/strict";
6
+ import process from "node:process";
7
+ import test from "node:test";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
11
+
12
+ function runCli(args, homeDir) {
13
+ return childProcess.spawnSync(process.execPath, ["scripts/oris-skills.mjs", ...args], {
14
+ cwd: root,
15
+ encoding: "utf8",
16
+ env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir },
17
+ shell: false,
18
+ });
19
+ }
20
+
21
+ function markdownFiles(dir) {
22
+ return fs
23
+ .readdirSync(dir, { recursive: true, withFileTypes: true })
24
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
25
+ .map((entry) => path.join(entry.parentPath ?? entry.path, entry.name));
26
+ }
27
+
28
+ // A standalone skill (Claude/Cursor) has no bundle root above it, so every reference it
29
+ // points to must resolve inside the skill folder — the regression that broke
30
+ // `references/conventions.md` at runtime.
31
+ test("installed Claude skill is self-contained", () => {
32
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-self-contained-"));
33
+ try {
34
+ fs.mkdirSync(path.join(homeDir, ".claude"), { recursive: true });
35
+ const result = runCli(["install", "--force"], homeDir);
36
+ assert.equal(result.status, 0, result.stderr);
37
+
38
+ const skill = path.join(homeDir, ".claude", "skills", "oris-flow");
39
+ assert.ok(fs.existsSync(skill), "oris-flow skill was installed");
40
+
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
+ }
48
+
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}`);
59
+ }
60
+ }
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
+ } finally {
64
+ fs.rmSync(homeDir, { recursive: true, force: true });
65
+ }
66
+ });
@@ -50,7 +50,9 @@ Match the desired outcome to ONE route, then apply its reference:
50
50
 
51
51
  ## Menu (low confidence)
52
52
 
53
- ASK "What do you want to do?" localized to `uiLanguage`, options:
53
+ ASK "What do you want to do?" in the user's language. This menu exceeds every
54
+ question tool's option cap — present it as a plain-text lettered list (the deliberate
55
+ fallback in `references/conventions.md` `## Questions`), options:
54
56
 
55
57
  - Setup repository
56
58
  - New project from scratch
@@ -16,7 +16,7 @@ details on request.
16
16
  | Loop status | `npx oris-skills loop chat --action status` / `npx oris-skills loop list` |
17
17
  | Skill missing after install | Reload the agent; check `~/.oris/oris-skills/` exists; re-run install with `--force`. |
18
18
  | Hook not firing | `npx oris-skills loop verify --temp` self-checks the runtime; `npx oris-skills loop bootstrap` re-arms repo hooks. |
19
- | Change language / test profiles | `/oris-flow` → Setup → language & profiles (settings live in `~/.oris/settings.json`). |
19
+ | Change document language / chat verbosity | `/oris-flow` → Setup (settings live in `.oris-flow/settings.json`). |
20
20
  | Ultra-short answers | `/oris-flow` → Setup → set `responseStyle: caveman` — chat shrinks to maximum token density; documents stay complete. |
21
21
  | Edit a loop's behavior | Edit the files in `.oris-flow/loops/{slug}/prompts/` — the next pass obeys them. |
22
22
 
@@ -18,7 +18,7 @@ refreshing. THEN ask ONE question with these options:
18
18
  - review stale sections
19
19
  - show summary (no writes)
20
20
  - recreate from scratch
21
- - language & profiles → Local Preferences below; touches only `~/.oris/settings.json`
21
+ - preferences → Local Preferences below; touches only `.oris-flow/settings.json`
22
22
  - clean runtime → preview with `npx oris-skills flow clean-runtime --repository-root <repo> --dry-run`, confirm, run without `--dry-run`
23
23
 
24
24
  ELSE → create from scratch.
@@ -29,9 +29,13 @@ ELSE → create from scratch.
29
29
  2. SCAN: `npx oris-skills flow scan --repository-root <repo> --json` — deterministic
30
30
  baseline: `projectType`, stacks, commands (with confidence), sections, `truncated`
31
31
  flag. Scan output = draft evidence, not truth.
32
- 3. IF `truncated: true` → say so; large monorepos may need per-area subagent scans.
33
- 4. ADD parallel subagent scans (docs, frontend, backend/data, tests/build, local
34
- skills/rules) only when the baseline leaves real gaps.
32
+ 3. IF `truncated: true` → say so, then per-area subagent scans are REQUIRED, not optional.
33
+ 4. FAN OUT parallel subagent scans (docs, frontend, backend/data, tests/build, local
34
+ skills/rules) by DEFAULT whenever the repo has more than one of those areas —
35
+ entering setup is the user's request to spawn (`references/conventions.md`
36
+ `## Subagents`). SKIP only when the repo is small enough that the baseline already
37
+ answers every §2 decision — say you skipped and why. No agent tool on the platform →
38
+ same scans inline, sequentially.
35
39
 
36
40
  ## 2. Present findings, decide one at a time
37
41
 
@@ -45,8 +49,6 @@ Then WALK decisions ONE per turn — short explainer first, recommended answer f
45
49
  project docs. Record in `setup.tasksRoot`.
46
50
  - **Verify commands** — show detected build+test commands; user confirms or corrects →
47
51
  mark `confirmed`. They feed implement, verify, and loop VERIFIER; unconfirmed = a guess.
48
- - **Refresh policy** (manual | monthly | after refactor | after test-layout change) —
49
- advisory only, never a scheduler.
50
52
  - **Version control for `.oris-flow/`** — commit (team default) or gitignore. Task docs
51
53
  follow the same choice.
52
54
 
@@ -78,10 +80,10 @@ ON explicit confirmation per file → scaffold the missing ones minimal.
78
80
 
79
81
  ## Local Preferences
80
82
 
81
- 1. READ `~/.oris/settings.json` (defaults in `references/settings.md`).
82
- 2. ASK what to change: uiLanguage | artifactLanguage | responseStyle (standard |
83
- caveman — maximum token density in chat) | default test profile | profiles.
84
- 3. SHOW masked summary (passwords `***`), CONFIRM, write ONLY the settings file.
83
+ 1. READ `.oris-flow/settings.json` (defaults in `references/settings.md`).
84
+ 2. ASK what to change: artifactLanguage (document language, default `en`) | responseStyle
85
+ (standard | caveman — maximum token density in chat).
86
+ 3. SHOW summary, CONFIRM, write ONLY the settings file.
85
87
 
86
88
  ## Never
87
89