oris-skills 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.cursor-plugin/plugin.json +2 -11
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +79 -31
  4. package/agents/oris-loop-debriefer.md +1 -0
  5. package/agents/oris-loop-doctor.md +1 -1
  6. package/agents/oris-loop-executor.md +5 -2
  7. package/agents/oris-loop-verifier.md +6 -2
  8. package/docs/architecture.md +9 -7
  9. package/docs/distribution.md +1 -1
  10. package/docs/maintainer-guide.md +5 -5
  11. package/docs/user-guide.md +12 -9
  12. package/package.json +2 -3
  13. package/references/clean-code-checklist.md +20 -107
  14. package/references/conventions.md +43 -3
  15. package/references/doc-policy.md +19 -3
  16. package/references/loop-contract.md +24 -17
  17. package/references/loop.schema.json +13 -0
  18. package/references/repo-map.md +3 -3
  19. package/references/repo-map.schema.json +41 -0
  20. package/references/settings.md +2 -1
  21. package/references/settings.schema.json +5 -0
  22. package/scripts/flow/oris-flow-layout.mjs +1 -0
  23. package/scripts/flow/oris-flow-scan.mjs +265 -27
  24. package/scripts/install/generate-agent-adapters.mjs +19 -2
  25. package/scripts/install/install-user-skills.mjs +61 -3
  26. package/scripts/install/uninstall-user-skills.mjs +17 -1
  27. package/scripts/loop/oris-loop-bootstrap.mjs +7 -2
  28. package/scripts/loop/oris-loop-chat.mjs +34 -16
  29. package/scripts/loop/oris-loop-demo.mjs +11 -20
  30. package/scripts/loop/oris-loop-document.mjs +24 -7
  31. package/scripts/loop/oris-loop-dry-run.mjs +8 -4
  32. package/scripts/loop/oris-loop-fixtures.mjs +13 -12
  33. package/scripts/loop/oris-loop-run.mjs +80 -20
  34. package/scripts/loop/oris-loop-stop.mjs +48 -9
  35. package/scripts/loop/oris-loop-templates.mjs +13 -10
  36. package/scripts/loop/oris-loop-verify.mjs +1 -2
  37. package/scripts/oris-skills.mjs +3 -2
  38. package/scripts/tests/run-all-tests.mjs +1 -2
  39. package/scripts/tests/test-oris-flow-scan.mjs +109 -1
  40. package/scripts/tests/test-oris-loop-document.mjs +39 -3
  41. package/scripts/tests/test-oris-loop-run.mjs +28 -2
  42. package/scripts/tests/test-oris-loop-smoke.mjs +36 -3
  43. package/scripts/tests/test-oris-loop-stop.mjs +65 -4
  44. package/scripts/tests/test-routing-lifecycle.mjs +38 -23
  45. package/scripts/tests/test-skill-style.mjs +64 -0
  46. package/skills/oris-flow/SKILL.md +53 -25
  47. package/skills/oris-flow/references/architecture.md +77 -0
  48. package/skills/oris-flow/references/change.md +60 -0
  49. package/skills/oris-flow/references/criteria.md +60 -0
  50. package/skills/oris-flow/references/discover.md +66 -0
  51. package/skills/oris-flow/references/docs.md +33 -0
  52. package/skills/oris-flow/references/fix.md +49 -0
  53. package/skills/oris-flow/references/help.md +34 -0
  54. package/skills/oris-flow/references/implement.md +48 -0
  55. package/skills/oris-flow/references/loop-craft.md +59 -0
  56. package/skills/oris-flow/references/loop-improve.md +32 -0
  57. package/skills/oris-flow/references/loop-run.md +47 -0
  58. package/skills/oris-flow/references/loop.md +56 -0
  59. package/skills/oris-flow/references/new.md +64 -0
  60. package/skills/oris-flow/references/plan.md +58 -0
  61. package/skills/oris-flow/references/setup.md +100 -0
  62. package/skills/oris-flow/references/verify.md +57 -0
  63. package/skills/{oris-loop → oris-flow}/templates/debriefer.md +2 -1
  64. package/skills/oris-flow/templates/doctor.md +22 -0
  65. package/skills/{oris-loop → oris-flow}/templates/executor.md +8 -2
  66. package/skills/{oris-loop → oris-flow}/templates/orchestrator.md +10 -7
  67. package/skills/{oris-loop → oris-flow}/templates/verifier.md +6 -3
  68. package/references/questions.md +0 -38
  69. package/skills/oris-flow-criteria/SKILL.md +0 -49
  70. package/skills/oris-flow-discover/SKILL.md +0 -58
  71. package/skills/oris-flow-docs/SKILL.md +0 -31
  72. package/skills/oris-flow-fix/SKILL.md +0 -42
  73. package/skills/oris-flow-implement/SKILL.md +0 -43
  74. package/skills/oris-flow-plan/SKILL.md +0 -51
  75. package/skills/oris-flow-setup/SKILL.md +0 -49
  76. package/skills/oris-help/SKILL.md +0 -30
  77. package/skills/oris-help/agents/openai.yaml +0 -4
  78. package/skills/oris-loop/SKILL.md +0 -66
  79. package/skills/oris-loop/agents/openai.yaml +0 -4
  80. package/skills/oris-loop/references/craft.md +0 -48
  81. package/skills/oris-loop/references/improve.md +0 -23
  82. package/skills/oris-loop/references/run.md +0 -30
  83. package/skills/oris-loop/templates/doctor.md +0 -20
@@ -28,6 +28,11 @@ function parseArgs(argv) {
28
28
  cursorRoot: path.join(home, ".cursor"),
29
29
  claudeRoot: path.join(home, ".claude"),
30
30
  codexRoot: path.join(home, ".codex"),
31
+ copilotRoot: process.platform === "win32"
32
+ ? path.join(home, "AppData", "Roaming", "Code", "User")
33
+ : process.platform === "darwin"
34
+ ? path.join(home, "Library", "Application Support", "Code", "User")
35
+ : path.join(home, ".config", "Code", "User"),
31
36
  };
32
37
  for (let i = 0; i < argv.length; i += 1) {
33
38
  const raw = argv[i];
@@ -44,6 +49,7 @@ function parseArgs(argv) {
44
49
  else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.cursorRoot = value();
45
50
  else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.claudeRoot = value();
46
51
  else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.codexRoot = value();
52
+ else if (key === "--copilot-root" || key.startsWith("--copilot-root=")) options.copilotRoot = value();
47
53
  else fail(`Unknown argument: ${raw}`);
48
54
  }
49
55
  return options;
@@ -86,6 +92,14 @@ function managedCodexPrompts(codexRoot) {
86
92
  .map((entry) => path.join(promptsRoot, entry.name));
87
93
  }
88
94
 
95
+ function managedCopilotPrompts(copilotRoot) {
96
+ const promptsRoot = path.join(copilotRoot, "prompts");
97
+ if (!fs.existsSync(promptsRoot)) return [];
98
+ return fs.readdirSync(promptsRoot, { withFileTypes: true })
99
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().startsWith("oris-") && entry.name.endsWith(".prompt.md"))
100
+ .map((entry) => path.join(promptsRoot, entry.name));
101
+ }
102
+
89
103
  function legacyOrisPaths(options) {
90
104
  return [
91
105
  path.join(options.cursorRoot, "oris-skills"),
@@ -104,6 +118,8 @@ function plannedRemovals(options) {
104
118
  ...managedSkillDirs(path.join(options.cursorRoot, "skills")),
105
119
  ...managedSkillDirs(path.join(options.claudeRoot, "skills")),
106
120
  ...managedCodexPrompts(options.codexRoot),
121
+ ...managedCopilotPrompts(options.copilotRoot),
122
+ path.join(path.dirname(options.bundleRoot), "oris-flow.md"),
107
123
  ...legacyOrisPaths(options),
108
124
  ]);
109
125
  return [...paths].map((target) => path.resolve(target)).filter(safeOrisPath).sort();
@@ -125,7 +141,7 @@ function removeOrisMcpServers(userMcpPath, dryRun) {
125
141
 
126
142
  export function uninstallUserSkills(rawArgs = []) {
127
143
  const options = parseArgs(rawArgs);
128
- for (const target of [options.bundleRoot, options.settingsPath, options.cursorRoot, options.claudeRoot, options.codexRoot]) {
144
+ for (const target of [options.bundleRoot, options.settingsPath, options.cursorRoot, options.claudeRoot, options.codexRoot, options.copilotRoot]) {
129
145
  assertInsideHome(target);
130
146
  }
131
147
 
@@ -7,6 +7,7 @@ import {
7
7
  ORIS_FLOW_LOOPS_ROOT,
8
8
  ORIS_FLOW_MANIFEST,
9
9
  ORIS_FLOW_RUNTIME,
10
+ ORIS_FLOW_TASKS_ROOT,
10
11
  ORIS_LOOP_SCHEMA,
11
12
  ORIS_LOOP_SCHEMA_VERSION,
12
13
  } from "../flow/oris-flow-layout.mjs";
@@ -135,9 +136,10 @@ export function buildAdapterDraft(repositoryRoot, hints = {}) {
135
136
  ".oris-flow/maps",
136
137
  ...(Array.isArray(hints.allowed) ? hints.allowed : []),
137
138
  ]);
138
- for (const optionalPath of ["docs", "docs/tasks", "specs", "src", "test", "tests"]) {
139
+ for (const optionalPath of ["docs", "docs/tasks", ORIS_FLOW_TASKS_ROOT, "specs", "src", "test", "tests"]) {
139
140
  if (fs.existsSync(path.join(repositoryRoot, optionalPath))) allowed.add(optionalPath);
140
141
  }
142
+ allowed.add(ORIS_FLOW_TASKS_ROOT);
141
143
  if (testDir) allowed.add(testDir);
142
144
 
143
145
  const commands = {
@@ -155,7 +157,10 @@ export function buildAdapterDraft(repositoryRoot, hints = {}) {
155
157
  schemaVersion: ORIS_LOOP_SCHEMA_VERSION,
156
158
  repository,
157
159
  paths: {
158
- taskRoots: fs.existsSync(path.join(repositoryRoot, "docs", "tasks")) ? ["docs/tasks"] : [],
160
+ taskRoots: [
161
+ ORIS_FLOW_TASKS_ROOT,
162
+ ...(fs.existsSync(path.join(repositoryRoot, "docs", "tasks")) ? ["docs/tasks"] : []),
163
+ ],
159
164
  runtime: ORIS_FLOW_RUNTIME,
160
165
  loops: ORIS_FLOW_LOOPS_ROOT,
161
166
  allowed: [...allowed],
@@ -18,6 +18,7 @@ import {
18
18
  ORIS_SKILLS_SETTINGS,
19
19
  } from "../flow/oris-flow-layout.mjs";
20
20
  import {
21
+ activeRoles,
21
22
  assessPromptFiles,
22
23
  improveMode,
23
24
  normalizeModels,
@@ -36,10 +37,10 @@ function parseArgs(argv) {
36
37
  loopSlug: "",
37
38
  taskPath: "",
38
39
  state: "continue",
40
+ progress: "",
39
41
  repositoryRoot: "",
40
42
  runner: "chat",
41
- maxIterations: 25,
42
- maxRepairCycles: 10,
43
+ maxIterations: 10,
43
44
  maxMinutes: 240,
44
45
  profile: "",
45
46
  settingsPath: ORIS_SKILLS_SETTINGS,
@@ -56,6 +57,7 @@ function parseArgs(argv) {
56
57
  else if (key === "--loop" || key === "--loop-slug" || key.startsWith("--loop=") || key.startsWith("--loop-slug=")) [i, options.loopSlug] = readValue(i, raw);
57
58
  else if (key === "--task" || key === "--task-path" || key.startsWith("--task=") || key.startsWith("--task-path=")) [i, options.taskPath] = readValue(i, raw);
58
59
  else if (key === "--state" || key.startsWith("--state=")) [i, options.state] = readValue(i, raw);
60
+ else if (key === "--progress" || key.startsWith("--progress=")) [i, options.progress] = readValue(i, raw);
59
61
  else if (key === "--repository-root" || key.startsWith("--repository-root=")) [i, options.repositoryRoot] = readValue(i, raw);
60
62
  else if (key === "--runner" || key.startsWith("--runner=")) [i, options.runner] = readValue(i, raw);
61
63
  else if (key === "--profile" || key.startsWith("--profile=")) [i, options.profile] = readValue(i, raw);
@@ -64,10 +66,6 @@ function parseArgs(argv) {
64
66
  const parsed = readValue(i, raw);
65
67
  i = parsed[0];
66
68
  options.maxIterations = Number.parseInt(parsed[1], 10);
67
- } else if (key === "--max-repair-cycles" || key.startsWith("--max-repair-cycles=")) {
68
- const parsed = readValue(i, raw);
69
- i = parsed[0];
70
- options.maxRepairCycles = Number.parseInt(parsed[1], 10);
71
69
  } else if (key === "--max-minutes" || key.startsWith("--max-minutes=")) {
72
70
  const parsed = readValue(i, raw);
73
71
  i = parsed[0];
@@ -76,9 +74,11 @@ function parseArgs(argv) {
76
74
  }
77
75
  options.action = options.action.toLowerCase();
78
76
  options.state = options.state.toLowerCase();
77
+ options.progress = options.progress.toLowerCase();
79
78
  options.runner = options.runner.toLowerCase();
80
79
  if (!["start", "set-state", "stop", "status", "repair"].includes(options.action)) fail("Unsupported --action.");
81
- if (!["continue", "advance", "complete", "blocked", "cancelled"].includes(options.state)) fail("Unsupported --state.");
80
+ if (!["continue", "advance", "complete", "blocked", "cancelled", "ask-user"].includes(options.state)) fail("Unsupported --state.");
81
+ if (options.progress && !["yes", "no"].includes(options.progress)) fail("Unsupported --progress (yes|no).");
82
82
  if (!["chat", "headless"].includes(options.runner)) fail("Unsupported --runner (chat|headless).");
83
83
  return options;
84
84
  }
@@ -133,7 +133,7 @@ function reconcileContext(root, state) {
133
133
  "Current phase index": Number(state.phaseIndex ?? 0),
134
134
  "Current iteration": Number(state.iteration ?? 0),
135
135
  "Phase iteration": Number(state.phaseIteration ?? 0),
136
- "Repair cycles": Number(state.repairCycles ?? 0),
136
+ "No-progress streak": Number(state.noProgressStreak ?? 0),
137
137
  Updated: new Date().toISOString(),
138
138
  })) {
139
139
  content = setContextField(content, name, value);
@@ -213,7 +213,7 @@ function normalizeChatState(state) {
213
213
  normalized.phaseName = currentPhase(normalized);
214
214
  normalized.iteration = Number(normalized.iteration ?? 0);
215
215
  normalized.phaseIteration = Number(normalized.phaseIteration ?? 0);
216
- normalized.repairCycles = Number(normalized.repairCycles ?? 0);
216
+ normalized.noProgressStreak = Number(normalized.noProgressStreak ?? 0);
217
217
  normalized.phaseIndex = Number.isInteger(Number(normalized.phaseIndex)) ? Number(normalized.phaseIndex) : 0;
218
218
  if (normalized.state === "running" && normalized.active === true) {
219
219
  normalized.state = "continue";
@@ -283,13 +283,14 @@ if (options.action === "start") {
283
283
  } catch (error) {
284
284
  fail(error.message);
285
285
  }
286
- const prompts = assessPromptFiles(path.dirname(loopPath));
286
+ const prompts = assessPromptFiles(path.dirname(loopPath), activeRoles(loopDocument.data));
287
287
  if (prompts.missing.length > 0) {
288
- fail(`Loop prompt files are missing or empty: ${prompts.missing.join(", ")}. Craft or copy them from skills/oris-loop/templates/.`);
288
+ fail(`Loop prompt files are missing or empty: ${prompts.missing.join(", ")}. Craft or copy them from skills/oris-flow/templates/.`);
289
289
  }
290
290
  if (fs.existsSync(statePath)) {
291
291
  const existing = normalizeChatState(JSON.parse(fs.readFileSync(statePath, "utf8")));
292
- if (existing.active === true && !terminalStates.has(existing.state)) {
292
+ // ask-user is a pause awaiting input, not an active run restart may take over.
293
+ if (existing.active === true && !terminalStates.has(existing.state) && existing.state !== "ask-user") {
293
294
  fail(`An Oris chat loop is already active for this workspace: ${existing.taskPath}. Run --action stop before starting again.`);
294
295
  }
295
296
  }
@@ -310,13 +311,14 @@ if (options.action === "start") {
310
311
  loopPath: loopDocs.loopRelative,
311
312
  contextPath: loopDocs.contextRelative,
312
313
  promptsPath: loopDocs.promptsRelative,
314
+ roles: activeRoles(loopMetadata),
313
315
  models: normalizeModels(loopMetadata.models, adapter.models),
314
316
  improveMode: improveMode(loopMetadata),
315
317
  conversationId: null,
316
318
  iteration: 0,
317
319
  maxIterations: loopMetadata.limits.maxIterations,
318
- repairCycles: 0,
319
- maxRepairCycles: options.maxRepairCycles,
320
+ noProgressStreak: 0,
321
+ maxNoProgress: loopMetadata.limits.maxNoProgress,
320
322
  maxMinutes: loopMetadata.limits.maxMinutes ?? options.maxMinutes,
321
323
  startedAt: new Date().toISOString(),
322
324
  updatedAt: new Date().toISOString(),
@@ -348,6 +350,7 @@ if (options.action === "repair") {
348
350
  ) {
349
351
  current.active = true;
350
352
  current.state = "continue";
353
+ current.noProgressStreak = 0;
351
354
  current.startedAt = new Date().toISOString();
352
355
  delete current.stopReason;
353
356
  }
@@ -364,20 +367,32 @@ if (options.action === "set-state") {
364
367
  if (options.state === "advance") {
365
368
  current.phaseIndex = phases.length === 1 ? 0 : (Number(current.phaseIndex ?? 0) + 1) % phases.length;
366
369
  current.phaseIteration = 0;
367
- current.repairCycles = Number(current.repairCycles ?? 0);
368
370
  current.state = "advance";
369
371
  current.active = true;
370
372
  } else {
371
373
  current.state = options.state;
372
374
  current.active = !terminalStates.has(options.state);
373
375
  }
376
+ if (options.progress) {
377
+ current.noProgressStreak = options.progress === "yes" ? 0 : Number(current.noProgressStreak ?? 0) + 1;
378
+ }
379
+ const maxNoProgress = Number(current.maxNoProgress ?? 0);
380
+ if (
381
+ maxNoProgress >= 1
382
+ && Number(current.noProgressStreak ?? 0) >= maxNoProgress
383
+ && (current.state === "continue" || current.state === "advance")
384
+ ) {
385
+ current.state = "blocked";
386
+ current.active = false;
387
+ current.stopReason = "no-progress limit exhausted";
388
+ }
374
389
  current.phases = phases;
375
390
  current.phaseName = currentPhase(current);
376
391
  current.schemaVersion = 1;
377
392
  current.updatedAt = new Date().toISOString();
378
393
  writeJson(statePath, current);
379
394
  reconcileContext(root, current);
380
- console.log(`Oris chat loop state: ${current.state}`);
395
+ console.log(`Oris chat loop state: ${current.state}${current.stopReason ? ` (${current.stopReason})` : ""}`);
381
396
  process.exit(0);
382
397
  }
383
398
 
@@ -401,6 +416,9 @@ console.log(JSON.stringify({
401
416
  taskPath: current.taskPath,
402
417
  iteration: Number(current.iteration ?? 0),
403
418
  maxIterations: Number(current.maxIterations ?? 0),
419
+ noProgressStreak: Number(current.noProgressStreak ?? 0),
420
+ maxNoProgress: Number(current.maxNoProgress ?? 0),
421
+ roles: Array.isArray(current.roles) ? current.roles : ["executor", "verifier"],
404
422
  improveMode: current.improveMode ?? "propose",
405
423
  models: current.models ?? {},
406
424
  conversationBound: Boolean(current.conversationId),
@@ -73,6 +73,7 @@ kind: oris-loop
73
73
  name: ${DEMO_SLUG}
74
74
  approved: true
75
75
  phases: [work]
76
+ roles: [executor, verifier, debriefer]
76
77
  limits:
77
78
  maxIterations: 3
78
79
  maxNoProgress: 2
@@ -97,6 +98,7 @@ Stop: complete at 3; blocked if the file is missing or not a number.
97
98
  This loop exists to LEARN the loop system safely:
98
99
  - it only touches files inside its own folder;
99
100
  - it runs 3 short passes, then completes;
101
+ - roles are executor + verifier (the default) plus the debriefer;
100
102
  - improve.mode is \`auto\`, so you can watch the debriefer tune \`prompts/\` between passes.
101
103
 
102
104
  Try: edit \`prompts/executor.md\` and see the next pass obey your change.
@@ -104,13 +106,14 @@ Try: edit \`prompts/executor.md\` and see the next pass obey your change.
104
106
 
105
107
  fs.writeFileSync(path.join(loopDir, "prompts", "executor.md"), `# Executor prompt — demo loop (edit me and watch the next pass change)
106
108
 
109
+ ORIS LOOP ACTIVE — approved demo pass.
107
110
  You are the executor of the Oris demo loop.
108
111
 
109
112
  THIS PASS: iteration {{iteration}}.
110
113
  DO: read ${workspaceRelative}/counter.txt, add 1, write the new value back.
111
114
  ONLY touch files under ${workspaceRelative}.
112
115
 
113
- RETURN compact JSON only:
116
+ RETURN compact JSON only (\`\`\`json fenced):
114
117
  { "role": "executor", "state": "claimed|blocked", "summary": "", "changedFiles": [], "claim": "counter is now N" }
115
118
  `, "utf8");
116
119
 
@@ -120,22 +123,10 @@ You are the independent verifier of the Oris demo loop.
120
123
  NEVER trust the executor claim without reading the file yourself.
121
124
 
122
125
  VERIFY: ${workspaceRelative}/counter.txt contains exactly the claimed number.
126
+ goalMet = true ONLY when the verified counter equals 3 (the loop goal).
123
127
 
124
- RETURN compact JSON only:
125
- { "role": "verifier", "state": "pass|fail|blocked", "summary": "", "evidence": "file content: N" }
126
- `, "utf8");
127
-
128
- fs.writeFileSync(path.join(loopDir, "prompts", "doctor.md"), `# Doctor prompt — demo loop
129
-
130
- You are the doctor of the Oris demo loop.
131
-
132
- DECIDE from the verifier evidence:
133
- - counter < 3 and verified → continue
134
- - counter = 3 and verified → complete
135
- - verification failed or file broken → stop
136
-
137
- RETURN compact JSON only:
138
- { "role": "doctor", "decision": "continue|complete|stop", "diagnosis": "" }
128
+ RETURN compact JSON only (\`\`\`json fenced):
129
+ { "role": "verifier", "state": "pass|fail|blocked|inconclusive", "goalMet": false, "summary": "", "evidence": "file content: N" }
139
130
  `, "utf8");
140
131
 
141
132
  fs.writeFileSync(path.join(loopDir, "prompts", "debriefer.md"), `# Debriefer prompt — demo loop (improve mode: auto)
@@ -157,7 +148,7 @@ Current phase: work
157
148
  Current phase index: 0
158
149
  Current iteration: 0
159
150
  Phase iteration: 0
160
- Repair cycles: 0
151
+ No-progress streak: 0
161
152
  Updated: ${new Date().toISOString()}
162
153
 
163
154
  ## Objective
@@ -172,9 +163,9 @@ Increment the counter from its current value.
172
163
  console.log(`Demo loop created: ${docs.loopRelative}`);
173
164
  console.log("");
174
165
  console.log("Learn it in 3 steps:");
175
- console.log(` 1. Preview a pass (nothing runs): oris-skills loop dry-run --loop ${DEMO_SLUG}`);
176
- console.log(` 2. Run it from your agent chat: oris-skills loop chat --action start --loop ${DEMO_SLUG}`);
166
+ console.log(` 1. Preview a pass (nothing runs): npx oris-skills loop dry-run --loop ${DEMO_SLUG}`);
167
+ console.log(` 2. Run it from your agent chat: npx oris-skills loop chat --action start --loop ${DEMO_SLUG}`);
177
168
  console.log(" then end the turn — the stop hook schedules pass 1 automatically.");
178
169
  console.log(` 3. Watch and tune: edit ${docs.promptsRelative}/executor.md between passes.`);
179
170
  console.log("");
180
- console.log(`Stop anytime: oris-skills loop chat --action stop`);
171
+ console.log(`Stop anytime: npx oris-skills loop chat --action stop`);
@@ -7,8 +7,7 @@ const phasePattern = /^[a-z][a-z0-9-]*$/;
7
7
 
8
8
  export const INHERIT_MODEL = "inherit";
9
9
  export const ROLE_NAMES = ["executor", "verifier", "doctor", "debriefer"];
10
- export const REQUIRED_PROMPT_FILES = ["executor.md", "verifier.md", "doctor.md"];
11
- export const OPTIONAL_PROMPT_FILES = ["orchestrator.md", "debriefer.md"];
10
+ export const DEFAULT_ROLES = ["executor", "verifier"];
12
11
  export const IMPROVE_MODES = new Set(["propose", "auto"]);
13
12
 
14
13
  export function parseFrontMatter(text, filePath = "loop.md") {
@@ -97,6 +96,19 @@ export function validateLoopMetadata(data, filePath = "loop.md") {
97
96
  if (!Array.isArray(data.phases) || data.phases.length < 1 || data.phases.some((phase) => typeof phase !== "string" || !phasePattern.test(phase))) {
98
97
  errors.push("phases must contain valid phase slugs");
99
98
  }
99
+ if (data.roles !== undefined) {
100
+ if (!Array.isArray(data.roles) || data.roles.some((role) => !ROLE_NAMES.includes(role))) {
101
+ errors.push(`roles must be an array of ${ROLE_NAMES.join("|")}`);
102
+ } else {
103
+ if (new Set(data.roles).size !== data.roles.length) errors.push("roles must not repeat");
104
+ for (const required of DEFAULT_ROLES) {
105
+ if (!data.roles.includes(required)) errors.push(`roles must include ${required}`);
106
+ }
107
+ }
108
+ }
109
+ if (data.improve?.mode === "auto" && !activeRoles(data).includes("debriefer")) {
110
+ errors.push("improve.mode auto requires the debriefer role (roles: [executor, verifier, debriefer])");
111
+ }
100
112
  if (!data.limits || !Number.isInteger(data.limits.maxIterations) || data.limits.maxIterations < 1 || data.limits.maxIterations > 500) {
101
113
  errors.push("limits.maxIterations must be 1..500");
102
114
  }
@@ -162,13 +174,18 @@ export function improveMode(data) {
162
174
  return data?.improve?.mode === "auto" ? "auto" : "propose";
163
175
  }
164
176
 
165
- /** Check the per-loop prompts/ directory. Returns { promptsDir, missing }. */
166
- export function assessPromptFiles(loopDir) {
177
+ /** Roles a loop runs each pass; executor + verifier unless loop.md opts more in. */
178
+ export function activeRoles(data) {
179
+ return Array.isArray(data?.roles) && data.roles.length > 0 ? [...data.roles] : [...DEFAULT_ROLES];
180
+ }
181
+
182
+ /** Check the per-loop prompts/ directory against the loop's active roles. */
183
+ export function assessPromptFiles(loopDir, roles = DEFAULT_ROLES) {
167
184
  const promptsDir = path.join(loopDir, "prompts");
168
185
  const missing = [];
169
- for (const file of REQUIRED_PROMPT_FILES) {
170
- const full = path.join(promptsDir, file);
171
- if (!fs.existsSync(full) || !fs.readFileSync(full, "utf8").trim()) missing.push(`prompts/${file}`);
186
+ for (const role of roles) {
187
+ const full = path.join(promptsDir, `${role}.md`);
188
+ if (!fs.existsSync(full) || !fs.readFileSync(full, "utf8").trim()) missing.push(`prompts/${role}.md`);
172
189
  }
173
190
  return { promptsDir, missing };
174
191
  }
@@ -11,6 +11,7 @@ import path from "node:path";
11
11
  import process from "node:process";
12
12
  import { readAdapter, resolveLoopDocumentPathsBySlug } from "./oris-loop-paths.mjs";
13
13
  import {
14
+ activeRoles,
14
15
  assessPromptFiles,
15
16
  improveMode,
16
17
  normalizeModels,
@@ -52,7 +53,7 @@ function getRoot(options) {
52
53
  }
53
54
 
54
55
  const options = parseArgs(process.argv.slice(2));
55
- if (!options.loopSlug) fail("--loop <slug> is required. List loops with: oris-skills loop list");
56
+ if (!options.loopSlug) fail("--loop <slug> is required. List loops with: npx oris-skills loop list");
56
57
  const root = getRoot(options);
57
58
  const adapter = readAdapter(root);
58
59
  const docs = resolveLoopDocumentPathsBySlug(root, options.loopSlug, adapter);
@@ -65,7 +66,8 @@ try {
65
66
  fail(error.message);
66
67
  }
67
68
  const metadata = loopDocument.data;
68
- const prompts = assessPromptFiles(path.dirname(docs.loopFull));
69
+ const roles = activeRoles(metadata);
70
+ const prompts = assessPromptFiles(path.dirname(docs.loopFull), roles);
69
71
  const models = normalizeModels(metadata.models, adapter.models);
70
72
 
71
73
  const perLoopOrchestrator = path.join(docs.promptsFull, "orchestrator.md");
@@ -84,6 +86,7 @@ const followup = substitutePlaceholders(template, {
84
86
  loopPath: docs.loopRelative,
85
87
  contextPath: docs.contextRelative,
86
88
  promptsDir: docs.promptsRelative,
89
+ roles: roles.join(", "),
87
90
  executorModel: models.executor,
88
91
  verifierModel: models.verifier,
89
92
  doctorModel: models.doctor,
@@ -95,6 +98,7 @@ const followup = substitutePlaceholders(template, {
95
98
  console.log(`Oris loop dry run: ${metadata.name}`);
96
99
  console.log(`Definition: ${docs.loopRelative} (schemaVersion ${metadata.schemaVersion}, approved: ${metadata.approved})`);
97
100
  console.log(`Prompts: ${docs.promptsRelative} ${prompts.missing.length === 0 ? "(complete)" : `(MISSING: ${prompts.missing.join(", ")})`}`);
101
+ console.log(`Roles: ${roles.join(", ")}`);
98
102
  console.log(`Models: executor=${models.executor} verifier=${models.verifier} doctor=${models.doctor} debriefer=${models.debriefer}`);
99
103
  console.log(`Improve mode: ${improveMode(metadata)}`);
100
104
  console.log(`Limits: ${metadata.limits.maxIterations} iterations, ${metadata.limits.maxNoProgress} no-progress, ${metadata.limits.maxMinutes ?? 240} minutes`);
@@ -104,7 +108,7 @@ console.log(followup);
104
108
  console.log("=== End of injected message ===");
105
109
  console.log("");
106
110
  if (prompts.missing.length > 0) {
107
- console.log("NOT runnable yet — create the missing prompt files first (templates: skills/oris-loop/templates/).");
111
+ console.log("NOT runnable yet — create the missing prompt files first (templates: skills/oris-flow/templates/).");
108
112
  process.exit(1);
109
113
  }
110
- console.log(`Runnable. Start it with: oris-skills loop chat --action start --loop ${metadata.name}`);
114
+ console.log(`Runnable. Start it with: npx oris-skills loop chat --action start --loop ${metadata.name}`);
@@ -8,7 +8,7 @@ import {
8
8
  writeStopHookWrapper,
9
9
  } from "./oris-loop-bootstrap.mjs";
10
10
  import { defaultLoopDocumentPaths, readAdapter } from "./oris-loop-paths.mjs";
11
- import { ORIS_FLOW_ADAPTER } from "../flow/oris-flow-layout.mjs";
11
+ import { ORIS_FLOW_ADAPTER, ORIS_FLOW_TASKS_ROOT } from "../flow/oris-flow-layout.mjs";
12
12
 
13
13
  export function writeMinimalAdapter(root) {
14
14
  const adapterPath = path.join(root, ORIS_FLOW_ADAPTER);
@@ -22,6 +22,7 @@ export function writeMinimalAdapter(root) {
22
22
  export function loopDocumentSource({
23
23
  program = "oris-loop-smoke",
24
24
  phases = ["work"],
25
+ roles = ["executor", "verifier"],
25
26
  improve = "propose",
26
27
  models = {},
27
28
  goal = "Run one deterministic smoke action, verify it, and record evidence.",
@@ -37,8 +38,9 @@ kind: oris-loop
37
38
  name: ${program}
38
39
  approved: true
39
40
  phases: [${phases.join(", ")}]
41
+ roles: [${roles.join(", ")}]
40
42
  limits:
41
- maxIterations: 25
43
+ maxIterations: 10
42
44
  maxNoProgress: 2
43
45
  maxMinutes: 240
44
46
  ${modelLines.join("\n")}
@@ -65,20 +67,17 @@ export function rolePromptFixtures() {
65
67
  return {
66
68
  "executor.md": `# Executor prompt
67
69
 
70
+ ORIS LOOP ACTIVE — approved smoke pass.
68
71
  Perform exactly one bounded smoke action inside allowed paths.
69
72
  Runtime: iteration {{iteration}}, phase {{phase}}, target {{currentTarget}}.
70
- RETURN compact JSON: { "role": "executor", "state": "claimed|blocked", "summary": "", "changedFiles": [], "claim": "" }
73
+ RETURN compact JSON (\`\`\`json fenced): { "role": "executor", "state": "claimed|blocked", "summary": "", "changedFiles": [], "claim": "" }
71
74
  `,
72
75
  "verifier.md": `# Verifier prompt
73
76
 
74
77
  Verify independently: \`node --version\` exits 0. NEVER trust the executor without running the check.
75
78
  Runtime: iteration {{iteration}}; executor claim supplied by orchestrator.
76
- RETURN compact JSON: { "role": "verifier", "state": "pass|fail|blocked", "summary": "", "evidence": "" }
77
- `,
78
- "doctor.md": `# Doctor prompt
79
-
80
- Diagnose from evidence. Stop rules: success when the smoke check passes; blocked on missing tools.
81
- RETURN compact JSON: { "role": "doctor", "decision": "continue|complete|stop", "diagnosis": "" }
79
+ goalMet = true when the smoke check passed (the loop goal is one verified pass).
80
+ RETURN compact JSON (\`\`\`json fenced): { "role": "verifier", "state": "pass|fail|blocked|inconclusive", "goalMet": false, "summary": "", "evidence": "" }
82
81
  `,
83
82
  };
84
83
  }
@@ -88,16 +87,18 @@ export function writeLoopTask(root, {
88
87
  program = "oris-loop-smoke",
89
88
  phases = ["work"],
90
89
  phase = phases[0],
90
+ roles = ["executor", "verifier"],
91
91
  improve = "propose",
92
92
  models = {},
93
93
  } = {}) {
94
- const taskPath = taskSlug;
94
+ const taskPath = `${ORIS_FLOW_TASKS_ROOT}/${taskSlug}`;
95
+ fs.mkdirSync(path.join(root, ...taskPath.split("/")), { recursive: true });
95
96
  const adapter = readAdapter(root);
96
97
  const loopDocs = defaultLoopDocumentPaths(root, taskPath, adapter);
97
98
  const loopPath = loopDocs.loopFull;
98
99
  const contextPath = loopDocs.contextFull;
99
100
  fs.mkdirSync(path.dirname(loopPath), { recursive: true });
100
- fs.writeFileSync(loopPath, loopDocumentSource({ program, phases, improve, models }), "utf8");
101
+ fs.writeFileSync(loopPath, loopDocumentSource({ program, phases, roles, improve, models }), "utf8");
101
102
  fs.mkdirSync(loopDocs.promptsFull, { recursive: true });
102
103
  for (const [name, content] of Object.entries(rolePromptFixtures())) {
103
104
  fs.writeFileSync(path.join(loopDocs.promptsFull, name), content, "utf8");
@@ -111,7 +112,7 @@ Current phase: ${phase}
111
112
  Current phase index: 0
112
113
  Current iteration: 0
113
114
  Phase iteration: 0
114
- Repair cycles: 0
115
+ No-progress streak: 0
115
116
  Updated: ${new Date().toISOString()}
116
117
 
117
118
  ## Objective