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,70 +0,0 @@
1
- import childProcess from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import assert from "node:assert/strict";
6
- import process from "node:process";
7
- import test from "node:test";
8
- import { fileURLToPath } from "node:url";
9
- import { generateAgentAdapters } from "../install/generate-agent-adapters.mjs";
10
-
11
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
12
-
13
- function runCli(args, homeDir) {
14
- return childProcess.spawnSync(process.execPath, ["scripts/oris-skills.mjs", ...args], {
15
- cwd: root,
16
- encoding: "utf8",
17
- env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir },
18
- shell: false,
19
- });
20
- }
21
-
22
- test("generates repo-local Cursor, Claude, and Codex agent adapters", () => {
23
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), "oris-agent-adapters-"));
24
- try {
25
- const generated = generateAgentAdapters(temp);
26
- assert.ok(generated.includes("oris-loop-executor"));
27
- assert.ok(fs.existsSync(path.join(temp, ".cursor", "agents", "oris-loop-executor.md")));
28
- assert.ok(fs.existsSync(path.join(temp, ".claude", "agents", "oris-loop-verifier.md")));
29
- assert.ok(fs.existsSync(path.join(temp, ".codex", "agents", "oris-loop-doctor.toml")));
30
- } finally {
31
- fs.rmSync(temp, { recursive: true, force: true });
32
- }
33
- });
34
-
35
- test("install never plans loop agent adapters in user agent homes", () => {
36
- const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-agent-install-"));
37
- try {
38
- fs.mkdirSync(path.join(homeDir, ".claude"), { recursive: true });
39
- fs.mkdirSync(path.join(homeDir, ".cursor"), { recursive: true });
40
- const result = runCli(["install", "--dry-run"], homeDir);
41
- assert.equal(result.status, 0, result.stderr);
42
- assert.doesNotMatch(result.stdout, /loop agents/i);
43
- assert.doesNotMatch(result.stdout, /agent adapters/i);
44
- } finally {
45
- fs.rmSync(homeDir, { recursive: true, force: true });
46
- }
47
- });
48
-
49
- test("uninstall removes legacy loop agent adapters but keeps user agents", () => {
50
- const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-agent-uninstall-"));
51
- try {
52
- const claudeAgents = path.join(homeDir, ".claude", "agents");
53
- const cursorAgents = path.join(homeDir, ".cursor", "agents");
54
- const codexAgents = path.join(homeDir, ".codex", "agents");
55
- for (const dir of [claudeAgents, cursorAgents, codexAgents]) fs.mkdirSync(dir, { recursive: true });
56
- fs.writeFileSync(path.join(claudeAgents, "oris-loop-executor.md"), "legacy adapter\n", "utf8");
57
- fs.writeFileSync(path.join(claudeAgents, "my-agent.md"), "user agent\n", "utf8");
58
- fs.writeFileSync(path.join(cursorAgents, "oris-loop-verifier.md"), "legacy adapter\n", "utf8");
59
- fs.writeFileSync(path.join(codexAgents, "oris-loop-doctor.toml"), "legacy adapter\n", "utf8");
60
-
61
- const result = runCli(["uninstall"], homeDir);
62
- assert.equal(result.status, 0, result.stderr);
63
- assert.equal(fs.existsSync(path.join(claudeAgents, "oris-loop-executor.md")), false);
64
- assert.equal(fs.existsSync(path.join(cursorAgents, "oris-loop-verifier.md")), false);
65
- assert.equal(fs.existsSync(path.join(codexAgents, "oris-loop-doctor.toml")), false);
66
- assert.equal(fs.existsSync(path.join(claudeAgents, "my-agent.md")), true, "non-Oris agents must survive");
67
- } finally {
68
- fs.rmSync(homeDir, { recursive: true, force: true });
69
- }
70
- });
@@ -1,58 +0,0 @@
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
- });
@@ -1,75 +0,0 @@
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
- });
@@ -1,94 +0,0 @@
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
- });
@@ -1,148 +0,0 @@
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
- activeRoles,
8
- assessPromptFiles,
9
- improveMode,
10
- normalizeModels,
11
- validateLoopDocumentText,
12
- } from "../loop/oris-loop-document.mjs";
13
-
14
- const validLoop = `---
15
- schemaVersion: 2
16
- kind: oris-loop
17
- name: bug-fix-loop
18
- approved: true
19
- phases: [observe, execute, verify, debrief]
20
- roles: [executor, verifier, debriefer]
21
- limits:
22
- maxIterations: 10
23
- maxNoProgress: 2
24
- maxMinutes: 60
25
- models:
26
- default: inherit
27
- verifier: smart
28
- improve:
29
- mode: auto
30
- permissions:
31
- allowedPaths: [src, tests]
32
- forbiddenActions: [commit, push, deploy, external-message]
33
- verification:
34
- commands: [npm test]
35
- evidenceRequired: true
36
- ---
37
-
38
- # Bug Fix Loop
39
-
40
- Goal: fix one verified bug per pass.
41
- Stop: all listed bugs fixed or blocked.
42
- `;
43
-
44
- test("validates Oris Loop v2 front matter", () => {
45
- const parsed = validateLoopDocumentText(validLoop, "bug-fix.loop.md");
46
- assert.equal(parsed.data.schemaVersion, 2);
47
- assert.equal(parsed.data.kind, "oris-loop");
48
- assert.equal(parsed.data.approved, true);
49
- assert.deepEqual(parsed.data.phases, ["observe", "execute", "verify", "debrief"]);
50
- assert.equal(parsed.data.limits.maxIterations, 10);
51
- assert.equal(parsed.data.verification.evidenceRequired, true);
52
- });
53
-
54
- test("rejects loops without user approval", () => {
55
- assert.throws(
56
- () => validateLoopDocumentText(validLoop.replace("approved: true", "approved: false"), "unapproved.loop.md"),
57
- /approved must be true/,
58
- );
59
- });
60
-
61
- test("rejects old schema versions", () => {
62
- assert.throws(
63
- () => validateLoopDocumentText(validLoop.replace("schemaVersion: 2", "schemaVersion: 1"), "old.loop.md"),
64
- /schemaVersion must be 2/,
65
- );
66
- });
67
-
68
- test("rejects empty model values but accepts arbitrary model ids", () => {
69
- assert.throws(
70
- () => validateLoopDocumentText(validLoop.replace("verifier: smart", 'verifier: ""'), "bad-model.loop.md"),
71
- /models\.verifier/,
72
- );
73
- const custom = validateLoopDocumentText(validLoop.replace("verifier: smart", "verifier: any-platform-model"), "custom-model.loop.md");
74
- assert.equal(custom.data.models.verifier, "any-platform-model");
75
- });
76
-
77
- test("rejects unknown improve modes", () => {
78
- assert.throws(
79
- () => validateLoopDocumentText(validLoop.replace("mode: auto", "mode: yolo"), "bad-improve.loop.md"),
80
- /improve\.mode/,
81
- );
82
- });
83
-
84
- test("roles default to executor + verifier and must always include both", () => {
85
- assert.deepEqual(activeRoles({}), ["executor", "verifier"]);
86
- assert.deepEqual(activeRoles({ roles: ["executor", "verifier", "doctor"] }), ["executor", "verifier", "doctor"]);
87
- assert.throws(
88
- () => validateLoopDocumentText(
89
- validLoop.replace("roles: [executor, verifier, debriefer]", "roles: [executor, debriefer]"),
90
- "no-verifier.loop.md",
91
- ),
92
- /roles must include verifier/,
93
- );
94
- assert.throws(
95
- () => validateLoopDocumentText(
96
- validLoop.replace("roles: [executor, verifier, debriefer]", "roles: [executor, verifier, narrator]"),
97
- "bad-role.loop.md",
98
- ),
99
- /roles must be an array/,
100
- );
101
- });
102
-
103
- test("improve.mode auto requires the debriefer role", () => {
104
- assert.throws(
105
- () => validateLoopDocumentText(
106
- validLoop.replace("roles: [executor, verifier, debriefer]", "roles: [executor, verifier]"),
107
- "auto-without-debriefer.loop.md",
108
- ),
109
- /improve\.mode auto requires the debriefer role/,
110
- );
111
- });
112
-
113
- test("normalizeModels resolves inherit, adapter aliases, and role fallbacks", () => {
114
- const models = normalizeModels(
115
- { default: "inherit", verifier: "smart", doctor: "explicit-model-id" },
116
- { smart: "platform-smart-model" },
117
- );
118
- assert.equal(models.default, "inherit");
119
- assert.equal(models.executor, "inherit");
120
- assert.equal(models.verifier, "platform-smart-model");
121
- assert.equal(models.doctor, "explicit-model-id");
122
- assert.equal(models.debriefer, "inherit");
123
- });
124
-
125
- test("improveMode defaults to propose", () => {
126
- assert.equal(improveMode({}), "propose");
127
- assert.equal(improveMode({ improve: { mode: "auto" } }), "auto");
128
- });
129
-
130
- test("assessPromptFiles reports missing prompts for the active roles only", () => {
131
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-prompts-"));
132
- try {
133
- const empty = assessPromptFiles(dir);
134
- assert.deepEqual(empty.missing, ["prompts/executor.md", "prompts/verifier.md"]);
135
- fs.mkdirSync(path.join(dir, "prompts"), { recursive: true });
136
- for (const file of ["executor.md", "verifier.md"]) {
137
- fs.writeFileSync(path.join(dir, "prompts", file), "prompt content\n", "utf8");
138
- }
139
- assert.deepEqual(assessPromptFiles(dir).missing, []);
140
- assert.deepEqual(
141
- assessPromptFiles(dir, ["executor", "verifier", "doctor"]).missing,
142
- ["prompts/doctor.md"],
143
- "opting in a role requires its prompt file",
144
- );
145
- } finally {
146
- fs.rmSync(dir, { recursive: true, force: true });
147
- }
148
- });
@@ -1,74 +0,0 @@
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
- defaultLoopDocumentPaths,
10
- discoverExistingLoops,
11
- resolveLoopDocumentPaths,
12
- } from "../loop/oris-loop-paths.mjs";
13
- import { loopDocumentSource } from "../loop/oris-loop-fixtures.mjs";
14
- import { ORIS_FLOW_LOOPS_ROOT } from "../flow/oris-flow-layout.mjs";
15
-
16
- function makeTempRepo() {
17
- return fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-list-"));
18
- }
19
-
20
- test("discoverExistingLoops finds approved loop documents", () => {
21
- const root = makeTempRepo();
22
- try {
23
- const loopFile = path.join(root, ORIS_FLOW_LOOPS_ROOT, "feature-a", "loop.md");
24
- const contextFile = path.join(root, ORIS_FLOW_LOOPS_ROOT, "feature-a", "context.md");
25
- fs.mkdirSync(path.dirname(loopFile), { recursive: true });
26
- fs.writeFileSync(loopFile, loopDocumentSource({ program: "feature-a", goal: "Verify feature A." }), "utf8");
27
- fs.writeFileSync(contextFile, "# Oris Loop Context\n\nRun: inactive\nState: ready\nCurrent program: feature-a\n\n## Objective\n\nVerify feature A\n", "utf8");
28
-
29
- const loops = discoverExistingLoops(root);
30
- assert.equal(loops.length, 1);
31
- assert.equal(loops[0].slug, "feature-a");
32
- assert.equal(loops[0].program, "feature-a");
33
- assert.match(loops[0].objective, /Verify feature A/);
34
- } finally {
35
- fs.rmSync(root, { recursive: true, force: true });
36
- }
37
- });
38
-
39
- test("discoverExistingLoops ignores incomplete folders", () => {
40
- const root = makeTempRepo();
41
- try {
42
- const partial = path.join(root, ORIS_FLOW_LOOPS_ROOT, "partial", "loop.md");
43
- fs.mkdirSync(path.dirname(partial), { recursive: true });
44
- fs.writeFileSync(partial, "not a loop\n", "utf8");
45
- assert.deepEqual(discoverExistingLoops(root), []);
46
- } finally {
47
- fs.rmSync(root, { recursive: true, force: true });
48
- }
49
- });
50
-
51
- test("defaultLoopDocumentPaths includes the prompts directory", () => {
52
- const root = makeTempRepo();
53
- try {
54
- const docs = defaultLoopDocumentPaths(root, "docs/tasks/my-feature");
55
- assert.match(docs.loopRelative, /^\.oris-flow\/loops\/my-feature\/loop\.md$/);
56
- assert.match(docs.contextRelative, /^\.oris-flow\/loops\/my-feature\/context\.md$/);
57
- assert.match(docs.promptsRelative, /^\.oris-flow\/loops\/my-feature\/prompts$/);
58
- } finally {
59
- fs.rmSync(root, { recursive: true, force: true });
60
- }
61
- });
62
-
63
- test("resolveLoopDocumentPaths resolves loop documents", () => {
64
- const root = makeTempRepo();
65
- try {
66
- const taskPath = "docs/tasks/preferred";
67
- const resolved = resolveLoopDocumentPaths(root, { taskPath });
68
- assert.match(resolved.loopRelative, /^\.oris-flow\/loops\/preferred\/loop\.md$/);
69
- assert.match(resolved.contextRelative, /^\.oris-flow\/loops\/preferred\/context\.md$/);
70
- assert.match(resolved.promptsRelative, /^\.oris-flow\/loops\/preferred\/prompts$/);
71
- } finally {
72
- fs.rmSync(root, { recursive: true, force: true });
73
- }
74
- });
@@ -1,71 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { buildRoleInvocation, doctorDecisionToState, extractJson, verifierVerdictToState } from "../loop/oris-loop-run.mjs";
4
- import { readTemplate, stripTemplateHeading, substitutePlaceholders } from "../loop/oris-loop-templates.mjs";
5
-
6
- test("buildRoleInvocation maps roles to codex and claude CLI calls with per-role models", () => {
7
- const codex = buildRoleInvocation("codex", { prompt: "do it", model: "model-x", root: "/repo" });
8
- assert.equal(codex.command, "codex");
9
- assert.deepEqual(codex.args, ["exec", "--sandbox", "workspace-write", "--cd", "/repo", "-m", "model-x", "do it"]);
10
-
11
- const claude = buildRoleInvocation("claude", { prompt: "do it", model: "model-y", root: "/repo" });
12
- assert.equal(claude.command, "claude");
13
- assert.deepEqual(claude.args, ["-p", "do it", "--permission-mode", "acceptEdits", "--model", "model-y"]);
14
-
15
- const inherit = buildRoleInvocation("claude", { prompt: "do it", model: "inherit", root: "/repo" });
16
- assert.ok(!inherit.args.includes("--model"), "inherit must not pass an explicit model");
17
- });
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
-
29
- test("extractJson pulls the last valid JSON object from noisy output", () => {
30
- const output = 'thinking...\n{"broken": \nand then {"role":"doctor","decision":"complete"} trailing';
31
- assert.deepEqual(extractJson(output), { role: "doctor", decision: "complete" });
32
- assert.equal(extractJson("no json here"), null);
33
- });
34
-
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", () => {
41
- assert.equal(doctorDecisionToState("complete"), "complete");
42
- assert.equal(doctorDecisionToState("continue"), "continue");
43
- assert.equal(doctorDecisionToState("retry"), "continue");
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");
47
- assert.equal(doctorDecisionToState("stop"), "blocked");
48
- assert.equal(doctorDecisionToState(undefined), "blocked");
49
- });
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
-
60
- test("orchestrator template resolves from the skill templates and substitutes placeholders", () => {
61
- const template = readTemplate("orchestrator", import.meta.url);
62
- assert.match(template, /ORIS LOOP RUN/);
63
- const filled = substitutePlaceholders(template, { iteration: 3, maxIterations: 9, loop: "demo" });
64
- assert.match(filled, /pass 3\/9/);
65
- assert.match(filled, /\{\{phase\}\}/, "unknown placeholders stay intact");
66
- });
67
-
68
- test("stripTemplateHeading removes the human comment line only", () => {
69
- assert.equal(stripTemplateHeading("# Comment line\n\nBody {{x}}\n"), "Body {{x}}");
70
- assert.equal(stripTemplateHeading("Body only\n"), "Body only");
71
- });