oris-skills 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.cursor-plugin/plugin.json +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/agents/oris-loop-debriefer.md +29 -0
  5. package/agents/oris-loop-doctor.md +32 -0
  6. package/agents/oris-loop-executor.md +32 -0
  7. package/agents/oris-loop-verifier.md +31 -0
  8. package/docs/architecture.md +26 -0
  9. package/docs/distribution.md +38 -0
  10. package/docs/maintainer-guide.md +32 -0
  11. package/docs/user-guide.md +42 -0
  12. package/package.json +49 -0
  13. package/references/clean-code-checklist.md +107 -0
  14. package/references/conventions.md +39 -0
  15. package/references/doc-policy.md +24 -0
  16. package/references/loop-adapter.schema.json +126 -0
  17. package/references/loop-contract.md +119 -0
  18. package/references/loop.schema.json +143 -0
  19. package/references/questions.md +38 -0
  20. package/references/repo-map.md +53 -0
  21. package/references/repo-map.schema.json +198 -0
  22. package/references/settings.md +35 -0
  23. package/references/settings.schema.json +75 -0
  24. package/scripts/flow/oris-flow-clean-runtime.mjs +182 -0
  25. package/scripts/flow/oris-flow-layout.mjs +53 -0
  26. package/scripts/flow/oris-flow-scan.mjs +350 -0
  27. package/scripts/flow/oris-flow-version-control.mjs +42 -0
  28. package/scripts/flow/oris-gitignore.mjs +79 -0
  29. package/scripts/install/generate-agent-adapters.mjs +74 -0
  30. package/scripts/install/install-user-skills.mjs +271 -0
  31. package/scripts/install/uninstall-user-skills.mjs +163 -0
  32. package/scripts/loop/oris-loop-bootstrap.mjs +383 -0
  33. package/scripts/loop/oris-loop-bundle.mjs +22 -0
  34. package/scripts/loop/oris-loop-chat.mjs +408 -0
  35. package/scripts/loop/oris-loop-demo.mjs +180 -0
  36. package/scripts/loop/oris-loop-document.mjs +179 -0
  37. package/scripts/loop/oris-loop-dry-run.mjs +110 -0
  38. package/scripts/loop/oris-loop-fixtures.mjs +148 -0
  39. package/scripts/loop/oris-loop-list.mjs +81 -0
  40. package/scripts/loop/oris-loop-paths.mjs +232 -0
  41. package/scripts/loop/oris-loop-run.mjs +241 -0
  42. package/scripts/loop/oris-loop-stop.mjs +319 -0
  43. package/scripts/loop/oris-loop-templates.mjs +77 -0
  44. package/scripts/loop/oris-loop-verify.mjs +206 -0
  45. package/scripts/oris-skills.mjs +116 -0
  46. package/scripts/package-oris-skills.mjs +53 -0
  47. package/scripts/tests/run-all-tests.mjs +38 -0
  48. package/scripts/tests/test-agent-adapters.mjs +19 -0
  49. package/scripts/tests/test-oris-1-0-cleanliness.mjs +58 -0
  50. package/scripts/tests/test-oris-flow-clean-runtime.mjs +75 -0
  51. package/scripts/tests/test-oris-flow-scan.mjs +49 -0
  52. package/scripts/tests/test-oris-gitignore.mjs +35 -0
  53. package/scripts/tests/test-oris-loop-bootstrap.mjs +94 -0
  54. package/scripts/tests/test-oris-loop-document.mjs +112 -0
  55. package/scripts/tests/test-oris-loop-list.mjs +74 -0
  56. package/scripts/tests/test-oris-loop-run.mjs +45 -0
  57. package/scripts/tests/test-oris-loop-smoke.mjs +130 -0
  58. package/scripts/tests/test-oris-loop-stop.mjs +371 -0
  59. package/scripts/tests/test-routing-lifecycle.mjs +181 -0
  60. package/scripts/tests/test-schemas.mjs +55 -0
  61. package/skills/oris-flow/SKILL.md +55 -0
  62. package/skills/oris-flow/agents/openai.yaml +4 -0
  63. package/skills/oris-flow-criteria/SKILL.md +49 -0
  64. package/skills/oris-flow-discover/SKILL.md +58 -0
  65. package/skills/oris-flow-docs/SKILL.md +31 -0
  66. package/skills/oris-flow-fix/SKILL.md +42 -0
  67. package/skills/oris-flow-implement/SKILL.md +43 -0
  68. package/skills/oris-flow-plan/SKILL.md +51 -0
  69. package/skills/oris-flow-setup/SKILL.md +49 -0
  70. package/skills/oris-help/SKILL.md +30 -0
  71. package/skills/oris-help/agents/openai.yaml +4 -0
  72. package/skills/oris-loop/SKILL.md +66 -0
  73. package/skills/oris-loop/agents/openai.yaml +4 -0
  74. package/skills/oris-loop/references/craft.md +48 -0
  75. package/skills/oris-loop/references/improve.md +23 -0
  76. package/skills/oris-loop/references/run.md +30 -0
  77. package/skills/oris-loop/templates/debriefer.md +18 -0
  78. package/skills/oris-loop/templates/doctor.md +20 -0
  79. package/skills/oris-loop/templates/executor.md +19 -0
  80. package/skills/oris-loop/templates/orchestrator.md +33 -0
  81. package/skills/oris-loop/templates/verifier.md +21 -0
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ import childProcess from "node:child_process";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { bootstrapLoop } from "./loop/oris-loop-bootstrap.mjs";
8
+
9
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+
11
+ function runNode(script, args, options = {}) {
12
+ const result = childProcess.spawnSync(process.execPath, [path.join(repoRoot, "scripts", script), ...args], {
13
+ cwd: process.cwd(),
14
+ stdio: "inherit",
15
+ shell: false,
16
+ });
17
+ const status = result.status ?? 1;
18
+ if (options.exit === false) return status;
19
+ process.exit(status);
20
+ }
21
+
22
+ function runGitRoot() {
23
+ const result = childProcess.spawnSync("git", ["rev-parse", "--show-toplevel"], {
24
+ cwd: process.cwd(),
25
+ shell: process.platform === "win32",
26
+ encoding: "utf8",
27
+ });
28
+ return result.status === 0 && result.stdout.trim() ? path.resolve(result.stdout.trim()) : process.cwd();
29
+ }
30
+
31
+ function printHelp() {
32
+ console.log(`Oris Skills
33
+
34
+ Install for your agents (Cursor, Claude Code, Codex):
35
+ oris-skills install [--agents cursor,claude,codex] [--dry-run]
36
+ oris-skills uninstall [--dry-run]
37
+ oris-skills reinstall [--dry-run]
38
+
39
+ Learn and test loops:
40
+ oris-skills loop demo create a safe tutorial loop
41
+ oris-skills loop dry-run --loop <slug> preview the next pass, run nothing
42
+ oris-skills loop verify --temp self-check the loop runtime
43
+
44
+ Run and manage loops:
45
+ oris-skills loop list [--json]
46
+ oris-skills loop bootstrap [--platforms cursor,claude]
47
+ oris-skills loop chat --action start --loop <slug>
48
+ oris-skills loop chat --action status|set-state|repair|stop [...]
49
+ oris-skills loop run --loop <slug> --agent codex|claude headless runner
50
+
51
+ Repository tools:
52
+ oris-skills doctor
53
+ oris-skills validate
54
+ oris-skills flow scan [...]
55
+ oris-skills flow version-control [...]
56
+ oris-skills flow clean-runtime [...]
57
+ `);
58
+ }
59
+
60
+ const [command, subcommand, ...rest] = process.argv.slice(2);
61
+ const commandArgs = [subcommand, ...rest].filter(Boolean);
62
+
63
+ if (!command || command === "help" || command === "--help" || command === "-h") {
64
+ printHelp();
65
+ process.exit(0);
66
+ }
67
+
68
+ if (command === "install") {
69
+ runNode("install/install-user-skills.mjs", commandArgs);
70
+ }
71
+
72
+ if (command === "uninstall") {
73
+ runNode("install/uninstall-user-skills.mjs", commandArgs);
74
+ }
75
+
76
+ if (command === "reinstall") {
77
+ const uninstallStatus = runNode("install/uninstall-user-skills.mjs", commandArgs, { exit: false });
78
+ if (uninstallStatus !== 0) process.exit(uninstallStatus);
79
+ runNode("install/install-user-skills.mjs", ["--force", ...commandArgs]);
80
+ }
81
+
82
+ if (command === "doctor") {
83
+ const root = runGitRoot();
84
+ const result = bootstrapLoop(root, { dryRun: true });
85
+ console.log(JSON.stringify({
86
+ repositoryRoot: root,
87
+ ready: result.ready,
88
+ missing: result.before.missing,
89
+ plannedActions: result.actions,
90
+ }, null, 2));
91
+ process.exit(result.ready ? 0 : 1);
92
+ }
93
+
94
+ if (command === "validate") {
95
+ runNode("tests/run-all-tests.mjs", ["--node-only", ...commandArgs]);
96
+ }
97
+
98
+ if (command === "loop") {
99
+ if (subcommand === "list") runNode("loop/oris-loop-list.mjs", rest);
100
+ if (subcommand === "bootstrap") runNode("loop/oris-loop-bootstrap.mjs", rest);
101
+ if (subcommand === "chat") runNode("loop/oris-loop-chat.mjs", rest);
102
+ if (subcommand === "verify") runNode("loop/oris-loop-verify.mjs", rest);
103
+ if (subcommand === "demo") runNode("loop/oris-loop-demo.mjs", rest);
104
+ if (subcommand === "dry-run") runNode("loop/oris-loop-dry-run.mjs", rest);
105
+ if (subcommand === "run") runNode("loop/oris-loop-run.mjs", rest);
106
+ }
107
+
108
+ if (command === "flow") {
109
+ if (subcommand === "scan") runNode("flow/oris-flow-scan.mjs", rest);
110
+ if (subcommand === "version-control") runNode("flow/oris-flow-version-control.mjs", rest);
111
+ if (subcommand === "clean-runtime") runNode("flow/oris-flow-clean-runtime.mjs", rest);
112
+ }
113
+
114
+ console.error(`Unknown command: ${[command, subcommand].filter(Boolean).join(" ")}`);
115
+ printHelp();
116
+ process.exit(1);
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const version = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
10
+
11
+ function parseOut(argv) {
12
+ const arg = argv.find((item) => item.startsWith("--out="));
13
+ if (arg) return path.resolve(arg.slice("--out=".length));
14
+ const index = argv.indexOf("--out");
15
+ if (index >= 0 && argv[index + 1]) return path.resolve(argv[index + 1]);
16
+ return path.join(repoRoot, "dist");
17
+ }
18
+
19
+ function copyRecursive(source, target) {
20
+ if (!fs.existsSync(source)) return;
21
+ const stat = fs.statSync(source);
22
+ if (stat.isDirectory()) {
23
+ fs.mkdirSync(target, { recursive: true });
24
+ for (const entry of fs.readdirSync(source)) {
25
+ copyRecursive(path.join(source, entry), path.join(target, entry));
26
+ }
27
+ return;
28
+ }
29
+ fs.mkdirSync(path.dirname(target), { recursive: true });
30
+ fs.copyFileSync(source, target);
31
+ }
32
+
33
+ const outRoot = parseOut(process.argv.slice(2));
34
+ const packageRoot = path.join(outRoot, `oris-skills-${version}`);
35
+ fs.rmSync(packageRoot, { recursive: true, force: true });
36
+ fs.mkdirSync(packageRoot, { recursive: true });
37
+
38
+ for (const item of [
39
+ ".cursor-plugin",
40
+ "skills",
41
+ "references",
42
+ "agents",
43
+ "docs",
44
+ "scripts",
45
+ "package.json",
46
+ "README.md",
47
+ "CHANGELOG.md",
48
+ "LICENSE",
49
+ ]) {
50
+ copyRecursive(path.join(repoRoot, item), path.join(packageRoot, item));
51
+ }
52
+
53
+ console.log(`Packaged ${packageRoot}`);
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ import childProcess from "node:child_process";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
9
+
10
+ const nodeTestFiles = [
11
+ "scripts/tests/test-schemas.mjs",
12
+ "scripts/tests/test-oris-loop-document.mjs",
13
+ "scripts/tests/test-agent-adapters.mjs",
14
+ "scripts/tests/test-oris-1-0-cleanliness.mjs",
15
+ "scripts/tests/test-oris-flow-scan.mjs",
16
+ "scripts/tests/test-routing-lifecycle.mjs",
17
+ "scripts/tests/test-oris-loop-stop.mjs",
18
+ "scripts/tests/test-oris-loop-smoke.mjs",
19
+ "scripts/tests/test-oris-loop-bootstrap.mjs",
20
+ "scripts/tests/test-oris-flow-clean-runtime.mjs",
21
+ "scripts/tests/test-oris-loop-list.mjs",
22
+ "scripts/tests/test-oris-loop-run.mjs",
23
+ "scripts/tests/test-oris-gitignore.mjs",
24
+ ];
25
+
26
+ function run(command, args, options = {}) {
27
+ console.log(`\n> ${[command, ...args].join(" ")}`);
28
+ const result = childProcess.spawnSync(command, args, {
29
+ cwd: root,
30
+ stdio: "inherit",
31
+ ...options,
32
+ });
33
+ if (result.status !== 0) process.exit(result.status ?? 1);
34
+ }
35
+
36
+ const nodeOnly = process.argv.includes("--node-only");
37
+ run(process.execPath, ["--test", ...nodeTestFiles]);
38
+
@@ -0,0 +1,19 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import assert from "node:assert/strict";
5
+ import test from "node:test";
6
+ import { generateAgentAdapters } from "../install/generate-agent-adapters.mjs";
7
+
8
+ test("generates Cursor, Claude, and Codex agent adapters", () => {
9
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "oris-agent-adapters-"));
10
+ try {
11
+ const generated = generateAgentAdapters(temp);
12
+ assert.ok(generated.includes("oris-loop-executor"));
13
+ assert.ok(fs.existsSync(path.join(temp, ".cursor", "agents", "oris-loop-executor.md")));
14
+ assert.ok(fs.existsSync(path.join(temp, ".claude", "agents", "oris-loop-verifier.md")));
15
+ assert.ok(fs.existsSync(path.join(temp, ".codex", "agents", "oris-loop-doctor.toml")));
16
+ } finally {
17
+ fs.rmSync(temp, { recursive: true, force: true });
18
+ }
19
+ });
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import assert from "node:assert/strict";
4
+ import test from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
8
+ const ignoredDirs = new Set([".git", "node_modules", ".cursor", ".claude", ".codex", "dist", ".cursor-plugin-install-backups", ".oris-flow"]);
9
+
10
+ function walk(dir, matches = []) {
11
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
12
+ if (ignoredDirs.has(entry.name)) continue;
13
+ const full = path.join(dir, entry.name);
14
+ if (entry.isDirectory()) walk(full, matches);
15
+ else matches.push(full);
16
+ }
17
+ return matches;
18
+ }
19
+
20
+ function repoFiles() {
21
+ return walk(repoRoot).map((filePath) => ({
22
+ filePath,
23
+ relative: path.relative(repoRoot, filePath).replace(/\\/g, "/"),
24
+ }));
25
+ }
26
+
27
+ test("repository contains no PowerShell scripts", () => {
28
+ const psFiles = repoFiles().filter((entry) => entry.relative.endsWith(".ps1"));
29
+ assert.deepEqual(psFiles.map((entry) => entry.relative), []);
30
+ });
31
+
32
+ test("Oris public loop text does not reference external loop brands", () => {
33
+ const offenders = repoFiles()
34
+ .filter((entry) => /\.(md|mjs|json|yml|yaml|cmd|sh|command)$/.test(entry.relative))
35
+ .filter((entry) => entry.relative !== "scripts/tests/test-oris-1-0-cleanliness.mjs")
36
+ .filter((entry) => entry.relative !== "CHANGELOG.md")
37
+ .filter((entry) => /loopy/i.test(fs.readFileSync(entry.filePath, "utf8")))
38
+ .map((entry) => entry.relative);
39
+ assert.deepEqual(offenders, []);
40
+ });
41
+
42
+ test("Oris Loop 1.0 does not use global preset storage", () => {
43
+ const offenders = repoFiles()
44
+ .filter((entry) => /\.(md|mjs|json|yml|yaml|cmd|sh|command)$/.test(entry.relative))
45
+ .filter((entry) => entry.relative !== "scripts/tests/test-oris-1-0-cleanliness.mjs")
46
+ .filter((entry) => /ORIS_FLOW_USER_PRESETS|~\/\.cursor\/oris-flow|oris-flow[\\/]presets/i.test(fs.readFileSync(entry.filePath, "utf8")))
47
+ .map((entry) => entry.relative);
48
+ assert.deepEqual(offenders, []);
49
+ });
50
+
51
+ test("Oris Loop does not ship or install loop libraries", () => {
52
+ const offenders = repoFiles()
53
+ .filter((entry) => /\.(md|mjs|json|yml|yaml|cmd|sh|command)$/.test(entry.relative))
54
+ .filter((entry) => entry.relative !== "scripts/tests/test-oris-1-0-cleanliness.mjs")
55
+ .filter((entry) => /\.oris-flow[\\/]library|flow[\\/]oris[\\/]library|oris-loop-library|ORIS_FLOW_(?:SOFTWARE_|CUSTOM_)?LIBRARY/i.test(fs.readFileSync(entry.filePath, "utf8")))
56
+ .map((entry) => entry.relative);
57
+ assert.deepEqual(offenders, []);
58
+ });
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { cleanFlowRuntime, resolveCleanTargets } from "../flow/oris-flow-clean-runtime.mjs";
7
+ import { ORIS_FLOW_ADAPTER, ORIS_FLOW_RUNTIME } from "../flow/oris-flow-layout.mjs";
8
+
9
+ test("resolveCleanTargets defaults to .oris-flow runtime", () => {
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-clean-"));
11
+ try {
12
+ assert.deepEqual(resolveCleanTargets(root), [path.join(root, ORIS_FLOW_RUNTIME)]);
13
+ } finally {
14
+ fs.rmSync(root, { recursive: true, force: true });
15
+ }
16
+ });
17
+
18
+ test("resolveCleanTargets honors adapter runtime under .oris-flow", () => {
19
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-clean-"));
20
+ try {
21
+ fs.mkdirSync(path.dirname(path.join(root, ORIS_FLOW_ADAPTER)), { recursive: true });
22
+ fs.writeFileSync(path.join(root, ORIS_FLOW_ADAPTER), `${JSON.stringify({
23
+ paths: { runtime: ".oris-flow/runtime" },
24
+ })}\n`, "utf8");
25
+ assert.deepEqual(resolveCleanTargets(root), [path.join(root, ORIS_FLOW_RUNTIME)]);
26
+ } finally {
27
+ fs.rmSync(root, { recursive: true, force: true });
28
+ }
29
+ });
30
+
31
+ test("cleanFlowRuntime removes loop logs and preserves setup files", () => {
32
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-clean-"));
33
+ try {
34
+ fs.mkdirSync(path.join(root, ".oris-flow", "maps"), { recursive: true });
35
+ fs.mkdirSync(path.join(root, ".oris-flow", "loops", "sample"), { recursive: true });
36
+ fs.writeFileSync(path.join(root, ".oris-flow", "manifest.json"), "{}\n", "utf8");
37
+ fs.writeFileSync(path.join(root, ".oris-flow", "maps", "summary.md"), "# setup\n", "utf8");
38
+ fs.writeFileSync(path.join(root, ".oris-flow", "loops", "sample", "loop.md"), "# loop\n", "utf8");
39
+ fs.writeFileSync(path.join(root, ORIS_FLOW_ADAPTER), `${JSON.stringify({
40
+ paths: { runtime: ".oris-flow/runtime" },
41
+ })}\n`, "utf8");
42
+
43
+ const runtimeDir = path.join(root, ORIS_FLOW_RUNTIME);
44
+ fs.mkdirSync(path.join(runtimeDir, "failures"), { recursive: true });
45
+ fs.writeFileSync(path.join(runtimeDir, "chat-active.json"), "{}\n", "utf8");
46
+ fs.writeFileSync(path.join(runtimeDir, "hook-debug.jsonl"), "{}\n", "utf8");
47
+ fs.writeFileSync(path.join(runtimeDir, "sample.lock.json"), "{}\n", "utf8");
48
+ fs.writeFileSync(path.join(runtimeDir, "failures", "sample.stream.jsonl"), "{}\n", "utf8");
49
+ fs.writeFileSync(path.join(runtimeDir, "stack.json"), "{}\n", "utf8");
50
+
51
+ const result = cleanFlowRuntime(root);
52
+ assert.equal(result.cleaned, true);
53
+ assert.equal(fs.existsSync(path.join(root, ORIS_FLOW_RUNTIME)), false);
54
+ assert.ok(fs.existsSync(path.join(root, ".oris-flow", "manifest.json")));
55
+ assert.ok(fs.existsSync(path.join(root, ".oris-flow", "loops", "sample", "loop.md")));
56
+ assert.ok(fs.existsSync(path.join(root, ORIS_FLOW_ADAPTER)));
57
+ } finally {
58
+ fs.rmSync(root, { recursive: true, force: true });
59
+ }
60
+ });
61
+
62
+ test("cleanFlowRuntime dry run lists files without deleting them", () => {
63
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-clean-"));
64
+ try {
65
+ const runtimeDir = path.join(root, ORIS_FLOW_RUNTIME);
66
+ fs.mkdirSync(runtimeDir, { recursive: true });
67
+ fs.writeFileSync(path.join(runtimeDir, "hook-debug.jsonl"), "{}\n", "utf8");
68
+
69
+ const result = cleanFlowRuntime(root, { dryRun: true });
70
+ assert.equal(result.cleaned, true);
71
+ assert.ok(fs.existsSync(path.join(runtimeDir, "hook-debug.jsonl")));
72
+ } finally {
73
+ fs.rmSync(root, { recursive: true, force: true });
74
+ }
75
+ });
@@ -0,0 +1,49 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { scanRepository, writeScanResult } from "../flow/oris-flow-scan.mjs";
7
+ import { ORIS_FLOW_MANIFEST } from "../flow/oris-flow-layout.mjs";
8
+
9
+ function write(filePath, content) {
10
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
11
+ fs.writeFileSync(filePath, content, "utf8");
12
+ }
13
+
14
+ test("scanner creates only evidence-backed map sections", () => {
15
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
16
+ try {
17
+ write(path.join(root, "README.md"), "# Demo\n");
18
+ write(path.join(root, "package.json"), `${JSON.stringify({ scripts: { test: "node --test" } })}\n`);
19
+ write(path.join(root, "src", "index.js"), "export const value = 1;\n");
20
+
21
+ const scan = scanRepository(root);
22
+ assert.ok(scan.manifest.sections.docs);
23
+ assert.ok(scan.manifest.sections.stack);
24
+ assert.ok(scan.manifest.sections.commands);
25
+ assert.ok(scan.manifest.sections.code);
26
+ assert.equal(scan.manifest.sections.backendData, undefined);
27
+ assert.equal(scan.manifest["known" + "Gaps"], undefined);
28
+
29
+ const written = writeScanResult(scan);
30
+ assert.equal(written.manifestPath, ORIS_FLOW_MANIFEST);
31
+ assert.ok(fs.existsSync(path.join(root, ORIS_FLOW_MANIFEST)));
32
+ assert.ok(fs.existsSync(path.join(root, ".oris-flow", "maps", "stack.md")));
33
+ } finally {
34
+ fs.rmSync(root, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ test("scanner supports documentation-only repositories", () => {
39
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
40
+ try {
41
+ write(path.join(root, "docs", "guide.md"), "# Guide\n");
42
+ const scan = scanRepository(root);
43
+ assert.deepEqual(Object.keys(scan.manifest.sections), ["docs"]);
44
+ assert.deepEqual(scan.stacks, []);
45
+ } finally {
46
+ fs.rmSync(root, { recursive: true, force: true });
47
+ }
48
+ });
49
+
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from "node:assert/strict";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import test from "node:test";
8
+ import {
9
+ applyOrisFlowVersionControl,
10
+ ORIS_FLOW_GITIGNORE_ENTRIES,
11
+ } from "../flow/oris-gitignore.mjs";
12
+
13
+ function makeTempRepo() {
14
+ return fs.mkdtempSync(path.join(os.tmpdir(), "oris-gitignore-"));
15
+ }
16
+
17
+ test("applyOrisFlowVersionControl appends and removes oris-flow entries", () => {
18
+ const root = makeTempRepo();
19
+ try {
20
+ const ignored = applyOrisFlowVersionControl(root, "gitignore");
21
+ assert.equal(ignored.action, "created");
22
+ const text = fs.readFileSync(path.join(root, ".gitignore"), "utf8");
23
+ for (const entry of ORIS_FLOW_GITIGNORE_ENTRIES) {
24
+ assert.match(text, new RegExp(entry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
25
+ }
26
+ const committed = applyOrisFlowVersionControl(root, "commit");
27
+ assert.equal(committed.action, "removed");
28
+ const after = fs.readFileSync(path.join(root, ".gitignore"), "utf8");
29
+ for (const entry of ORIS_FLOW_GITIGNORE_ENTRIES) {
30
+ assert.doesNotMatch(after, new RegExp(`^\\s*${entry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "m"));
31
+ }
32
+ } finally {
33
+ fs.rmSync(root, { recursive: true, force: true });
34
+ }
35
+ });
@@ -0,0 +1,94 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import test from "node:test";
7
+ import {
8
+ assessLoopReadiness,
9
+ bootstrapLoop,
10
+ detectPlaywrightRoot,
11
+ } from "../loop/oris-loop-bootstrap.mjs";
12
+ import { ORIS_FLOW_ADAPTER } from "../flow/oris-flow-layout.mjs";
13
+
14
+ function initGitRepo(root) {
15
+ const git = spawnSync("git", ["init", "-q"], { cwd: root, encoding: "utf8" });
16
+ assert.equal(git.status, 0, git.stderr);
17
+ }
18
+
19
+ test("bootstrap creates loop readiness files when missing", () => {
20
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-bootstrap-"));
21
+ try {
22
+ initGitRepo(root);
23
+ fs.mkdirSync(path.join(root, "Test", "e2e"), { recursive: true });
24
+ fs.writeFileSync(path.join(root, "Test", "e2e", "playwright.config.ts"), "export default { testDir: '.' };\n", "utf8");
25
+ fs.writeFileSync(path.join(root, "Test", "e2e", "package.json"), `${JSON.stringify({ scripts: { check: "playwright test --list" } })}\n`, "utf8");
26
+
27
+ const before = assessLoopReadiness(root);
28
+ assert.equal(before.ready, false);
29
+
30
+ const result = bootstrapLoop(root);
31
+ assert.equal(result.ready, true);
32
+ assert.ok(fs.existsSync(path.join(root, ORIS_FLOW_ADAPTER)));
33
+ assert.ok(fs.existsSync(path.join(root, ".cursor", "hooks.json")));
34
+ assert.ok(fs.existsSync(path.join(root, ".cursor", "hooks", "oris-loop-stop.mjs")));
35
+ assert.ok(fs.existsSync(path.join(root, ".claude", "settings.json")));
36
+ assert.ok(fs.existsSync(path.join(root, ".claude", "hooks", "oris-loop-stop.mjs")));
37
+ assert.match(fs.readFileSync(path.join(root, ".claude", "hooks", "oris-loop-stop.mjs"), "utf8"), /--platform", "claude/);
38
+ const claudeSettings = JSON.parse(fs.readFileSync(path.join(root, ".claude", "settings.json"), "utf8"));
39
+ assert.ok(JSON.stringify(claudeSettings.hooks.Stop).includes("oris-loop-stop"));
40
+ assert.match(fs.readFileSync(path.join(root, ".gitignore"), "utf8"), /\.oris-flow\/runtime\//);
41
+
42
+ const adapter = JSON.parse(fs.readFileSync(path.join(root, ORIS_FLOW_ADAPTER), "utf8"));
43
+ assert.equal(adapter.paths.testDir, "Test/e2e");
44
+ assert.ok(adapter.commands["e2e-check"]);
45
+ } finally {
46
+ fs.rmSync(root, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ test("bootstrap is idempotent when loop is already configured", () => {
51
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-bootstrap-"));
52
+ try {
53
+ initGitRepo(root);
54
+ bootstrapLoop(root);
55
+ const adapterBefore = fs.readFileSync(path.join(root, ORIS_FLOW_ADAPTER), "utf8");
56
+ const result = bootstrapLoop(root);
57
+ assert.equal(result.ready, true);
58
+ assert.equal(fs.readFileSync(path.join(root, ORIS_FLOW_ADAPTER), "utf8"), adapterBefore);
59
+ } finally {
60
+ fs.rmSync(root, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ test("bootstrap merges stop hook into existing hooks.json", () => {
65
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-bootstrap-"));
66
+ try {
67
+ initGitRepo(root);
68
+ fs.mkdirSync(path.join(root, ".cursor"), { recursive: true });
69
+ fs.writeFileSync(path.join(root, ".cursor", "hooks.json"), `${JSON.stringify({
70
+ version: 1,
71
+ hooks: { stop: [{ command: "node .cursor/hooks/custom-stop.mjs", loop_limit: null }] },
72
+ }, null, 2)}\n`, "utf8");
73
+
74
+ const result = bootstrapLoop(root);
75
+ assert.equal(result.ready, true);
76
+ const hooks = JSON.parse(fs.readFileSync(path.join(root, ".cursor", "hooks.json"), "utf8"));
77
+ assert.equal(hooks.hooks.stop.length, 2);
78
+ assert.ok(hooks.hooks.stop.some((entry) => String(entry.command).includes("custom-stop")));
79
+ assert.ok(hooks.hooks.stop.some((entry) => String(entry.command).includes("oris-loop-stop")));
80
+ } finally {
81
+ fs.rmSync(root, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ test("detectPlaywrightRoot resolves config directory", () => {
86
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-bootstrap-"));
87
+ try {
88
+ fs.mkdirSync(path.join(root, "Test", "e2e"), { recursive: true });
89
+ fs.writeFileSync(path.join(root, "Test", "e2e", "playwright.config.ts"), "export default { testDir: '.' };\n", "utf8");
90
+ assert.equal(detectPlaywrightRoot(root), "Test/e2e");
91
+ } finally {
92
+ fs.rmSync(root, { recursive: true, force: true });
93
+ }
94
+ });
@@ -0,0 +1,112 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import {
7
+ assessPromptFiles,
8
+ improveMode,
9
+ normalizeModels,
10
+ validateLoopDocumentText,
11
+ } from "../loop/oris-loop-document.mjs";
12
+
13
+ const validLoop = `---
14
+ schemaVersion: 2
15
+ kind: oris-loop
16
+ name: bug-fix-loop
17
+ approved: true
18
+ phases: [observe, execute, verify, debrief]
19
+ limits:
20
+ maxIterations: 10
21
+ maxNoProgress: 2
22
+ maxMinutes: 60
23
+ models:
24
+ default: inherit
25
+ verifier: smart
26
+ improve:
27
+ mode: auto
28
+ permissions:
29
+ allowedPaths: [src, tests]
30
+ forbiddenActions: [commit, push, deploy, external-message]
31
+ verification:
32
+ commands: [npm test]
33
+ evidenceRequired: true
34
+ ---
35
+
36
+ # Bug Fix Loop
37
+
38
+ Goal: fix one verified bug per pass.
39
+ Stop: all listed bugs fixed or blocked.
40
+ `;
41
+
42
+ test("validates Oris Loop v2 front matter", () => {
43
+ const parsed = validateLoopDocumentText(validLoop, "bug-fix.loop.md");
44
+ assert.equal(parsed.data.schemaVersion, 2);
45
+ assert.equal(parsed.data.kind, "oris-loop");
46
+ assert.equal(parsed.data.approved, true);
47
+ assert.deepEqual(parsed.data.phases, ["observe", "execute", "verify", "debrief"]);
48
+ assert.equal(parsed.data.limits.maxIterations, 10);
49
+ assert.equal(parsed.data.verification.evidenceRequired, true);
50
+ });
51
+
52
+ test("rejects loops without user approval", () => {
53
+ assert.throws(
54
+ () => validateLoopDocumentText(validLoop.replace("approved: true", "approved: false"), "unapproved.loop.md"),
55
+ /approved must be true/,
56
+ );
57
+ });
58
+
59
+ test("rejects old schema versions", () => {
60
+ assert.throws(
61
+ () => validateLoopDocumentText(validLoop.replace("schemaVersion: 2", "schemaVersion: 1"), "old.loop.md"),
62
+ /schemaVersion must be 2/,
63
+ );
64
+ });
65
+
66
+ test("rejects empty model values but accepts arbitrary model ids", () => {
67
+ assert.throws(
68
+ () => validateLoopDocumentText(validLoop.replace("verifier: smart", 'verifier: ""'), "bad-model.loop.md"),
69
+ /models\.verifier/,
70
+ );
71
+ const custom = validateLoopDocumentText(validLoop.replace("verifier: smart", "verifier: any-platform-model"), "custom-model.loop.md");
72
+ assert.equal(custom.data.models.verifier, "any-platform-model");
73
+ });
74
+
75
+ test("rejects unknown improve modes", () => {
76
+ assert.throws(
77
+ () => validateLoopDocumentText(validLoop.replace("mode: auto", "mode: yolo"), "bad-improve.loop.md"),
78
+ /improve\.mode/,
79
+ );
80
+ });
81
+
82
+ test("normalizeModels resolves inherit, adapter aliases, and role fallbacks", () => {
83
+ const models = normalizeModels(
84
+ { default: "inherit", verifier: "smart", doctor: "explicit-model-id" },
85
+ { smart: "platform-smart-model" },
86
+ );
87
+ assert.equal(models.default, "inherit");
88
+ assert.equal(models.executor, "inherit");
89
+ assert.equal(models.verifier, "platform-smart-model");
90
+ assert.equal(models.doctor, "explicit-model-id");
91
+ assert.equal(models.debriefer, "inherit");
92
+ });
93
+
94
+ test("improveMode defaults to propose", () => {
95
+ assert.equal(improveMode({}), "propose");
96
+ assert.equal(improveMode({ improve: { mode: "auto" } }), "auto");
97
+ });
98
+
99
+ test("assessPromptFiles reports missing role prompt files", () => {
100
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-prompts-"));
101
+ try {
102
+ const empty = assessPromptFiles(dir);
103
+ assert.deepEqual(empty.missing, ["prompts/executor.md", "prompts/verifier.md", "prompts/doctor.md"]);
104
+ fs.mkdirSync(path.join(dir, "prompts"), { recursive: true });
105
+ for (const file of ["executor.md", "verifier.md", "doctor.md"]) {
106
+ fs.writeFileSync(path.join(dir, "prompts", file), "prompt content\n", "utf8");
107
+ }
108
+ assert.deepEqual(assessPromptFiles(dir).missing, []);
109
+ } finally {
110
+ fs.rmSync(dir, { recursive: true, force: true });
111
+ }
112
+ });