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,120 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { spawnSync } from "node:child_process";
3
- import fs from "node:fs";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import test from "node:test";
7
- import { fileURLToPath } from "node:url";
8
- import { initSmokeRepository } from "../loop/oris-loop-fixtures.mjs";
9
- import { ORIS_FLOW_RUNTIME } from "../flow/oris-flow-layout.mjs";
10
-
11
- const scriptsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "loop");
12
-
13
- function runScript(scriptName, args = []) {
14
- return spawnSync(process.execPath, [path.join(scriptsRoot, scriptName), ...args], {
15
- cwd: scriptsRoot,
16
- encoding: "utf8",
17
- env: {
18
- ...process.env,
19
- ORIS_SKILLS_SCRIPTS_ROOT: scriptsRoot,
20
- },
21
- });
22
- }
23
-
24
- test("same-chat verify passes on temporary repository", () => {
25
- const result = runScript("oris-loop-verify.mjs", ["--temp"]);
26
- assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
27
- assert.match(result.stdout, /Same-chat verification passed\./);
28
- });
29
-
30
- test("phase advance moves through declared phases", () => {
31
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-advance-"));
32
- try {
33
- const task = initSmokeRepository(root, {
34
- taskSlug: "advance-smoke",
35
- program: "advance-smoke",
36
- phases: ["test", "fix", "clean"],
37
- });
38
- const chat = path.join(scriptsRoot, "oris-loop-chat.mjs");
39
- const common = ["--repository-root", root, "--task", task.taskPath];
40
- const start = spawnSync(process.execPath, [chat, "--action", "start", ...common], { encoding: "utf8" });
41
- assert.equal(start.status, 0, `${start.stdout}\n${start.stderr}`);
42
-
43
- for (let index = 0; index < 2; index += 1) {
44
- const advance = spawnSync(process.execPath, [
45
- chat,
46
- "--action", "set-state",
47
- "--state", "advance",
48
- "--progress", "yes",
49
- "--repository-root", root,
50
- ], { encoding: "utf8" });
51
- assert.equal(advance.status, 0, `${advance.stdout}\n${advance.stderr}`);
52
- }
53
-
54
- const state = JSON.parse(fs.readFileSync(path.join(root, ORIS_FLOW_RUNTIME, "chat-active.json"), "utf8"));
55
- assert.equal(state.active, true);
56
- assert.equal(state.state, "advance");
57
- assert.equal(state.noProgressStreak, 0);
58
- assert.deepEqual(state.roles, ["executor", "verifier"]);
59
- assert.equal(state.phaseName, "clean");
60
- } finally {
61
- fs.rmSync(root, { recursive: true, force: true });
62
- }
63
- });
64
-
65
- test("no-progress passes force the loop to blocked", () => {
66
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-noprog-"));
67
- try {
68
- const task = initSmokeRepository(root, {
69
- taskSlug: "noprog-smoke",
70
- program: "noprog-smoke",
71
- });
72
- const chat = path.join(scriptsRoot, "oris-loop-chat.mjs");
73
- const start = spawnSync(process.execPath, [
74
- chat, "--action", "start", "--repository-root", root, "--task", task.taskPath,
75
- ], { encoding: "utf8" });
76
- assert.equal(start.status, 0, `${start.stdout}\n${start.stderr}`);
77
-
78
- // maxNoProgress is 2 in the smoke loop document.
79
- for (let index = 0; index < 2; index += 1) {
80
- const step = spawnSync(process.execPath, [
81
- chat, "--action", "set-state", "--state", "continue", "--progress", "no", "--repository-root", root,
82
- ], { encoding: "utf8" });
83
- assert.equal(step.status, 0, `${step.stdout}\n${step.stderr}`);
84
- }
85
-
86
- const state = JSON.parse(fs.readFileSync(path.join(root, ORIS_FLOW_RUNTIME, "chat-active.json"), "utf8"));
87
- assert.equal(state.state, "blocked");
88
- assert.equal(state.active, false);
89
- assert.equal(state.stopReason, "no-progress limit exhausted");
90
- assert.equal(state.noProgressStreak, 2);
91
- } finally {
92
- fs.rmSync(root, { recursive: true, force: true });
93
- }
94
- });
95
-
96
- test("same-chat start accepts an existing loop slug", () => {
97
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-slug-"));
98
- try {
99
- initSmokeRepository(root, {
100
- taskSlug: "feature-121313-therapy-quality-hardening",
101
- program: "therapy-quality-hardening",
102
- });
103
- const chat = path.join(scriptsRoot, "oris-loop-chat.mjs");
104
- const start = spawnSync(process.execPath, [
105
- chat,
106
- "--action", "start",
107
- "--loop", "feature-121313-therapy-quality-hardening",
108
- "--repository-root", root,
109
- ], { encoding: "utf8" });
110
- assert.equal(start.status, 0, `${start.stdout}\n${start.stderr}`);
111
-
112
- const state = JSON.parse(fs.readFileSync(path.join(root, ORIS_FLOW_RUNTIME, "chat-active.json"), "utf8"));
113
- assert.equal(state.active, true);
114
- assert.equal(state.taskPath, "feature-121313-therapy-quality-hardening");
115
- assert.equal(state.loopPath, ".oris-flow/loops/feature-121313-therapy-quality-hardening/loop.md");
116
- } finally {
117
- fs.rmSync(root, { recursive: true, force: true });
118
- }
119
- });
120
-
@@ -1,432 +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 { fileURLToPath } from "node:url";
8
- import { ORIS_FLOW_RUNTIME } from "../flow/oris-flow-layout.mjs";
9
-
10
- const scriptsRoot = path.dirname(fileURLToPath(import.meta.url));
11
- const hook = path.join(scriptsRoot, "..", "loop", "oris-loop-stop.mjs");
12
-
13
- function runHook(root, input, state, extraArgs = []) {
14
- const statePath = path.join(root, "chat-active.json");
15
- if (state !== undefined) {
16
- fs.writeFileSync(statePath, `${JSON.stringify(state)}\n`, "utf8");
17
- }
18
- const result = spawnSync(process.execPath, [hook, ...extraArgs], {
19
- input: JSON.stringify(input),
20
- encoding: "utf8",
21
- env: { ...process.env, ORIS_LOOP_CHAT_STATE: statePath },
22
- });
23
- assert.equal(result.status, 0, result.stderr);
24
- return {
25
- output: JSON.parse(result.stdout.trim()),
26
- statePath,
27
- state: fs.existsSync(statePath)
28
- ? JSON.parse(fs.readFileSync(statePath, "utf8"))
29
- : undefined,
30
- };
31
- }
32
-
33
- function activeState(overrides = {}) {
34
- return {
35
- schemaVersion: 1,
36
- active: true,
37
- state: "continue",
38
- program: "custom",
39
- phases: ["test", "fix", "clean"],
40
- phaseIndex: 0,
41
- phaseName: "test",
42
- phaseIteration: 0,
43
- taskPath: "sample",
44
- loopPath: ".oris-flow/loops/sample/loop.md",
45
- contextPath: ".oris-flow/loops/sample/context.md",
46
- promptsPath: ".oris-flow/loops/sample/prompts",
47
- improveMode: "auto",
48
- models: {
49
- default: "inherit",
50
- executor: "model-e",
51
- verifier: "model-v",
52
- doctor: "model-d",
53
- debriefer: "inherit",
54
- },
55
- roles: ["executor", "verifier", "doctor"],
56
- conversationId: null,
57
- iteration: 0,
58
- maxIterations: 5,
59
- noProgressStreak: 0,
60
- maxNoProgress: 2,
61
- maxMinutes: 30,
62
- startedAt: new Date().toISOString(),
63
- ...overrides,
64
- };
65
- }
66
-
67
- test("is a no-op without active state", () => {
68
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
69
- try {
70
- const result = runHook(root, {
71
- conversation_id: "c1",
72
- status: "completed",
73
- loop_count: 0,
74
- });
75
- assert.deepEqual(result.output, {});
76
- } finally {
77
- fs.rmSync(root, { recursive: true, force: true });
78
- }
79
- });
80
-
81
- test("cursor: claims the conversation and injects the orchestrator prompt", () => {
82
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
83
- try {
84
- const result = runHook(
85
- root,
86
- { conversation_id: "c1", status: "completed", loop_count: 0 },
87
- activeState(),
88
- );
89
- const message = result.output.followup_message;
90
- assert.match(message, /ORIS LOOP RUN — pass 1\/5/);
91
- assert.match(message, /Loop: custom \| Phase: test \(1\/3\)/);
92
- assert.match(message, /Definition: \.oris-flow\/loops\/sample\/loop\.md/);
93
- assert.match(message, /Prompts: \.oris-flow\/loops\/sample\/prompts/);
94
- assert.match(message, /executor=model-e verifier=model-v doctor=model-d debriefer=inherit/);
95
- assert.match(message, /Active roles: executor, verifier, doctor/);
96
- assert.match(message, /Improve mode: auto/);
97
- assert.match(message, /ROLE: you are the orchestrator only/);
98
- assert.match(message, /SPAWN executor subagent/);
99
- assert.match(message, /SPAWN verifier subagent/);
100
- assert.match(message, /SPAWN doctor subagent/);
101
- assert.match(message, /npx oris-skills loop chat --action set-state --state/);
102
- assert.match(message, /--progress <yes\|no>/);
103
- assert.match(message, /ORIS_LOOP_STATE: <continue\|advance\|complete\|blocked\|ask-user>/);
104
- assert.doesNotMatch(message, /\{\{iteration\}\}/, "runtime placeholders must be substituted");
105
- assert.equal(result.state.conversationId, "c1");
106
- assert.equal(result.state.iteration, 1);
107
- assert.equal(result.state.state, "running");
108
- } finally {
109
- fs.rmSync(root, { recursive: true, force: true });
110
- }
111
- });
112
-
113
- test("claude: emits decision block with the same orchestrator prompt", () => {
114
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
115
- try {
116
- const result = runHook(
117
- root,
118
- { session_id: "s1", hook_event_name: "Stop", stop_hook_active: false, cwd: root },
119
- activeState(),
120
- ["--platform", "claude"],
121
- );
122
- assert.equal(result.output.decision, "block");
123
- assert.match(result.output.reason, /ORIS LOOP RUN — pass 1\/5/);
124
- assert.match(result.output.reason, /ROLE: you are the orchestrator only/);
125
- assert.equal(result.state.conversationId, "s1");
126
- assert.equal(result.state.iteration, 1);
127
- } finally {
128
- fs.rmSync(root, { recursive: true, force: true });
129
- }
130
- });
131
-
132
- test("claude: platform is auto-detected from Stop payload", () => {
133
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
134
- try {
135
- const result = runHook(
136
- root,
137
- { session_id: "s2", hook_event_name: "Stop", stop_hook_active: true, cwd: root },
138
- activeState(),
139
- );
140
- assert.equal(result.output.decision, "block");
141
- assert.ok(result.output.reason.length > 0);
142
- } finally {
143
- fs.rmSync(root, { recursive: true, force: true });
144
- }
145
- });
146
-
147
- test("cursor: auto-detected even when the payload also carries session_id", () => {
148
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
149
- try {
150
- // Real Cursor payloads include BOTH conversation_id and session_id.
151
- const result = runHook(
152
- root,
153
- { conversation_id: "c9", session_id: "s9", status: "completed", loop_count: 0 },
154
- activeState(),
155
- );
156
- assert.ok(result.output.followup_message, "must answer in Cursor format");
157
- assert.equal(result.output.decision, undefined);
158
- assert.equal(result.state.conversationId, "c9");
159
- } finally {
160
- fs.rmSync(root, { recursive: true, force: true });
161
- }
162
- });
163
-
164
- test("per-loop prompts/orchestrator.md overrides the default template", () => {
165
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
166
- try {
167
- const promptsDir = path.join(root, ".oris-flow", "loops", "sample", "prompts");
168
- fs.mkdirSync(promptsDir, { recursive: true });
169
- fs.writeFileSync(path.join(promptsDir, "orchestrator.md"), "# Custom orchestrator\nCUSTOM PASS {{iteration}} for {{loop}}\n", "utf8");
170
- const result = runHook(
171
- root,
172
- { conversation_id: "c1", status: "completed", loop_count: 0, workspace_roots: [root] },
173
- activeState({ repositoryRoot: root.replaceAll("\\", "/") }),
174
- );
175
- assert.equal(result.output.followup_message, "CUSTOM PASS 1 for custom");
176
- } finally {
177
- fs.rmSync(root, { recursive: true, force: true });
178
- }
179
- });
180
-
181
- test("does not continue another conversation or failed stop", () => {
182
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
183
- try {
184
- const mismatch = runHook(
185
- root,
186
- { conversation_id: "other", status: "completed", loop_count: 0 },
187
- activeState({ conversationId: "bound" }),
188
- );
189
- assert.deepEqual(mismatch.output, {});
190
- const failed = runHook(
191
- root,
192
- { conversation_id: "bound", status: "error", loop_count: 0 },
193
- activeState({ conversationId: "bound" }),
194
- );
195
- assert.deepEqual(failed.output, {});
196
- } finally {
197
- fs.rmSync(root, { recursive: true, force: true });
198
- }
199
- });
200
-
201
- test("stops on terminal state and exhausted limits", () => {
202
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
203
- try {
204
- for (const state of ["complete", "blocked", "cancelled"]) {
205
- const terminal = runHook(
206
- root,
207
- { conversation_id: "c1", status: "completed", loop_count: 0 },
208
- activeState({ state, conversationId: "c1" }),
209
- );
210
- assert.deepEqual(terminal.output, {});
211
- }
212
-
213
- const limited = runHook(
214
- root,
215
- { conversation_id: "c1", status: "completed", loop_count: 2 },
216
- activeState({
217
- conversationId: "c1",
218
- iteration: 2,
219
- maxIterations: 2,
220
- }),
221
- );
222
- assert.deepEqual(limited.output, {});
223
- assert.equal(limited.state.active, false);
224
- assert.equal(limited.state.state, "blocked");
225
-
226
- const expired = runHook(
227
- root,
228
- { conversation_id: "c1", status: "completed", loop_count: 0 },
229
- activeState({
230
- conversationId: "c1",
231
- maxMinutes: 1,
232
- startedAt: new Date(Date.now() - 120_000).toISOString(),
233
- }),
234
- );
235
- assert.deepEqual(expired.output, {});
236
- assert.equal(expired.state.state, "blocked");
237
-
238
- const stalled = runHook(
239
- root,
240
- { conversation_id: "c1", status: "completed", loop_count: 0 },
241
- activeState({ conversationId: "c1", noProgressStreak: 2, maxNoProgress: 2 }),
242
- );
243
- assert.deepEqual(stalled.output, {});
244
- assert.equal(stalled.state.state, "blocked");
245
- assert.equal(stalled.state.stopReason, "no-progress limit exhausted");
246
- } finally {
247
- fs.rmSync(root, { recursive: true, force: true });
248
- }
249
- });
250
-
251
- test("claude: stopping on an exhausted limit tells the user via systemMessage", () => {
252
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
253
- try {
254
- const result = runHook(
255
- root,
256
- { session_id: "s1", hook_event_name: "Stop", stop_hook_active: false, cwd: root },
257
- activeState({ conversationId: "s1", iteration: 5, maxIterations: 5 }),
258
- ["--platform", "claude"],
259
- );
260
- assert.match(result.output.systemMessage, /iteration limit exhausted/);
261
- assert.equal(result.output.decision, undefined, "must not block the chat");
262
- assert.equal(result.state.state, "blocked");
263
- } finally {
264
- fs.rmSync(root, { recursive: true, force: true });
265
- }
266
- });
267
-
268
- test("ask-user pauses the loop: the hook does not resume it", () => {
269
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
270
- try {
271
- const result = runHook(
272
- root,
273
- { conversation_id: "c1", status: "completed", loop_count: 0 },
274
- activeState({ state: "ask-user", conversationId: "c1" }),
275
- );
276
- assert.deepEqual(result.output, {});
277
- assert.equal(result.state.state, "ask-user", "pause must not be rewritten");
278
- } finally {
279
- fs.rmSync(root, { recursive: true, force: true });
280
- }
281
- });
282
-
283
- test("is a no-op for corrupted runtime state", () => {
284
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
285
- const statePath = path.join(root, "chat-active.json");
286
- try {
287
- fs.writeFileSync(statePath, "{broken", "utf8");
288
- const result = spawnSync(process.execPath, [hook], {
289
- input: JSON.stringify({ conversation_id: "c1", status: "completed", loop_count: 0 }),
290
- encoding: "utf8",
291
- env: { ...process.env, ORIS_LOOP_CHAT_STATE: statePath },
292
- });
293
- assert.equal(result.status, 0, result.stderr);
294
- assert.deepEqual(JSON.parse(result.stdout.trim()), {});
295
- } finally {
296
- fs.rmSync(root, { recursive: true, force: true });
297
- }
298
- });
299
-
300
- test("does not continue unsupported runtime schema", () => {
301
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
302
- try {
303
- const result = runHook(
304
- root,
305
- { conversation_id: "c1", status: "completed", loop_count: 0 },
306
- activeState({ schemaVersion: 99 }),
307
- );
308
- assert.deepEqual(result.output, {});
309
- } finally {
310
- fs.rmSync(root, { recursive: true, force: true });
311
- }
312
- });
313
-
314
- test("continues when the previous hook left state running", () => {
315
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
316
- try {
317
- const result = runHook(
318
- root,
319
- { conversation_id: "c1", status: "completed", loop_count: 0 },
320
- activeState({ state: "running", conversationId: "c1", iteration: 1, phaseIteration: 1 }),
321
- );
322
- assert.match(result.output.followup_message, /pass 2\/5/);
323
- assert.equal(result.state.iteration, 2);
324
- assert.equal(result.state.state, "running");
325
- } finally {
326
- fs.rmSync(root, { recursive: true, force: true });
327
- }
328
- });
329
-
330
- test("resolves runtime state from workspace_roots", () => {
331
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
332
- const repo = path.join(root, "repo");
333
- const runtimeDir = path.join(repo, ORIS_FLOW_RUNTIME);
334
- fs.mkdirSync(runtimeDir, { recursive: true });
335
- const statePath = path.join(runtimeDir, "chat-active.json");
336
- try {
337
- fs.writeFileSync(statePath, `${JSON.stringify(activeState())}\n`, "utf8");
338
- const result = spawnSync(process.execPath, [hook], {
339
- input: JSON.stringify({
340
- conversation_id: "c1",
341
- status: "completed",
342
- loop_count: 0,
343
- workspace_roots: [repo],
344
- }),
345
- encoding: "utf8",
346
- cwd: root,
347
- });
348
- assert.equal(result.status, 0, result.stderr);
349
- const output = JSON.parse(result.stdout.trim());
350
- assert.match(output.followup_message, /ORIS LOOP RUN/);
351
- const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
352
- assert.equal(state.conversationId, "c1");
353
- } finally {
354
- fs.rmSync(root, { recursive: true, force: true });
355
- }
356
- });
357
-
358
- test("advance uses the same conversation with phase instruction", () => {
359
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
360
- try {
361
- const result = runHook(
362
- root,
363
- { conversation_id: "c1", status: "completed", loop_count: 0 },
364
- activeState({ state: "advance", conversationId: "c1" }),
365
- );
366
- assert.match(result.output.followup_message, /advance to the next declared phase/);
367
- } finally {
368
- fs.rmSync(root, { recursive: true, force: true });
369
- }
370
- });
371
-
372
- test("writes debug reason when conversation id is missing", () => {
373
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
374
- try {
375
- const result = runHook(
376
- root,
377
- { status: "completed", loop_count: 0 },
378
- activeState(),
379
- );
380
- assert.deepEqual(result.output, {});
381
- const debugPath = path.join(root, "hook-debug.jsonl");
382
- assert.ok(fs.existsSync(debugPath));
383
- const lines = fs.readFileSync(debugPath, "utf8").trim().split("\n");
384
- const last = JSON.parse(lines[lines.length - 1]);
385
- assert.equal(last.decision, "noop");
386
- assert.equal(last.reason, "missing-conversation-id");
387
- } finally {
388
- fs.rmSync(root, { recursive: true, force: true });
389
- }
390
- });
391
-
392
- test("writes debug reason when status is aborted", () => {
393
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
394
- try {
395
- runHook(
396
- root,
397
- { conversation_id: "c1", status: "aborted", loop_count: 0 },
398
- activeState(),
399
- );
400
- const debugPath = path.join(root, "hook-debug.jsonl");
401
- const last = JSON.parse(fs.readFileSync(debugPath, "utf8").trim().split("\n").at(-1));
402
- assert.equal(last.reason, "status-not-completed:aborted");
403
- } finally {
404
- fs.rmSync(root, { recursive: true, force: true });
405
- }
406
- });
407
-
408
- test("parses hook stdin with UTF-8 BOM from Cursor on Windows", () => {
409
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-hook-"));
410
- const statePath = path.join(root, "chat-active.json");
411
- try {
412
- fs.writeFileSync(statePath, `${JSON.stringify(activeState())}\n`, "utf8");
413
- const payload = JSON.stringify({
414
- conversation_id: "c1",
415
- status: "completed",
416
- loop_count: 0,
417
- });
418
- const result = spawnSync(process.execPath, [hook], {
419
- input: `${payload}`,
420
- encoding: "utf8",
421
- env: { ...process.env, ORIS_LOOP_CHAT_STATE: statePath },
422
- });
423
- assert.equal(result.status, 0, result.stderr);
424
- const output = JSON.parse(result.stdout.trim());
425
- assert.match(output.followup_message, /ORIS LOOP RUN/);
426
- const debugPath = path.join(root, "hook-debug.jsonl");
427
- const last = JSON.parse(fs.readFileSync(debugPath, "utf8").trim().split("\n").at(-1));
428
- assert.equal(last.decision, "followup");
429
- } finally {
430
- fs.rmSync(root, { recursive: true, force: true });
431
- }
432
- });
@@ -1,59 +0,0 @@
1
- # Craft a loop — four decisions, everything else defaults
2
-
3
- Interview → draft prompts → user approves → write. The user owns the final wording of
4
- every prompt. A good loop needs exactly FOUR decisions; the rest has a stated default.
5
-
6
- ## Interview (four decisions, one per turn)
7
-
8
- READ first: `.oris-flow/manifest.json` + relevant maps — project facts live there; NEVER
9
- re-ask them. Question kernel: `references/conventions.md`.
10
-
11
- 1. GOAL — what does "done" look like? One sentence; it becomes the loop's `Goal:`.
12
- 2. PROOF — how does a pass PROVE it worked? Commands, criteria, observable behavior —
13
- this feeds the VERIFIER prompt verbatim, including when `goalMet` becomes true.
14
- Weak proof = weak loop; push hardest here.
15
- 3. SCOPE — what may the loop inspect and change; what is off-limits.
16
- 4. STOP — when to stop, ask for help, or declare no-progress. One sentence; becomes `Stop:`.
17
-
18
- DEFAULTS — state them once in the recap, change only if the user objects or the work
19
- demands it: roles executor + verifier · limits 10 passes / 240 minutes · models `inherit`
20
- · `improve.mode: propose`. OFFER doctor ONLY for multi-phase stop logic; debriefer ONLY
21
- for long unattended runs (and required for `improve.mode: auto`).
22
-
23
- INFER phases and structure from the answers — NEVER ask the user to design schemas or
24
- subagent contracts. Fresh evidence could not change the next action → recommend a
25
- one-shot workflow instead of a loop.
26
-
27
- ## Draft prompts
28
-
29
- - COPY templates from `skills/oris-flow/templates/` — ONLY the active roles.
30
- - FILL the CAPS placeholders from the four decisions; the verifier prompt embeds
31
- decision 2 (PROOF) verbatim.
32
- - KEEP runtime placeholders (`{{iteration}}`, `{{phase}}`, `{{currentTarget}}`) — the
33
- orchestrator fills them each pass.
34
- - TRACE one full pass silently (observe → act → verify → record → stop rule). REPAIR weak
35
- verification or vague stops BEFORE showing anything.
36
-
37
- ## Approval gate
38
-
39
- SHOW: goal + stop (2 lines) + the defaults line, then each draft prompt.
40
- ASK one question: write the loop | edit verifier | edit executor | edit doctor | edit debriefer | keep interviewing.
41
- ITERATE until the user chooses "write the loop". NEVER write files before that.
42
-
43
- ## Write
44
-
45
- Under `.oris-flow/loops/{slug}/`:
46
-
47
- - `loop.md` — front matter per `references/loop-contract.md` (`schemaVersion: 2`,
48
- `approved: true`, `roles:` from the interview) + body: `Goal:` and `Stop:` lines.
49
- - `prompts/<role>.md` — one file per active role.
50
- - `context.md` — objective + next action.
51
- - empty `receipts/`, `proposals/`, `history/`.
52
-
53
- THEN `npx oris-skills loop dry-run --loop {slug}` and show the result.
54
- IF the user chose "run now" → follow `skills/oris-flow/references/loop-run.md`.
55
-
56
- ## Adapt an existing loop
57
-
58
- COPY the source loop's prompts, EDIT only what the interview changed, run the same
59
- approval gate. NEVER overwrite the source loop.
@@ -1,32 +0,0 @@
1
- # Improve a loop — evidence first, smallest change
2
-
3
- READ receipts + the evidence they reference BEFORE touching anything. No pattern from a
4
- single run unless the defect is obvious.
5
-
6
- CLASSIFY the cause first:
7
- loop design | execution choice | verifier gap | environment/tooling | changed goal | insufficient evidence.
8
- ONLY loop-design and verifier-gap causes justify changing the loop.
9
-
10
- ## Where changes land
11
-
12
- | Change | improve.mode: propose | improve.mode: auto |
13
- |--------|----------------------|--------------------|
14
- | prompts/*.md wording | proposal in `proposals/` | debriefer edits directly; old file → `history/`; noted in receipt |
15
- | context.md facts | allowed | allowed |
16
- | scope, limits, permissions, verification commands (loop.md) | proposal + explicit user approval | SAME — never automatic |
17
-
18
- RULE: one improvement per debrief, the smallest change the evidence justifies.
19
- APPLYING an approved loop.md change → archive the previous `loop.md` to
20
- `history/loop.vN.md`, record the decision in the next receipt.
21
-
22
- ## Never
23
-
24
- - Let an unapproved proposal affect execution.
25
- - Change scope, limits, permissions, or verification commands without explicit approval —
26
- in ANY improve mode.
27
- - Tune prompts to make the verifier easier to satisfy — fix the loop, not the bar.
28
-
29
- ## Outcome
30
-
31
- Report exactly one:
32
- `Keep loop` | `Tuned prompts` | `Patch proposed` | `Execution issue` | `Environment blocked` | `Inconclusive`.
@@ -1,47 +0,0 @@
1
- # Run a loop — arm once, then obey the hook
2
-
3
- The chat is the control plane, never the worker. Arm the loop, end the turn; every pass
4
- after that is injected by the platform hook and followed exactly.
5
-
6
- ## Arm (first turn)
7
-
8
- 1. RESOLVE the slug: `npx oris-skills loop list --json`.
9
- 2. CHECK readiness: `npx oris-skills loop dry-run --loop <slug>` — validates prompts and
10
- shows the exact pass message.
11
- 3. ARM: `npx oris-skills loop chat --action start --loop <slug>` (rebind a broken runtime
12
- with `--action repair`).
13
- 4. END the turn with one status line. The stop hook injects the next pass automatically.
14
-
15
- `--task <path>` only materializes a NEW loop from a task path — never an alias for a slug.
16
-
17
- ## Each pass (triggered by the hook message)
18
-
19
- The injected message IS the orchestrator prompt (from `prompts/orchestrator.md` or the
20
- shipped default). FOLLOW it exactly:
21
-
22
- - Orchestrator = control plane only. NEVER do executor/verifier/doctor work in the main chat.
23
- - SPAWN one subagent per ACTIVE role (listed in the pass message) with its
24
- `prompts/<role>.md` file, runtime placeholders filled, model from the pass message.
25
- - No doctor among the active roles → map the verifier verdict yourself:
26
- pass + goalMet → complete · pass / fail → continue · blocked / inconclusive → ask-user.
27
- - Windows: workers must not chain commands with `&&`; use the tool working directory.
28
- - RECEIPT per pass (see contract); raw logs → `.oris-flow/runtime/evidence/`.
29
- - CLOSE: `npx oris-skills loop chat --action set-state --state <state> --progress <yes|no>`,
30
- then end the turn with `ORIS_LOOP_STATE: <state>`.
31
- - On ask-user: say what decision is needed; the loop stays paused until
32
- `set-state --state continue`.
33
-
34
- ## Never
35
-
36
- - Start or simulate a pass in the same turn you armed the loop — the hook drives.
37
- - Ask "run next pass?" after continue/advance — END THE TURN instead.
38
- - Invent role instructions: prompt files missing → `set-state blocked`, tell the user to
39
- complete craft.
40
- - Degrade into worker roles when subagents cannot be spawned → `set-state blocked`.
41
- - Classify an error, a missing tool, or an exhausted limit as success.
42
-
43
- ## Headless (Codex / CI)
44
-
45
- `npx oris-skills loop run --loop <slug> --agent codex|claude`
46
- Each active role runs as a separate CLI process with its own prompt file and model
47
- (read-only roles run without write access); receipts and state work the same.