oris-skills 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +2 -11
- package/CHANGELOG.md +48 -0
- package/README.md +79 -31
- package/agents/oris-loop-debriefer.md +1 -0
- package/agents/oris-loop-doctor.md +1 -1
- package/agents/oris-loop-executor.md +5 -2
- package/agents/oris-loop-verifier.md +6 -2
- package/docs/architecture.md +9 -7
- package/docs/distribution.md +1 -1
- package/docs/maintainer-guide.md +5 -5
- package/docs/user-guide.md +12 -9
- package/package.json +2 -3
- package/references/clean-code-checklist.md +20 -107
- package/references/conventions.md +43 -3
- package/references/doc-policy.md +19 -3
- package/references/loop-contract.md +24 -17
- package/references/loop.schema.json +13 -0
- package/references/repo-map.md +3 -3
- package/references/repo-map.schema.json +41 -0
- package/references/settings.md +2 -1
- package/references/settings.schema.json +5 -0
- package/scripts/flow/oris-flow-layout.mjs +1 -0
- package/scripts/flow/oris-flow-scan.mjs +265 -27
- package/scripts/install/generate-agent-adapters.mjs +19 -2
- package/scripts/install/install-user-skills.mjs +61 -3
- package/scripts/install/uninstall-user-skills.mjs +17 -1
- package/scripts/loop/oris-loop-bootstrap.mjs +7 -2
- package/scripts/loop/oris-loop-chat.mjs +34 -16
- package/scripts/loop/oris-loop-demo.mjs +11 -20
- package/scripts/loop/oris-loop-document.mjs +24 -7
- package/scripts/loop/oris-loop-dry-run.mjs +8 -4
- package/scripts/loop/oris-loop-fixtures.mjs +13 -12
- package/scripts/loop/oris-loop-run.mjs +80 -20
- package/scripts/loop/oris-loop-stop.mjs +48 -9
- package/scripts/loop/oris-loop-templates.mjs +13 -10
- package/scripts/loop/oris-loop-verify.mjs +1 -2
- package/scripts/oris-skills.mjs +3 -2
- package/scripts/tests/run-all-tests.mjs +1 -2
- package/scripts/tests/test-oris-flow-scan.mjs +109 -1
- package/scripts/tests/test-oris-loop-document.mjs +39 -3
- package/scripts/tests/test-oris-loop-run.mjs +28 -2
- package/scripts/tests/test-oris-loop-smoke.mjs +36 -3
- package/scripts/tests/test-oris-loop-stop.mjs +65 -4
- package/scripts/tests/test-routing-lifecycle.mjs +38 -23
- package/scripts/tests/test-skill-style.mjs +64 -0
- package/skills/oris-flow/SKILL.md +53 -25
- package/skills/oris-flow/references/architecture.md +77 -0
- package/skills/oris-flow/references/change.md +60 -0
- package/skills/oris-flow/references/criteria.md +60 -0
- package/skills/oris-flow/references/discover.md +66 -0
- package/skills/oris-flow/references/docs.md +33 -0
- package/skills/oris-flow/references/fix.md +49 -0
- package/skills/oris-flow/references/help.md +34 -0
- package/skills/oris-flow/references/implement.md +48 -0
- package/skills/oris-flow/references/loop-craft.md +59 -0
- package/skills/oris-flow/references/loop-improve.md +32 -0
- package/skills/oris-flow/references/loop-run.md +47 -0
- package/skills/oris-flow/references/loop.md +56 -0
- package/skills/oris-flow/references/new.md +64 -0
- package/skills/oris-flow/references/plan.md +58 -0
- package/skills/oris-flow/references/setup.md +100 -0
- package/skills/oris-flow/references/verify.md +57 -0
- package/skills/{oris-loop → oris-flow}/templates/debriefer.md +2 -1
- package/skills/oris-flow/templates/doctor.md +22 -0
- package/skills/{oris-loop → oris-flow}/templates/executor.md +8 -2
- package/skills/{oris-loop → oris-flow}/templates/orchestrator.md +10 -7
- package/skills/{oris-loop → oris-flow}/templates/verifier.md +6 -3
- package/references/questions.md +0 -38
- package/skills/oris-flow-criteria/SKILL.md +0 -49
- package/skills/oris-flow-discover/SKILL.md +0 -58
- package/skills/oris-flow-docs/SKILL.md +0 -31
- package/skills/oris-flow-fix/SKILL.md +0 -42
- package/skills/oris-flow-implement/SKILL.md +0 -43
- package/skills/oris-flow-plan/SKILL.md +0 -51
- package/skills/oris-flow-setup/SKILL.md +0 -49
- package/skills/oris-help/SKILL.md +0 -30
- package/skills/oris-help/agents/openai.yaml +0 -4
- package/skills/oris-loop/SKILL.md +0 -66
- package/skills/oris-loop/agents/openai.yaml +0 -4
- package/skills/oris-loop/references/craft.md +0 -48
- package/skills/oris-loop/references/improve.md +0 -23
- package/skills/oris-loop/references/run.md +0 -30
- package/skills/oris-loop/templates/doctor.md +0 -20
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { buildRoleInvocation, doctorDecisionToState, extractJson } from "../loop/oris-loop-run.mjs";
|
|
3
|
+
import { buildRoleInvocation, doctorDecisionToState, extractJson, verifierVerdictToState } from "../loop/oris-loop-run.mjs";
|
|
4
4
|
import { readTemplate, stripTemplateHeading, substitutePlaceholders } from "../loop/oris-loop-templates.mjs";
|
|
5
5
|
|
|
6
6
|
test("buildRoleInvocation maps roles to codex and claude CLI calls with per-role models", () => {
|
|
@@ -16,21 +16,47 @@ test("buildRoleInvocation maps roles to codex and claude CLI calls with per-role
|
|
|
16
16
|
assert.ok(!inherit.args.includes("--model"), "inherit must not pass an explicit model");
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
test("readonly roles never get write access at the harness level", () => {
|
|
20
|
+
const claude = buildRoleInvocation("claude", { prompt: "check", model: "inherit", root: "/repo", readonly: true });
|
|
21
|
+
assert.ok(claude.args.includes("plan"), "claude readonly roles run in plan permission mode");
|
|
22
|
+
assert.ok(!claude.args.includes("acceptEdits"));
|
|
23
|
+
|
|
24
|
+
const codex = buildRoleInvocation("codex", { prompt: "check", model: "inherit", root: "/repo", readonly: true });
|
|
25
|
+
assert.ok(codex.args.includes("read-only"), "codex readonly roles run in the read-only sandbox");
|
|
26
|
+
assert.ok(!codex.args.includes("workspace-write"));
|
|
27
|
+
});
|
|
28
|
+
|
|
19
29
|
test("extractJson pulls the last valid JSON object from noisy output", () => {
|
|
20
30
|
const output = 'thinking...\n{"broken": \nand then {"role":"doctor","decision":"complete"} trailing';
|
|
21
31
|
assert.deepEqual(extractJson(output), { role: "doctor", decision: "complete" });
|
|
22
32
|
assert.equal(extractJson("no json here"), null);
|
|
23
33
|
});
|
|
24
34
|
|
|
25
|
-
test("
|
|
35
|
+
test("extractJson prefers the fenced json block over stray braces", () => {
|
|
36
|
+
const output = 'prose with {braces} everywhere\n```json\n{"role":"verifier","state":"pass","goalMet":true}\n```\nmore {noise}';
|
|
37
|
+
assert.deepEqual(extractJson(output), { role: "verifier", state: "pass", goalMet: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("doctorDecisionToState maps the canonical decision enum to loop states", () => {
|
|
26
41
|
assert.equal(doctorDecisionToState("complete"), "complete");
|
|
27
42
|
assert.equal(doctorDecisionToState("continue"), "continue");
|
|
28
43
|
assert.equal(doctorDecisionToState("retry"), "continue");
|
|
29
44
|
assert.equal(doctorDecisionToState("advance"), "advance");
|
|
45
|
+
assert.equal(doctorDecisionToState("ask-user"), "ask-user", "ask-user must pause, never fail");
|
|
46
|
+
assert.equal(doctorDecisionToState("propose-patch"), "ask-user", "a proposed patch awaits approval");
|
|
30
47
|
assert.equal(doctorDecisionToState("stop"), "blocked");
|
|
31
48
|
assert.equal(doctorDecisionToState(undefined), "blocked");
|
|
32
49
|
});
|
|
33
50
|
|
|
51
|
+
test("verifierVerdictToState drives two-role loops without a doctor", () => {
|
|
52
|
+
assert.equal(verifierVerdictToState({ state: "pass", goalMet: true }), "complete");
|
|
53
|
+
assert.equal(verifierVerdictToState({ state: "pass", goalMet: false }), "continue");
|
|
54
|
+
assert.equal(verifierVerdictToState({ state: "fail" }), "continue");
|
|
55
|
+
assert.equal(verifierVerdictToState({ state: "blocked" }), "ask-user");
|
|
56
|
+
assert.equal(verifierVerdictToState({ state: "inconclusive" }), "ask-user");
|
|
57
|
+
assert.equal(verifierVerdictToState(null), "ask-user", "unparseable verifier output pauses the loop");
|
|
58
|
+
});
|
|
59
|
+
|
|
34
60
|
test("orchestrator template resolves from the skill templates and substitutes placeholders", () => {
|
|
35
61
|
const template = readTemplate("orchestrator", import.meta.url);
|
|
36
62
|
assert.match(template, /ORIS LOOP RUN/);
|
|
@@ -27,7 +27,7 @@ test("same-chat verify passes on temporary repository", () => {
|
|
|
27
27
|
assert.match(result.stdout, /Same-chat verification passed\./);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test("phase advance
|
|
30
|
+
test("phase advance moves through declared phases", () => {
|
|
31
31
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-advance-"));
|
|
32
32
|
try {
|
|
33
33
|
const task = initSmokeRepository(root, {
|
|
@@ -36,7 +36,7 @@ test("phase advance does not consume repair-cycle limit", () => {
|
|
|
36
36
|
phases: ["test", "fix", "clean"],
|
|
37
37
|
});
|
|
38
38
|
const chat = path.join(scriptsRoot, "oris-loop-chat.mjs");
|
|
39
|
-
const common = ["--repository-root", root, "--task", task.taskPath
|
|
39
|
+
const common = ["--repository-root", root, "--task", task.taskPath];
|
|
40
40
|
const start = spawnSync(process.execPath, [chat, "--action", "start", ...common], { encoding: "utf8" });
|
|
41
41
|
assert.equal(start.status, 0, `${start.stdout}\n${start.stderr}`);
|
|
42
42
|
|
|
@@ -45,6 +45,7 @@ test("phase advance does not consume repair-cycle limit", () => {
|
|
|
45
45
|
chat,
|
|
46
46
|
"--action", "set-state",
|
|
47
47
|
"--state", "advance",
|
|
48
|
+
"--progress", "yes",
|
|
48
49
|
"--repository-root", root,
|
|
49
50
|
], { encoding: "utf8" });
|
|
50
51
|
assert.equal(advance.status, 0, `${advance.stdout}\n${advance.stderr}`);
|
|
@@ -53,13 +54,45 @@ test("phase advance does not consume repair-cycle limit", () => {
|
|
|
53
54
|
const state = JSON.parse(fs.readFileSync(path.join(root, ORIS_FLOW_RUNTIME, "chat-active.json"), "utf8"));
|
|
54
55
|
assert.equal(state.active, true);
|
|
55
56
|
assert.equal(state.state, "advance");
|
|
56
|
-
assert.equal(state.
|
|
57
|
+
assert.equal(state.noProgressStreak, 0);
|
|
58
|
+
assert.deepEqual(state.roles, ["executor", "verifier"]);
|
|
57
59
|
assert.equal(state.phaseName, "clean");
|
|
58
60
|
} finally {
|
|
59
61
|
fs.rmSync(root, { recursive: true, force: true });
|
|
60
62
|
}
|
|
61
63
|
});
|
|
62
64
|
|
|
65
|
+
test("no-progress passes force the loop to blocked", () => {
|
|
66
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-noprog-"));
|
|
67
|
+
try {
|
|
68
|
+
const task = initSmokeRepository(root, {
|
|
69
|
+
taskSlug: "noprog-smoke",
|
|
70
|
+
program: "noprog-smoke",
|
|
71
|
+
});
|
|
72
|
+
const chat = path.join(scriptsRoot, "oris-loop-chat.mjs");
|
|
73
|
+
const start = spawnSync(process.execPath, [
|
|
74
|
+
chat, "--action", "start", "--repository-root", root, "--task", task.taskPath,
|
|
75
|
+
], { encoding: "utf8" });
|
|
76
|
+
assert.equal(start.status, 0, `${start.stdout}\n${start.stderr}`);
|
|
77
|
+
|
|
78
|
+
// maxNoProgress is 2 in the smoke loop document.
|
|
79
|
+
for (let index = 0; index < 2; index += 1) {
|
|
80
|
+
const step = spawnSync(process.execPath, [
|
|
81
|
+
chat, "--action", "set-state", "--state", "continue", "--progress", "no", "--repository-root", root,
|
|
82
|
+
], { encoding: "utf8" });
|
|
83
|
+
assert.equal(step.status, 0, `${step.stdout}\n${step.stderr}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const state = JSON.parse(fs.readFileSync(path.join(root, ORIS_FLOW_RUNTIME, "chat-active.json"), "utf8"));
|
|
87
|
+
assert.equal(state.state, "blocked");
|
|
88
|
+
assert.equal(state.active, false);
|
|
89
|
+
assert.equal(state.stopReason, "no-progress limit exhausted");
|
|
90
|
+
assert.equal(state.noProgressStreak, 2);
|
|
91
|
+
} finally {
|
|
92
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
63
96
|
test("same-chat start accepts an existing loop slug", () => {
|
|
64
97
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-slug-"));
|
|
65
98
|
try {
|
|
@@ -52,11 +52,12 @@ function activeState(overrides = {}) {
|
|
|
52
52
|
doctor: "model-d",
|
|
53
53
|
debriefer: "inherit",
|
|
54
54
|
},
|
|
55
|
+
roles: ["executor", "verifier", "doctor"],
|
|
55
56
|
conversationId: null,
|
|
56
57
|
iteration: 0,
|
|
57
58
|
maxIterations: 5,
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
noProgressStreak: 0,
|
|
60
|
+
maxNoProgress: 2,
|
|
60
61
|
maxMinutes: 30,
|
|
61
62
|
startedAt: new Date().toISOString(),
|
|
62
63
|
...overrides,
|
|
@@ -91,13 +92,15 @@ test("cursor: claims the conversation and injects the orchestrator prompt", () =
|
|
|
91
92
|
assert.match(message, /Definition: \.oris-flow\/loops\/sample\/loop\.md/);
|
|
92
93
|
assert.match(message, /Prompts: \.oris-flow\/loops\/sample\/prompts/);
|
|
93
94
|
assert.match(message, /executor=model-e verifier=model-v doctor=model-d debriefer=inherit/);
|
|
95
|
+
assert.match(message, /Active roles: executor, verifier, doctor/);
|
|
94
96
|
assert.match(message, /Improve mode: auto/);
|
|
95
97
|
assert.match(message, /ROLE: you are the orchestrator only/);
|
|
96
98
|
assert.match(message, /SPAWN executor subagent/);
|
|
97
99
|
assert.match(message, /SPAWN verifier subagent/);
|
|
98
100
|
assert.match(message, /SPAWN doctor subagent/);
|
|
99
|
-
assert.match(message, /oris-skills loop chat --action set-state --state/);
|
|
100
|
-
assert.match(message,
|
|
101
|
+
assert.match(message, /npx oris-skills loop chat --action set-state --state/);
|
|
102
|
+
assert.match(message, /--progress <yes\|no>/);
|
|
103
|
+
assert.match(message, /ORIS_LOOP_STATE: <continue\|advance\|complete\|blocked\|ask-user>/);
|
|
101
104
|
assert.doesNotMatch(message, /\{\{iteration\}\}/, "runtime placeholders must be substituted");
|
|
102
105
|
assert.equal(result.state.conversationId, "c1");
|
|
103
106
|
assert.equal(result.state.iteration, 1);
|
|
@@ -141,6 +144,23 @@ test("claude: platform is auto-detected from Stop payload", () => {
|
|
|
141
144
|
}
|
|
142
145
|
});
|
|
143
146
|
|
|
147
|
+
test("cursor: auto-detected even when the payload also carries session_id", () => {
|
|
148
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
|
|
149
|
+
try {
|
|
150
|
+
// Real Cursor payloads include BOTH conversation_id and session_id.
|
|
151
|
+
const result = runHook(
|
|
152
|
+
root,
|
|
153
|
+
{ conversation_id: "c9", session_id: "s9", status: "completed", loop_count: 0 },
|
|
154
|
+
activeState(),
|
|
155
|
+
);
|
|
156
|
+
assert.ok(result.output.followup_message, "must answer in Cursor format");
|
|
157
|
+
assert.equal(result.output.decision, undefined);
|
|
158
|
+
assert.equal(result.state.conversationId, "c9");
|
|
159
|
+
} finally {
|
|
160
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
144
164
|
test("per-loop prompts/orchestrator.md overrides the default template", () => {
|
|
145
165
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
|
|
146
166
|
try {
|
|
@@ -214,6 +234,47 @@ test("stops on terminal state and exhausted limits", () => {
|
|
|
214
234
|
);
|
|
215
235
|
assert.deepEqual(expired.output, {});
|
|
216
236
|
assert.equal(expired.state.state, "blocked");
|
|
237
|
+
|
|
238
|
+
const stalled = runHook(
|
|
239
|
+
root,
|
|
240
|
+
{ conversation_id: "c1", status: "completed", loop_count: 0 },
|
|
241
|
+
activeState({ conversationId: "c1", noProgressStreak: 2, maxNoProgress: 2 }),
|
|
242
|
+
);
|
|
243
|
+
assert.deepEqual(stalled.output, {});
|
|
244
|
+
assert.equal(stalled.state.state, "blocked");
|
|
245
|
+
assert.equal(stalled.state.stopReason, "no-progress limit exhausted");
|
|
246
|
+
} finally {
|
|
247
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("claude: stopping on an exhausted limit tells the user via systemMessage", () => {
|
|
252
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
|
|
253
|
+
try {
|
|
254
|
+
const result = runHook(
|
|
255
|
+
root,
|
|
256
|
+
{ session_id: "s1", hook_event_name: "Stop", stop_hook_active: false, cwd: root },
|
|
257
|
+
activeState({ conversationId: "s1", iteration: 5, maxIterations: 5 }),
|
|
258
|
+
["--platform", "claude"],
|
|
259
|
+
);
|
|
260
|
+
assert.match(result.output.systemMessage, /iteration limit exhausted/);
|
|
261
|
+
assert.equal(result.output.decision, undefined, "must not block the chat");
|
|
262
|
+
assert.equal(result.state.state, "blocked");
|
|
263
|
+
} finally {
|
|
264
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("ask-user pauses the loop: the hook does not resume it", () => {
|
|
269
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
|
|
270
|
+
try {
|
|
271
|
+
const result = runHook(
|
|
272
|
+
root,
|
|
273
|
+
{ conversation_id: "c1", status: "completed", loop_count: 0 },
|
|
274
|
+
activeState({ state: "ask-user", conversationId: "c1" }),
|
|
275
|
+
);
|
|
276
|
+
assert.deepEqual(result.output, {});
|
|
277
|
+
assert.equal(result.state.state, "ask-user", "pause must not be rewritten");
|
|
217
278
|
} finally {
|
|
218
279
|
fs.rmSync(root, { recursive: true, force: true });
|
|
219
280
|
}
|
|
@@ -28,7 +28,7 @@ function runCli(args, homeDir) {
|
|
|
28
28
|
|
|
29
29
|
test("plugin manifest skills all exist and match front-matter names", () => {
|
|
30
30
|
const manifest = JSON.parse(read(".cursor-plugin/plugin.json"));
|
|
31
|
-
assert.
|
|
31
|
+
assert.equal(manifest.skills.length, 1, "one user-facing skill");
|
|
32
32
|
for (const entry of manifest.skills) {
|
|
33
33
|
const relative = entry.replace(/^[.\\/]+/, "");
|
|
34
34
|
const skillFile = path.join(relative, "SKILL.md");
|
|
@@ -40,8 +40,8 @@ test("plugin manifest skills all exist and match front-matter names", () => {
|
|
|
40
40
|
|
|
41
41
|
test("oris-flow route table destinations exist and routing stays in-chat", () => {
|
|
42
42
|
const text = read("skills/oris-flow/SKILL.md");
|
|
43
|
-
const destinations = [...text.matchAll(/`(skills\/[a-z-]
|
|
44
|
-
assert.ok(destinations.length >=
|
|
43
|
+
const destinations = [...text.matchAll(/`(skills\/oris-flow\/references\/[a-z-]+\.md)`/g)].map((match) => match[1]);
|
|
44
|
+
assert.ok(destinations.length >= 13, "route table must list at least 13 destinations");
|
|
45
45
|
for (const destination of destinations) assert.equal(exists(destination), true, destination);
|
|
46
46
|
assert.deepEqual(destinations, [...new Set(destinations)], "route destinations must be unique");
|
|
47
47
|
assert.match(text, /NEVER tell the user to type another command/i);
|
|
@@ -54,14 +54,13 @@ test("shared conventions centralize question, language, and gate rules", () => {
|
|
|
54
54
|
assert.match(conventions, /ONE question per turn/i);
|
|
55
55
|
assert.match(conventions, /Explain the options.*Spiega le opzioni/s);
|
|
56
56
|
assert.match(conventions, /NEVER write documents, code, settings, Git, or DevOps without explicit user confirmation/i);
|
|
57
|
-
|
|
58
|
-
assert.match(
|
|
59
|
-
assert.match(
|
|
60
|
-
assert.match(
|
|
61
|
-
assert.match(
|
|
62
|
-
assert.match(
|
|
63
|
-
assert.
|
|
64
|
-
assert.match(questions, /`Annulla`/);
|
|
57
|
+
assert.match(conventions, /`Basta domande — procedi`/);
|
|
58
|
+
assert.match(conventions, /`Genera il documento`/);
|
|
59
|
+
assert.match(conventions, /`Continua l'intervista`/);
|
|
60
|
+
assert.match(conventions, /`Approva documento`/);
|
|
61
|
+
assert.match(conventions, /`Annulla`/);
|
|
62
|
+
assert.match(conventions, /oris-flow-criteria.*writes first/i, "the criteria write-first exception must live where skills read it");
|
|
63
|
+
assert.equal(exists("references/questions.md"), false, "questions.md was merged into conventions.md");
|
|
65
64
|
});
|
|
66
65
|
|
|
67
66
|
test("every skill points at conventions instead of repeating shared rules", () => {
|
|
@@ -74,10 +73,10 @@ test("every skill points at conventions instead of repeating shared rules", () =
|
|
|
74
73
|
});
|
|
75
74
|
|
|
76
75
|
test("loop skill covers craft, demo, run, audit, improve and editable prompt files", () => {
|
|
77
|
-
const skill = read("skills/oris-loop
|
|
78
|
-
assert.match(skill, /references\/craft\.md/);
|
|
79
|
-
assert.match(skill, /references\/run\.md/);
|
|
80
|
-
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/);
|
|
81
80
|
assert.match(skill, /oris-skills loop demo/);
|
|
82
81
|
assert.match(skill, /oris-skills loop dry-run/);
|
|
83
82
|
assert.match(skill, /oris-skills loop list --json/);
|
|
@@ -85,10 +84,10 @@ test("loop skill covers craft, demo, run, audit, improve and editable prompt fil
|
|
|
85
84
|
assert.match(skill, /improve\.mode: auto/);
|
|
86
85
|
assert.match(skill, /Cursor|Claude Code|Codex/);
|
|
87
86
|
for (const reference of ["craft", "run", "improve"]) {
|
|
88
|
-
assert.equal(exists(`skills/oris-
|
|
87
|
+
assert.equal(exists(`skills/oris-flow/references/loop-${reference}.md`), true, reference);
|
|
89
88
|
}
|
|
90
89
|
for (const template of ["orchestrator", "executor", "verifier", "doctor", "debriefer"]) {
|
|
91
|
-
assert.equal(exists(`skills/oris-
|
|
90
|
+
assert.equal(exists(`skills/oris-flow/templates/${template}.md`), true, template);
|
|
92
91
|
}
|
|
93
92
|
});
|
|
94
93
|
|
|
@@ -100,23 +99,26 @@ test("loop contract stays subagent-first with per-role editable prompts", () =>
|
|
|
100
99
|
assert.match(contract, /improve/);
|
|
101
100
|
assert.match(contract, /inherit/);
|
|
102
101
|
assert.match(contract, /NEVER success/i);
|
|
103
|
-
const run = read("skills/oris-
|
|
102
|
+
const run = read("skills/oris-flow/references/loop-run.md");
|
|
104
103
|
assert.match(run, /oris-skills loop chat --action start --loop <slug>/);
|
|
105
104
|
assert.match(run, /NEVER do executor\/verifier\/doctor work in the main chat/i);
|
|
106
105
|
assert.match(run, /set-state blocked/);
|
|
107
106
|
assert.match(run, /oris-skills loop run --loop <slug> --agent codex\|claude/);
|
|
108
|
-
const craft = read("skills/oris-
|
|
109
|
-
assert.match(craft, /skills\/oris-
|
|
107
|
+
const craft = read("skills/oris-flow/references/loop-craft.md");
|
|
108
|
+
assert.match(craft, /skills\/oris-flow\/templates\//);
|
|
110
109
|
assert.match(craft, /write the loop \| edit verifier \| edit executor \| edit doctor/);
|
|
111
|
-
|
|
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-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-flow/references/loop.md")]) {
|
|
116
|
+
assert.doesNotMatch(text, /ORIS LOOP ACTIVE/, "the gate literal lives in the contract and executor prompts only");
|
|
115
117
|
}
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
test("setup and repo-map keep the installed CLI as the command surface", () => {
|
|
119
|
-
const setup = read("skills/oris-flow
|
|
121
|
+
const setup = read("skills/oris-flow/references/setup.md");
|
|
120
122
|
assert.match(setup, /oris-skills flow scan --repository-root <repo> --json/);
|
|
121
123
|
assert.match(setup, /oris-skills loop bootstrap --repository-root <repo>/);
|
|
122
124
|
assert.match(setup, /oris-skills flow clean-runtime --repository-root <repo> --dry-run/);
|
|
@@ -179,3 +181,16 @@ test("CLI exposes install, loop learnability, and flow helper commands", () => {
|
|
|
179
181
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
180
182
|
}
|
|
181
183
|
});
|
|
184
|
+
|
|
185
|
+
test("copilot install is planned alongside the universal pointer", () => {
|
|
186
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-copilot-"));
|
|
187
|
+
try {
|
|
188
|
+
const result = runCli(["install", "--dry-run", "--agents", "copilot"], homeDir);
|
|
189
|
+
assert.equal(result.status, 0, result.stderr);
|
|
190
|
+
assert.match(result.stdout, /Copilot prompts/);
|
|
191
|
+
assert.match(result.stdout, /universal pointer/);
|
|
192
|
+
assert.match(result.stdout, /oris-flow\.md/);
|
|
193
|
+
} finally {
|
|
194
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
// The Oris skill style, enforced: front matter that routes, leading-word moves,
|
|
8
|
+
// an explicit Never block, a Done when checklist, and a hard line budget.
|
|
9
|
+
// One user-facing skill exists; every route is one of its references.
|
|
10
|
+
|
|
11
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
12
|
+
const skillsRoot = path.join(repoRoot, "skills");
|
|
13
|
+
const MAX_LINES = 110;
|
|
14
|
+
|
|
15
|
+
const skillDirs = fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
16
|
+
.filter((entry) => entry.isDirectory())
|
|
17
|
+
.map((entry) => entry.name);
|
|
18
|
+
|
|
19
|
+
function resolveHandles(text, label, localRoot) {
|
|
20
|
+
for (const match of text.matchAll(/`(?:~\/\.oris\/oris-skills\/)?((?:skills|references)\/[A-Za-z0-9/._-]+\.md)`/g)) {
|
|
21
|
+
const target = path.join(repoRoot, match[1]);
|
|
22
|
+
const local = path.join(localRoot, match[1]);
|
|
23
|
+
assert.ok(fs.existsSync(target) || fs.existsSync(local), `${label}: routing handle resolves: ${match[1]}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("exactly one user-facing skill: oris-flow", () => {
|
|
28
|
+
assert.deepEqual(skillDirs, ["oris-flow"], "users see one skill; routes are its references");
|
|
29
|
+
assert.ok(fs.existsSync(path.join(skillsRoot, "oris-flow", "SKILL.md")));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("skill style: oris-flow", () => {
|
|
33
|
+
const text = fs.readFileSync(path.join(skillsRoot, "oris-flow", "SKILL.md"), "utf8");
|
|
34
|
+
const front = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
35
|
+
assert.ok(front, "has front matter");
|
|
36
|
+
assert.match(front[1], /^name: oris-flow$/m, "front matter name matches the folder");
|
|
37
|
+
const description = front[1].match(/^description: (.+)$/m)?.[1];
|
|
38
|
+
assert.ok(description, "front matter has description");
|
|
39
|
+
assert.match(description, /Use for/, "description names its triggers");
|
|
40
|
+
assert.doesNotMatch(front[1], /disable-model-invocation/, "the single entry point must stay model-invocable");
|
|
41
|
+
assert.ok(text.includes("## Never"), "carries an explicit Never block");
|
|
42
|
+
assert.ok(text.includes("## Done when"), "ends with a Done when checklist");
|
|
43
|
+
const lines = text.split(/\r?\n/).length;
|
|
44
|
+
assert.ok(lines <= MAX_LINES, `SKILL.md is ${lines} lines (max ${MAX_LINES}) — move detail to references/`);
|
|
45
|
+
resolveHandles(text, "SKILL.md", path.join(skillsRoot, "oris-flow"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("every route reference carries the style and resolves its handles", () => {
|
|
49
|
+
const skillText = fs.readFileSync(path.join(skillsRoot, "oris-flow", "SKILL.md"), "utf8");
|
|
50
|
+
const routes = [...skillText.matchAll(/`skills\/oris-flow\/references\/([a-z-]+\.md)`/g)].map((match) => match[1]);
|
|
51
|
+
assert.ok(routes.length >= 13, `router table lists all routes (found ${routes.length})`);
|
|
52
|
+
assert.deepEqual(routes, [...new Set(routes)], "route destinations are unique");
|
|
53
|
+
const referencesDir = path.join(skillsRoot, "oris-flow", "references");
|
|
54
|
+
for (const file of routes) {
|
|
55
|
+
const text = fs.readFileSync(path.join(referencesDir, file), "utf8");
|
|
56
|
+
assert.doesNotMatch(text, /^---\r?\n/, `${file}: references carry no front matter`);
|
|
57
|
+
assert.ok(text.includes("## Never"), `${file}: carries an explicit Never block`);
|
|
58
|
+
assert.ok(text.includes("## Done when"), `${file}: ends with a Done when checklist`);
|
|
59
|
+
}
|
|
60
|
+
for (const file of fs.readdirSync(referencesDir)) {
|
|
61
|
+
const text = fs.readFileSync(path.join(referencesDir, file), "utf8");
|
|
62
|
+
resolveHandles(text, file, path.join(skillsRoot, "oris-flow"));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
@@ -1,55 +1,83 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oris-flow
|
|
3
|
-
description: Oris
|
|
3
|
+
description: The one Oris skill - read the intent, pick ONE route, apply it in the same chat. Use for /oris-flow and whenever the user wants to set up a repo for agents, start a project, understand or define behavior, write acceptance criteria, plan, implement, fix a bug, verify criteria, handle a spec change, run a work loop (fix until green, repeat until verified), review architecture, update task docs, or asks how Oris works.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Oris Flow —
|
|
6
|
+
# Oris Flow — the router
|
|
7
7
|
|
|
8
|
-
The only
|
|
8
|
+
The only skill users see. READ the intent, PICK one route, say one line
|
|
9
|
+
("Entering <route>: <why>"), then READ the route's reference file and APPLY it top to
|
|
10
|
+
bottom in the same chat — its Never and Done when bind the whole route.
|
|
11
|
+
NEVER tell the user to type another command; NEVER narrate the reading
|
|
12
|
+
(`references/conventions.md` `## Silence`).
|
|
9
13
|
|
|
10
|
-
RULES:
|
|
14
|
+
RULES: `references/conventions.md`. This router NEVER writes documents, code, Git, or
|
|
15
|
+
DevOps — the route's own reference owns its gates.
|
|
11
16
|
|
|
12
17
|
## Route
|
|
13
18
|
|
|
14
|
-
Match the desired outcome to ONE route:
|
|
19
|
+
Match the desired outcome to ONE route, then apply its reference:
|
|
15
20
|
|
|
16
|
-
| Route | When the user wants… |
|
|
21
|
+
| Route | When the user wants… | Apply |
|
|
17
22
|
|-------|----------------------|-------|
|
|
18
|
-
| setup | prepare / map / refresh
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
| setup | prepare / map / refresh an existing repo for Oris and agents | `skills/oris-flow/references/setup.md` |
|
|
24
|
+
| new | bootstrap a project from scratch, greenfield, empty directory | `skills/oris-flow/references/new.md` |
|
|
25
|
+
| discover | understand or define product behavior, functional analysis | `skills/oris-flow/references/discover.md` |
|
|
26
|
+
| criteria | acceptance criteria, QA checks, testable scenarios | `skills/oris-flow/references/criteria.md` |
|
|
27
|
+
| plan | a technical implementation plan before coding | `skills/oris-flow/references/plan.md` |
|
|
28
|
+
| implement | build from a plan or confirmed scope | `skills/oris-flow/references/implement.md` |
|
|
29
|
+
| fix | broken behavior, a regression, a failed QA check | `skills/oris-flow/references/fix.md` |
|
|
30
|
+
| verify | check acceptance criteria against the real product, once | `skills/oris-flow/references/verify.md` |
|
|
31
|
+
| change | the spec changed — update analysis, criteria, plan, and history together | `skills/oris-flow/references/change.md` |
|
|
32
|
+
| loop | repeat work until verified — fix until green, step through a plan | `skills/oris-flow/references/loop.md` |
|
|
33
|
+
| architecture | review architecture, find deepening/refactor opportunities | `skills/oris-flow/references/architecture.md` |
|
|
34
|
+
| docs | update task docs after a change | `skills/oris-flow/references/docs.md` |
|
|
35
|
+
| help | how Oris works, install, update, troubleshooting | `skills/oris-flow/references/help.md` |
|
|
36
|
+
|
|
37
|
+
## Precedence (the calls that look ambiguous)
|
|
38
|
+
|
|
39
|
+
1. INTENT clear → enter the route NOW. Unclear → ONE menu question (below); NEVER guess
|
|
40
|
+
between two routes and never ask twice.
|
|
41
|
+
2. RETHINK beats build: analyze, question, or reshape existing work →
|
|
42
|
+
route to the interview owner (discover / plan / change), NEVER straight to implement.
|
|
43
|
+
3. CHANGED spec on a feature that already has task docs or code → change, not discover —
|
|
44
|
+
change owns the delta and the history.
|
|
45
|
+
4. BROKEN behavior → fix. NEW planned work → implement. The menu answer "Implement or
|
|
46
|
+
fix" gets exactly one follow-up question to split them.
|
|
47
|
+
5. CHECK once → verify. Repeat-until-verified → loop.
|
|
48
|
+
6. EMPTY directory or from-scratch intent → new. Existing repo without
|
|
49
|
+
`.oris-flow/manifest.json` → recommend setup first; respect the user's choice.
|
|
34
50
|
|
|
35
51
|
## Menu (low confidence)
|
|
36
52
|
|
|
37
53
|
ASK "What do you want to do?" localized to `uiLanguage`, options:
|
|
38
54
|
|
|
39
55
|
- Setup repository
|
|
56
|
+
- New project from scratch
|
|
40
57
|
- Understand / define behavior (discovery)
|
|
41
58
|
- Acceptance criteria
|
|
42
59
|
- Technical plan
|
|
43
60
|
- Implement or fix
|
|
61
|
+
- Verify acceptance criteria
|
|
62
|
+
- Spec changed: update feature docs
|
|
44
63
|
- Loop: repeat until verified
|
|
64
|
+
- Architecture review
|
|
45
65
|
- Update docs
|
|
46
66
|
- Help
|
|
47
67
|
- Explain the options / Spiega le opzioni
|
|
48
68
|
|
|
49
|
-
IF "Implement or fix" → one follow-up: new planned work (implement) or broken behavior (fix)?
|
|
50
69
|
IF "Explain the options" → explain briefly, re-ask the same question.
|
|
70
|
+
Every other label maps 1:1 to its route in the table above.
|
|
71
|
+
|
|
72
|
+
## Never
|
|
73
|
+
|
|
74
|
+
- Force a route onto a plain question — no flow intent means no menu: just answer.
|
|
75
|
+
- Write anything — the route's reference owns every gate.
|
|
76
|
+
- Hand the user a command or skill name to type — enter the route yourself.
|
|
77
|
+
- Route to implement while the plan itself is in question.
|
|
78
|
+
- Keep interviewing here: one menu question maximum, then enter.
|
|
51
79
|
|
|
52
80
|
## Done when
|
|
53
81
|
|
|
54
|
-
- [ ] One route entered in the same chat, or the menu was asked.
|
|
55
|
-
- [ ] No writes happened before the
|
|
82
|
+
- [ ] One route entered and applied in the same chat, or the menu was asked exactly once.
|
|
83
|
+
- [ ] No writes happened before the route's own gate.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Architecture Review
|
|
2
|
+
|
|
3
|
+
Surface architectural friction and propose deepening opportunities — refactors that turn
|
|
4
|
+
shallow modules into deep ones. The aim is testability and AI-navigability.
|
|
5
|
+
|
|
6
|
+
RULES: `references/conventions.md`. Output is a report + an interview; implementation
|
|
7
|
+
routes to plan/implement.
|
|
8
|
+
|
|
9
|
+
## Vocabulary (use these words exactly)
|
|
10
|
+
|
|
11
|
+
- **module** — a unit with an interface and an implementation. Never "component",
|
|
12
|
+
"service", "layer".
|
|
13
|
+
- **interface** — what callers must know. Never "API", "signature".
|
|
14
|
+
- **deep / shallow** — deep: small interface, big implementation. Shallow: interface
|
|
15
|
+
nearly as complex as the implementation.
|
|
16
|
+
- **seam** — where two modules meet. Never "boundary". One adapter = hypothetical seam;
|
|
17
|
+
two = real.
|
|
18
|
+
- **locality** — bugs concentrate where the behavior lives; **leverage** — one interface
|
|
19
|
+
serves N call sites.
|
|
20
|
+
- **deletion test** — would deleting the module concentrate complexity (good signal) or
|
|
21
|
+
just move it?
|
|
22
|
+
|
|
23
|
+
## 1. Explore
|
|
24
|
+
|
|
25
|
+
1. READ first: setup map, `CONTEXT.md` (domain names for good seams), `docs/adr/`
|
|
26
|
+
(decisions NOT to re-litigate).
|
|
27
|
+
2. WALK the codebase with parallel subagents. No rigid heuristics — note real friction:
|
|
28
|
+
- understanding one concept requires bouncing between many small modules;
|
|
29
|
+
- shallow modules (deletion test says "concentrates");
|
|
30
|
+
- pure functions extracted for testability while the bugs hide in how they're called;
|
|
31
|
+
- tight coupling leaking across seams;
|
|
32
|
+
- untested code, or code untestable through its current interface.
|
|
33
|
+
|
|
34
|
+
## 2. Report (HTML, outside the repo)
|
|
35
|
+
|
|
36
|
+
WRITE a self-contained HTML file to the OS temp dir (`architecture-review-<timestamp>.html`),
|
|
37
|
+
OPEN it (`start` | `open` | `xdg-open`), TELL the absolute path.
|
|
38
|
+
|
|
39
|
+
FORMAT: Tailwind CDN + Mermaid CDN. Per candidate, one card:
|
|
40
|
+
- title (names the deepening) + strength badge: `Strong` | `Worth exploring` | `Speculative`;
|
|
41
|
+
- files (monospaced), Problem (one sentence), Solution (one sentence);
|
|
42
|
+
- Before/After diagram side by side — Mermaid for graph-shaped, hand-built divs/SVG for
|
|
43
|
+
mass diagrams; if the diagram needs a paragraph, redraw the diagram;
|
|
44
|
+
- wins as bullets ≤6 words, glossary terms only — never "cleaner code";
|
|
45
|
+
- ADR conflict → amber callout ("contradicts ADR-0007 — worth reopening because…").
|
|
46
|
+
|
|
47
|
+
END with Top recommendation: one candidate, one sentence why.
|
|
48
|
+
ASK: "Which of these would you like to explore?" — DO NOT propose interfaces yet.
|
|
49
|
+
|
|
50
|
+
## 3. Walk the picked candidate
|
|
51
|
+
|
|
52
|
+
INTERVIEW per conventions (one question per turn, recommended answer first):
|
|
53
|
+
constraints, dependencies, the shape of the deepened module, what sits behind the seam,
|
|
54
|
+
which tests survive.
|
|
55
|
+
|
|
56
|
+
SIDE EFFECTS as decisions crystallize:
|
|
57
|
+
- new concept named → add the term to `CONTEXT.md` (create lazily);
|
|
58
|
+
- fuzzy term sharpened → update `CONTEXT.md` in place;
|
|
59
|
+
- candidate rejected for a load-bearing reason → offer an ADR so future reviews don't
|
|
60
|
+
re-suggest it (skip ephemeral reasons).
|
|
61
|
+
|
|
62
|
+
AFTER: offer technical plan for the deepening (`skills/oris-flow/references/plan.md`) |
|
|
63
|
+
explore another candidate | stop.
|
|
64
|
+
|
|
65
|
+
## Never
|
|
66
|
+
|
|
67
|
+
- Edit product code — the deepening routes to plan, then implement.
|
|
68
|
+
- Use non-glossary words: component, service, layer, boundary, "cleaner code".
|
|
69
|
+
- Re-litigate an ADR silently — surface the conflict, let the user reopen it.
|
|
70
|
+
- Write the report inside the repo.
|
|
71
|
+
|
|
72
|
+
## Done when
|
|
73
|
+
|
|
74
|
+
- [ ] CONTEXT.md and ADRs read before exploring; no ADR re-litigated silently.
|
|
75
|
+
- [ ] Report written to temp (repo untouched), opened, path shown.
|
|
76
|
+
- [ ] Glossary terms used exactly; wins named in locality/leverage terms.
|
|
77
|
+
- [ ] Implementation routed to plan, not started here.
|