oris-skills 2.2.3 → 3.0.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.
Files changed (86) hide show
  1. package/.cursor-plugin/plugin.json +2 -3
  2. package/CHANGELOG.md +24 -6
  3. package/README.md +28 -60
  4. package/docs/architecture.md +15 -11
  5. package/docs/distribution.md +3 -3
  6. package/docs/maintainer-guide.md +4 -4
  7. package/docs/user-guide.md +12 -20
  8. package/package.json +3 -6
  9. package/references/clean-code-checklist.md +2 -2
  10. package/references/conventions.md +36 -16
  11. package/references/doc-policy.md +5 -5
  12. package/references/research.md +45 -0
  13. package/references/settings.md +11 -5
  14. package/references/settings.schema.json +10 -0
  15. package/scripts/flow/oris-flow-layout.mjs +0 -16
  16. package/scripts/flow/oris-flow-scan.mjs +32 -317
  17. package/scripts/flow/oris-gitignore.mjs +1 -5
  18. package/scripts/install/install-user-skills.mjs +3 -3
  19. package/scripts/install/uninstall-user-skills.mjs +2 -29
  20. package/scripts/oris-skills.mjs +0 -47
  21. package/scripts/tests/run-all-tests.mjs +1 -9
  22. package/scripts/tests/test-cleanliness.mjs +54 -0
  23. package/scripts/tests/test-oris-flow-scan.mjs +21 -91
  24. package/scripts/tests/test-routing-lifecycle.mjs +24 -54
  25. package/scripts/tests/test-schemas.mjs +17 -31
  26. package/scripts/tests/test-skill-style.mjs +5 -2
  27. package/skills/oris-flow/SKILL.md +21 -11
  28. package/skills/oris-flow/agents/openai.yaml +1 -1
  29. package/skills/oris-flow/references/architecture.md +6 -3
  30. package/skills/oris-flow/references/change.md +3 -0
  31. package/skills/oris-flow/references/commit.md +9 -8
  32. package/skills/oris-flow/references/criteria.md +9 -5
  33. package/skills/oris-flow/references/discover.md +2 -2
  34. package/skills/oris-flow/references/docs.md +5 -1
  35. package/skills/oris-flow/references/fix.md +10 -5
  36. package/skills/oris-flow/references/handoff.md +67 -0
  37. package/skills/oris-flow/references/help.md +3 -6
  38. package/skills/oris-flow/references/implement.md +7 -4
  39. package/skills/oris-flow/references/new.md +6 -6
  40. package/skills/oris-flow/references/plan.md +7 -2
  41. package/skills/oris-flow/references/pr.md +2 -2
  42. package/skills/oris-flow/references/setup.md +33 -39
  43. package/skills/oris-flow/references/triage.md +69 -0
  44. package/skills/oris-flow/references/verify.md +5 -5
  45. package/agents/oris-loop-debriefer.md +0 -30
  46. package/agents/oris-loop-doctor.md +0 -32
  47. package/agents/oris-loop-executor.md +0 -35
  48. package/agents/oris-loop-verifier.md +0 -35
  49. package/references/loop-adapter.schema.json +0 -93
  50. package/references/loop-contract.md +0 -126
  51. package/references/loop.schema.json +0 -156
  52. package/references/repo-map.md +0 -51
  53. package/references/repo-map.schema.json +0 -213
  54. package/scripts/flow/oris-flow-clean-runtime.mjs +0 -182
  55. package/scripts/install/generate-agent-adapters.mjs +0 -81
  56. package/scripts/loop/oris-loop-bootstrap.mjs +0 -383
  57. package/scripts/loop/oris-loop-bundle.mjs +0 -22
  58. package/scripts/loop/oris-loop-chat.mjs +0 -396
  59. package/scripts/loop/oris-loop-demo.mjs +0 -171
  60. package/scripts/loop/oris-loop-document.mjs +0 -196
  61. package/scripts/loop/oris-loop-dry-run.mjs +0 -114
  62. package/scripts/loop/oris-loop-fixtures.mjs +0 -149
  63. package/scripts/loop/oris-loop-list.mjs +0 -81
  64. package/scripts/loop/oris-loop-paths.mjs +0 -232
  65. package/scripts/loop/oris-loop-run.mjs +0 -301
  66. package/scripts/loop/oris-loop-stop.mjs +0 -358
  67. package/scripts/loop/oris-loop-templates.mjs +0 -80
  68. package/scripts/loop/oris-loop-verify.mjs +0 -205
  69. package/scripts/tests/test-agent-adapters.mjs +0 -70
  70. package/scripts/tests/test-oris-1-0-cleanliness.mjs +0 -58
  71. package/scripts/tests/test-oris-flow-clean-runtime.mjs +0 -75
  72. package/scripts/tests/test-oris-loop-bootstrap.mjs +0 -94
  73. package/scripts/tests/test-oris-loop-document.mjs +0 -148
  74. package/scripts/tests/test-oris-loop-list.mjs +0 -74
  75. package/scripts/tests/test-oris-loop-run.mjs +0 -71
  76. package/scripts/tests/test-oris-loop-smoke.mjs +0 -120
  77. package/scripts/tests/test-oris-loop-stop.mjs +0 -432
  78. package/skills/oris-flow/references/loop-craft.md +0 -59
  79. package/skills/oris-flow/references/loop-improve.md +0 -32
  80. package/skills/oris-flow/references/loop-run.md +0 -47
  81. package/skills/oris-flow/references/loop.md +0 -56
  82. package/skills/oris-flow/templates/debriefer.md +0 -19
  83. package/skills/oris-flow/templates/doctor.md +0 -22
  84. package/skills/oris-flow/templates/executor.md +0 -25
  85. package/skills/oris-flow/templates/orchestrator.md +0 -36
  86. package/skills/oris-flow/templates/verifier.md +0 -24
@@ -1,182 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import {
5
- ORIS_FLOW_ADAPTER,
6
- ORIS_FLOW_LOOPS_ROOT,
7
- ORIS_FLOW_MANIFEST,
8
- ORIS_FLOW_MAPS_ROOT,
9
- ORIS_FLOW_RUNTIME,
10
- } from "./oris-flow-layout.mjs";
11
-
12
- const DEFAULT_RUNTIME_RELATIVE = ORIS_FLOW_RUNTIME;
13
-
14
- function readJson(filePath) {
15
- if (!fs.existsSync(filePath)) return null;
16
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
17
- }
18
-
19
- function assertSafeRelativePath(relativePath, label) {
20
- const normalized = String(relativePath ?? "").replace(/\\/g, "/");
21
- if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) {
22
- throw new Error(`${label} must be a repository-relative path.`);
23
- }
24
- if (normalized.split("/").includes("..")) {
25
- throw new Error(`${label} must not contain '..'.`);
26
- }
27
- return normalized;
28
- }
29
-
30
- function assertUnderRepository(repositoryRoot, targetPath) {
31
- const root = path.resolve(repositoryRoot);
32
- const resolved = path.resolve(targetPath);
33
- const relative = path.relative(root, resolved);
34
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
35
- throw new Error(`Refusing to clean path outside repository: ${targetPath}`);
36
- }
37
- return resolved;
38
- }
39
-
40
- export function resolveCleanTargets(repositoryRoot) {
41
- const root = path.resolve(repositoryRoot);
42
- const adapter = readJson(path.join(root, ORIS_FLOW_ADAPTER));
43
- const runtimeRelative = assertSafeRelativePath(
44
- adapter?.paths?.runtime ?? DEFAULT_RUNTIME_RELATIVE,
45
- "paths.runtime",
46
- );
47
-
48
- if (runtimeRelative.replace(/\\/g, "/") === ORIS_FLOW_RUNTIME) {
49
- return [assertUnderRepository(root, path.join(root, ...ORIS_FLOW_RUNTIME.split("/")))];
50
- }
51
-
52
- return [assertUnderRepository(root, path.join(root, ...runtimeRelative.split("/")))];
53
- }
54
-
55
- function listEntries(directoryPath) {
56
- if (!fs.existsSync(directoryPath)) return [];
57
- const entries = [];
58
- const stack = [directoryPath];
59
- while (stack.length > 0) {
60
- const current = stack.pop();
61
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
62
- const fullPath = path.join(current, entry.name);
63
- entries.push(fullPath);
64
- if (entry.isDirectory()) stack.push(fullPath);
65
- }
66
- }
67
- return entries.sort();
68
- }
69
-
70
- function removeEntry(entryPath) {
71
- if (!fs.existsSync(entryPath)) return;
72
- const stat = fs.lstatSync(entryPath);
73
- if (stat.isDirectory()) {
74
- fs.rmSync(entryPath, { recursive: true, force: true });
75
- return;
76
- }
77
- fs.rmSync(entryPath, { force: true });
78
- }
79
-
80
- export function cleanFlowRuntime(repositoryRoot, options = {}) {
81
- const root = path.resolve(repositoryRoot);
82
- const dryRun = Boolean(options.dryRun);
83
- const targets = resolveCleanTargets(root);
84
- const removed = [];
85
- const preserved = [
86
- ORIS_FLOW_MANIFEST,
87
- `${ORIS_FLOW_MAPS_ROOT}/**`,
88
- ORIS_FLOW_ADAPTER,
89
- `${ORIS_FLOW_LOOPS_ROOT}/**`,
90
- ".cursor/hooks.json",
91
- ".cursor/hooks/oris-loop-stop.mjs",
92
- ];
93
-
94
- for (const target of targets) {
95
- if (!fs.existsSync(target)) continue;
96
- const stat = fs.lstatSync(target);
97
- if (stat.isDirectory()) {
98
- for (const entry of listEntries(target)) {
99
- const relative = path.relative(root, entry).split(path.sep).join("/");
100
- if (dryRun) {
101
- removed.push(relative);
102
- } else {
103
- removeEntry(entry);
104
- removed.push(relative);
105
- }
106
- }
107
- if (dryRun) {
108
- removed.push(path.relative(root, target).split(path.sep).join("/"));
109
- } else {
110
- removeEntry(target);
111
- removed.push(path.relative(root, target).split(path.sep).join("/"));
112
- }
113
- continue;
114
- }
115
- const relative = path.relative(root, target).split(path.sep).join("/");
116
- if (dryRun) {
117
- removed.push(relative);
118
- } else {
119
- removeEntry(target);
120
- removed.push(relative);
121
- }
122
- }
123
-
124
- return {
125
- repositoryRoot: root,
126
- dryRun,
127
- targets: targets.map((target) => path.relative(root, target).split(path.sep).join("/")),
128
- removed,
129
- preserved,
130
- cleaned: removed.length > 0,
131
- };
132
- }
133
-
134
- function parseArgs(argv) {
135
- const options = {
136
- repositoryRoot: process.cwd(),
137
- dryRun: false,
138
- json: false,
139
- };
140
- for (let i = 0; i < argv.length; i += 1) {
141
- const raw = argv[i];
142
- if (raw === "--dry-run") options.dryRun = true;
143
- else if (raw === "--json") options.json = true;
144
- else if (raw.startsWith("--repository-root=")) options.repositoryRoot = raw.slice("--repository-root=".length);
145
- else if (raw === "--repository-root" && argv[i + 1]) {
146
- options.repositoryRoot = argv[i + 1];
147
- i += 1;
148
- } else if (!raw.startsWith("-")) {
149
- options.repositoryRoot = raw;
150
- }
151
- }
152
- return options;
153
- }
154
-
155
- export function printCleanReport(result) {
156
- console.log(`Oris Flow runtime clean${result.dryRun ? " (dry run)" : ""}`);
157
- console.log(`Repository: ${result.repositoryRoot}`);
158
- console.log("");
159
- console.log("Targets:");
160
- for (const target of result.targets) console.log(`- ${target}`);
161
- console.log("");
162
- if (result.removed.length === 0) {
163
- console.log("No ephemeral runtime files found.");
164
- return;
165
- }
166
- console.log(`${result.dryRun ? "Would remove" : "Removed"} ${result.removed.length} path(s):`);
167
- for (const entry of result.removed) console.log(`- ${entry}`);
168
- console.log("");
169
- console.log("Preserved by design:");
170
- for (const entry of result.preserved) console.log(`- ${entry}`);
171
- }
172
-
173
- if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
174
- const options = parseArgs(process.argv.slice(2));
175
- const result = cleanFlowRuntime(options.repositoryRoot, { dryRun: options.dryRun });
176
- if (options.json) {
177
- console.log(JSON.stringify(result, null, 2));
178
- } else {
179
- printCleanReport(result);
180
- }
181
- process.exit(0);
182
- }
@@ -1,81 +0,0 @@
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 sourceRoot = path.join(repoRoot, "agents");
10
-
11
- function readAgent(filePath) {
12
- const text = fs.readFileSync(filePath, "utf8");
13
- const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
14
- if (!match) throw new Error(`Agent file is missing front matter: ${filePath}`);
15
- const frontMatter = Object.fromEntries(
16
- match[1]
17
- .split(/\r?\n/)
18
- .map((line) => line.trim())
19
- .filter(Boolean)
20
- .map((line) => {
21
- const index = line.indexOf(":");
22
- if (index < 0) throw new Error(`Unsupported front matter line in ${filePath}: ${line}`);
23
- return [line.slice(0, index).trim(), line.slice(index + 1).trim()];
24
- }),
25
- );
26
- return { frontMatter, body: match[2].trim() };
27
- }
28
-
29
- function writeFile(filePath, content) {
30
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
31
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, "utf8");
32
- }
33
-
34
- function renderMarkdownAgent(agent) {
35
- return `---
36
- name: ${agent.frontMatter.name}
37
- description: ${agent.frontMatter.description}
38
- readonly: ${agent.frontMatter.readonly ?? "false"}
39
- ---
40
-
41
- ${agent.body}
42
- `;
43
- }
44
-
45
- function renderCodexAgent(agent) {
46
- const name = JSON.stringify(agent.frontMatter.name);
47
- const description = JSON.stringify(agent.frontMatter.description);
48
- const instructions = JSON.stringify(agent.body);
49
- return `name = ${name}
50
- description = ${description}
51
- developer_instructions = ${instructions}
52
- `;
53
- }
54
-
55
- function readAgents() {
56
- return fs.readdirSync(sourceRoot)
57
- .filter((name) => name.endsWith(".md"))
58
- .map((name) => ({ name, ...readAgent(path.join(sourceRoot, name)) }));
59
- }
60
-
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
- */
66
- export function generateAgentAdapters(root = repoRoot) {
67
- const agents = readAgents();
68
- for (const agent of agents) {
69
- writeFile(path.join(root, ".cursor", "agents", agent.name), renderMarkdownAgent(agent));
70
- writeFile(path.join(root, ".claude", "agents", agent.name), renderMarkdownAgent(agent));
71
- writeFile(path.join(root, ".codex", "agents", agent.name.replace(/\.md$/, ".toml")), renderCodexAgent(agent));
72
- }
73
- return agents.map((agent) => agent.frontMatter.name);
74
- }
75
-
76
- if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
77
- const rootArg = process.argv.find((arg) => arg.startsWith("--root="));
78
- const root = rootArg ? path.resolve(rootArg.slice("--root=".length)) : repoRoot;
79
- const generated = generateAgentAdapters(root);
80
- console.log(JSON.stringify({ generated }, null, 2));
81
- }
@@ -1,383 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { ensureGitignoreEntries, ORIS_LOOP_GITIGNORE_ENTRY } from "../flow/oris-gitignore.mjs";
5
- import {
6
- ORIS_FLOW_ADAPTER,
7
- ORIS_FLOW_LOOPS_ROOT,
8
- ORIS_FLOW_MANIFEST,
9
- ORIS_FLOW_RUNTIME,
10
- ORIS_FLOW_TASKS_ROOT,
11
- ORIS_LOOP_SCHEMA,
12
- ORIS_LOOP_SCHEMA_VERSION,
13
- } from "../flow/oris-flow-layout.mjs";
14
-
15
- export const HOOK_PLATFORMS = ["cursor", "claude"];
16
-
17
- /** Repo-local wrapper: resolves the installed bundle and forwards stdin. */
18
- export function stopHookWrapperSource(platform) {
19
- return `#!/usr/bin/env node
20
- import childProcess from "node:child_process";
21
- import fs from "node:fs";
22
- import os from "node:os";
23
- import path from "node:path";
24
- import process from "node:process";
25
-
26
- function resolveStopHook() {
27
- const candidates = [];
28
- if (process.env.ORIS_SKILLS_SCRIPTS_ROOT) {
29
- const scriptsRoot = path.resolve(process.env.ORIS_SKILLS_SCRIPTS_ROOT);
30
- candidates.push(path.join(scriptsRoot, "loop", "oris-loop-stop.mjs"));
31
- candidates.push(path.join(scriptsRoot, "oris-loop-stop.mjs"));
32
- }
33
- candidates.push(path.join(os.homedir(), ".oris", "oris-skills", "scripts", "loop", "oris-loop-stop.mjs"));
34
- candidates.push(path.join(os.homedir(), ".cursor", "oris-skills", "scripts", "loop", "oris-loop-stop.mjs"));
35
- for (const candidate of candidates) {
36
- if (fs.existsSync(candidate)) return candidate;
37
- }
38
- process.stderr.write("Oris Loop stop hook not found. Run: npx oris-skills install\\n");
39
- process.exit(1);
40
- }
41
-
42
- const result = childProcess.spawnSync(process.execPath, [resolveStopHook(), "--platform", "${platform}"], {
43
- stdio: "inherit",
44
- env: process.env,
45
- });
46
- process.exit(result.status ?? 1);
47
- `;
48
- }
49
-
50
- const CURSOR_STOP_COMMAND = "node .cursor/hooks/oris-loop-stop.mjs";
51
- const CLAUDE_STOP_COMMAND = "node .claude/hooks/oris-loop-stop.mjs";
52
-
53
- function readJson(filePath) {
54
- if (!fs.existsSync(filePath)) return null;
55
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
56
- }
57
-
58
- function listFiles(dir, depth = 0, maxDepth = 4, matches = []) {
59
- if (depth > maxDepth || !fs.existsSync(dir)) return matches;
60
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
61
- if (entry.name === "node_modules" || entry.name === ".git") continue;
62
- const full = path.join(dir, entry.name);
63
- if (entry.isFile() && /^playwright\.config\.(ts|js|mjs|cjs)$/.test(entry.name)) {
64
- matches.push(full);
65
- } else if (entry.isDirectory()) {
66
- listFiles(full, depth + 1, maxDepth, matches);
67
- }
68
- }
69
- return matches;
70
- }
71
-
72
- function toPosixRelative(root, absolutePath) {
73
- return path.relative(root, absolutePath).split(path.sep).join("/");
74
- }
75
-
76
- export function detectPlaywrightRoot(repositoryRoot) {
77
- const configs = listFiles(repositoryRoot);
78
- const preferred = configs.sort((a, b) => a.length - b.length)[0];
79
- if (!preferred) return null;
80
- const configDir = path.dirname(preferred);
81
- const text = fs.readFileSync(preferred, "utf8");
82
- const match = text.match(/testDir:\s*['"]([^'"]+)['"]/);
83
- if (match && match[1] !== ".") {
84
- return toPosixRelative(repositoryRoot, path.resolve(configDir, match[1]));
85
- }
86
- return toPosixRelative(repositoryRoot, configDir);
87
- }
88
-
89
- function detectRepositoryName(repositoryRoot) {
90
- const flowManifest = readJson(path.join(repositoryRoot, ORIS_FLOW_MANIFEST));
91
- if (flowManifest?.repository) return String(flowManifest.repository);
92
- return path.basename(repositoryRoot);
93
- }
94
-
95
- function detectPackageManagerCommand(repositoryRoot, testDir) {
96
- const packageJsonPath = path.join(repositoryRoot, testDir, "package.json");
97
- if (!fs.existsSync(packageJsonPath)) return null;
98
- const pkg = readJson(packageJsonPath);
99
- const scripts = pkg?.scripts ?? {};
100
- const packageRoot = path.dirname(packageJsonPath);
101
- const manager = fs.existsSync(path.join(packageRoot, "pnpm-lock.yaml"))
102
- ? "pnpm"
103
- : fs.existsSync(path.join(packageRoot, "yarn.lock"))
104
- ? "yarn"
105
- : "npm";
106
- const executable = process.platform === "win32" ? `${manager}.cmd` : manager;
107
- const runArgs = manager === "yarn" ? [] : ["run"];
108
- if (scripts.check) {
109
- return {
110
- name: "e2e-check",
111
- command: { executable, args: [...runArgs, "check"], cwd: testDir },
112
- };
113
- }
114
- if (scripts.test) {
115
- return {
116
- name: "e2e-target",
117
- command: { executable, args: [...runArgs, "test", "--", "--project=chromium"], cwd: testDir },
118
- };
119
- }
120
- return null;
121
- }
122
-
123
- function detectProtectedPaths(repositoryRoot) {
124
- const paths = [".git"];
125
- for (const candidate of ["azure-pipelines.yml", ".github/workflows", ".gitlab-ci.yml", "NuGet.config"]) {
126
- if (fs.existsSync(path.join(repositoryRoot, candidate))) paths.push(candidate);
127
- }
128
- return paths;
129
- }
130
-
131
- export function buildAdapterDraft(repositoryRoot, hints = {}) {
132
- const repository = hints.repository ?? detectRepositoryName(repositoryRoot);
133
- const testDir = hints.testDir ?? detectPlaywrightRoot(repositoryRoot);
134
- const allowed = new Set([
135
- ORIS_FLOW_LOOPS_ROOT,
136
- ".oris-flow/maps",
137
- ...(Array.isArray(hints.allowed) ? hints.allowed : []),
138
- ]);
139
- for (const optionalPath of ["docs", "docs/tasks", ORIS_FLOW_TASKS_ROOT, "specs", "src", "test", "tests"]) {
140
- if (fs.existsSync(path.join(repositoryRoot, optionalPath))) allowed.add(optionalPath);
141
- }
142
- allowed.add(ORIS_FLOW_TASKS_ROOT);
143
- if (testDir) allowed.add(testDir);
144
-
145
- const commands = {
146
- verify: {
147
- executable: process.execPath,
148
- args: ["--version"],
149
- cwd: ".",
150
- },
151
- };
152
- const npmCommand = testDir ? detectPackageManagerCommand(repositoryRoot, testDir) : null;
153
- if (npmCommand) commands[npmCommand.name] = npmCommand.command;
154
-
155
- const adapter = {
156
- $schema: ORIS_LOOP_SCHEMA,
157
- schemaVersion: ORIS_LOOP_SCHEMA_VERSION,
158
- repository,
159
- paths: {
160
- taskRoots: [
161
- ORIS_FLOW_TASKS_ROOT,
162
- ...(fs.existsSync(path.join(repositoryRoot, "docs", "tasks")) ? ["docs/tasks"] : []),
163
- ],
164
- runtime: ORIS_FLOW_RUNTIME,
165
- loops: ORIS_FLOW_LOOPS_ROOT,
166
- allowed: [...allowed],
167
- protected: detectProtectedPaths(repositoryRoot),
168
- },
169
- commands,
170
- models: {},
171
- preflight: {
172
- minFreeDiskGiB: 5,
173
- },
174
- };
175
- if (testDir) adapter.paths.testDir = testDir;
176
- return adapter;
177
- }
178
-
179
- function hookFiles(platform, root) {
180
- if (platform === "claude") {
181
- return {
182
- registrationPath: path.join(root, ".claude", "settings.json"),
183
- wrapperPath: path.join(root, ".claude", "hooks", "oris-loop-stop.mjs"),
184
- };
185
- }
186
- return {
187
- registrationPath: path.join(root, ".cursor", "hooks.json"),
188
- wrapperPath: path.join(root, ".cursor", "hooks", "oris-loop-stop.mjs"),
189
- };
190
- }
191
-
192
- function hookRegistered(platform, registrationPath) {
193
- const data = readJson(registrationPath);
194
- if (!data) return false;
195
- if (platform === "claude") {
196
- return JSON.stringify(data?.hooks?.Stop ?? []).includes("oris-loop-stop");
197
- }
198
- const stops = data?.hooks?.stop ?? [];
199
- return stops.some((entry) => String(entry?.command ?? "").includes("oris-loop-stop"));
200
- }
201
-
202
- export function assessLoopReadiness(repositoryRoot, platforms = HOOK_PLATFORMS) {
203
- const root = path.resolve(repositoryRoot);
204
- const adapterPath = path.join(root, ORIS_FLOW_ADAPTER);
205
- const missing = [];
206
- if (!fs.existsSync(adapterPath)) missing.push(ORIS_FLOW_ADAPTER);
207
- for (const platform of platforms) {
208
- const { registrationPath, wrapperPath } = hookFiles(platform, root);
209
- if (!hookRegistered(platform, registrationPath)) {
210
- missing.push(`${toPosixRelative(root, registrationPath)} stop registration for Oris Loop`);
211
- }
212
- if (!fs.existsSync(wrapperPath)) missing.push(toPosixRelative(root, wrapperPath));
213
- }
214
- return {
215
- ready: missing.length === 0,
216
- missing,
217
- paths: { adapterPath },
218
- };
219
- }
220
-
221
- function ensureGitignore(repositoryRoot, dryRun) {
222
- return ensureGitignoreEntries(repositoryRoot, [ORIS_LOOP_GITIGNORE_ENTRY], dryRun);
223
- }
224
-
225
- export function writeStopHookWrapper(repositoryRoot, platform = "cursor") {
226
- const { wrapperPath } = hookFiles(platform, path.resolve(repositoryRoot));
227
- fs.mkdirSync(path.dirname(wrapperPath), { recursive: true });
228
- fs.writeFileSync(wrapperPath, stopHookWrapperSource(platform), "utf8");
229
- return wrapperPath;
230
- }
231
-
232
- export function writeHooksRegistration(repositoryRoot, platform = "cursor") {
233
- const root = path.resolve(repositoryRoot);
234
- const { registrationPath } = hookFiles(platform, root);
235
- fs.mkdirSync(path.dirname(registrationPath), { recursive: true });
236
- const existing = readJson(registrationPath) ?? (platform === "claude" ? {} : { version: 1, hooks: {} });
237
- if (platform === "claude") {
238
- existing.hooks ??= {};
239
- const stops = Array.isArray(existing.hooks.Stop) ? existing.hooks.Stop : [];
240
- if (!JSON.stringify(stops).includes("oris-loop-stop")) {
241
- stops.push({ hooks: [{ type: "command", command: CLAUDE_STOP_COMMAND }] });
242
- }
243
- existing.hooks.Stop = stops;
244
- } else {
245
- existing.version ??= 1;
246
- existing.hooks ??= {};
247
- const stops = Array.isArray(existing.hooks.stop) ? existing.hooks.stop : [];
248
- if (!stops.some((entry) => String(entry?.command ?? "").includes("oris-loop-stop"))) {
249
- stops.push({ command: CURSOR_STOP_COMMAND, loop_limit: null });
250
- }
251
- existing.hooks.stop = stops;
252
- }
253
- fs.writeFileSync(registrationPath, `${JSON.stringify(existing, null, 2)}\n`, "utf8");
254
- return registrationPath;
255
- }
256
-
257
- function ensureHookPlatform(repositoryRoot, platform, dryRun, actions) {
258
- const root = path.resolve(repositoryRoot);
259
- const { registrationPath, wrapperPath } = hookFiles(platform, root);
260
- if (hookRegistered(platform, registrationPath)) {
261
- actions.push({ action: "present", path: registrationPath });
262
- } else if (dryRun) {
263
- actions.push({ action: fs.existsSync(registrationPath) ? "merge" : "create", path: registrationPath });
264
- } else {
265
- actions.push({ action: "registered", path: writeHooksRegistration(root, platform) });
266
- }
267
- if (fs.existsSync(wrapperPath)) {
268
- actions.push({ action: "present", path: wrapperPath });
269
- } else if (dryRun) {
270
- actions.push({ action: "create", path: wrapperPath });
271
- } else {
272
- actions.push({ action: "created", path: writeStopHookWrapper(root, platform) });
273
- }
274
- }
275
-
276
- export function bootstrapLoop(repositoryRoot, options = {}) {
277
- const root = path.resolve(repositoryRoot);
278
- const dryRun = Boolean(options.dryRun);
279
- const platforms = Array.isArray(options.platforms) && options.platforms.length > 0
280
- ? options.platforms.filter((platform) => HOOK_PLATFORMS.includes(platform))
281
- : HOOK_PLATFORMS;
282
- const assessment = assessLoopReadiness(root, platforms);
283
- if (assessment.ready) {
284
- return {
285
- repositoryRoot: root,
286
- dryRun,
287
- platforms,
288
- before: assessment,
289
- after: assessment,
290
- actions: [],
291
- ready: true,
292
- };
293
- }
294
- const actions = [];
295
-
296
- if (!fs.existsSync(path.join(root, ORIS_FLOW_ADAPTER))) {
297
- const adapterPath = path.join(root, ORIS_FLOW_ADAPTER);
298
- if (dryRun) {
299
- actions.push({ action: "create", path: adapterPath, detail: "adapter draft" });
300
- } else {
301
- const adapter = buildAdapterDraft(root, options);
302
- fs.mkdirSync(path.dirname(adapterPath), { recursive: true });
303
- fs.writeFileSync(adapterPath, `${JSON.stringify(adapter, null, 2)}\n`, "utf8");
304
- actions.push({ action: "created", path: adapterPath });
305
- }
306
- } else {
307
- actions.push({ action: "present", path: path.join(root, ORIS_FLOW_ADAPTER) });
308
- }
309
-
310
- for (const platform of platforms) {
311
- ensureHookPlatform(root, platform, dryRun, actions);
312
- }
313
-
314
- actions.push(ensureGitignore(root, dryRun));
315
-
316
- const after = dryRun ? assessment : assessLoopReadiness(root, platforms);
317
- return {
318
- repositoryRoot: root,
319
- dryRun,
320
- platforms,
321
- before: assessment,
322
- after,
323
- actions,
324
- ready: after.ready,
325
- };
326
- }
327
-
328
- function parseArgs(argv) {
329
- const options = {
330
- repositoryRoot: process.cwd(),
331
- dryRun: false,
332
- json: false,
333
- platforms: [],
334
- };
335
- for (let i = 0; i < argv.length; i += 1) {
336
- const raw = argv[i];
337
- if (raw === "--dry-run") options.dryRun = true;
338
- else if (raw === "--json") options.json = true;
339
- else if (raw.startsWith("--platforms=")) options.platforms = raw.slice("--platforms=".length).split(",").map((value) => value.trim()).filter(Boolean);
340
- else if (raw === "--platforms" && argv[i + 1]) {
341
- options.platforms = argv[i + 1].split(",").map((value) => value.trim()).filter(Boolean);
342
- i += 1;
343
- } else if (raw.startsWith("--repository-root=")) options.repositoryRoot = raw.slice("--repository-root=".length);
344
- else if (raw === "--repository-root" && argv[i + 1]) {
345
- options.repositoryRoot = argv[i + 1];
346
- i += 1;
347
- } else if (!raw.startsWith("-")) {
348
- options.repositoryRoot = raw;
349
- }
350
- }
351
- return options;
352
- }
353
-
354
- export function printBootstrapReport(result) {
355
- console.log(`Oris Loop bootstrap${result.dryRun ? " (dry run)" : ""}`);
356
- console.log(`Repository: ${result.repositoryRoot}`);
357
- console.log(`Hook platforms: ${result.platforms.join(", ")}`);
358
- console.log("");
359
- if (result.before.ready) {
360
- console.log("Loop readiness: already configured.");
361
- return;
362
- }
363
- console.log(`Loop readiness before: missing ${result.before.missing.length} item(s)`);
364
- for (const item of result.before.missing) console.log(`- ${item}`);
365
- console.log("");
366
- console.log("Planned actions:");
367
- for (const action of result.actions) {
368
- console.log(`- ${action.action}: ${action.path}${action.detail ? ` (${action.detail})` : ""}`);
369
- }
370
- console.log("");
371
- console.log(result.ready ? "Loop readiness: complete." : "Loop readiness: incomplete.");
372
- }
373
-
374
- if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
375
- const options = parseArgs(process.argv.slice(2));
376
- const result = bootstrapLoop(options.repositoryRoot, { dryRun: options.dryRun, platforms: options.platforms });
377
- if (options.json) {
378
- console.log(JSON.stringify(result, null, 2));
379
- } else {
380
- printBootstrapReport(result);
381
- }
382
- process.exit(result.ready || result.dryRun ? 0 : 1);
383
- }
@@ -1,22 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- /** Resolve the OrisSkills scripts directory for bundled loop helpers. */
7
- export function resolveScriptsRoot(fromImportMetaUrl) {
8
- if (process.env.ORIS_SKILLS_SCRIPTS_ROOT) {
9
- return path.resolve(process.env.ORIS_SKILLS_SCRIPTS_ROOT);
10
- }
11
- const local = path.dirname(fileURLToPath(fromImportMetaUrl));
12
- if (fs.existsSync(path.join(local, "oris-loop-stop.mjs"))) {
13
- return local;
14
- }
15
- const installed = path.join(os.homedir(), ".oris", "oris-skills", "scripts", "loop");
16
- if (fs.existsSync(installed)) return installed;
17
- return path.join(os.homedir(), ".cursor", "oris-skills", "scripts", "loop");
18
- }
19
-
20
- export function scriptPath(fromImportMetaUrl, fileName) {
21
- return path.join(resolveScriptsRoot(fromImportMetaUrl), fileName);
22
- }