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.
- package/.cursor-plugin/plugin.json +2 -11
- package/CHANGELOG.md +48 -0
- package/README.md +79 -31
- package/agents/oris-loop-debriefer.md +1 -0
- package/agents/oris-loop-doctor.md +1 -1
- package/agents/oris-loop-executor.md +5 -2
- package/agents/oris-loop-verifier.md +6 -2
- package/docs/architecture.md +9 -7
- package/docs/distribution.md +1 -1
- package/docs/maintainer-guide.md +5 -5
- package/docs/user-guide.md +12 -9
- package/package.json +2 -3
- package/references/clean-code-checklist.md +20 -107
- package/references/conventions.md +43 -3
- package/references/doc-policy.md +19 -3
- package/references/loop-contract.md +24 -17
- package/references/loop.schema.json +13 -0
- package/references/repo-map.md +3 -3
- package/references/repo-map.schema.json +41 -0
- package/references/settings.md +2 -1
- package/references/settings.schema.json +5 -0
- package/scripts/flow/oris-flow-layout.mjs +1 -0
- package/scripts/flow/oris-flow-scan.mjs +265 -27
- package/scripts/install/generate-agent-adapters.mjs +19 -2
- package/scripts/install/install-user-skills.mjs +61 -3
- package/scripts/install/uninstall-user-skills.mjs +17 -1
- package/scripts/loop/oris-loop-bootstrap.mjs +7 -2
- package/scripts/loop/oris-loop-chat.mjs +34 -16
- package/scripts/loop/oris-loop-demo.mjs +11 -20
- package/scripts/loop/oris-loop-document.mjs +24 -7
- package/scripts/loop/oris-loop-dry-run.mjs +8 -4
- package/scripts/loop/oris-loop-fixtures.mjs +13 -12
- package/scripts/loop/oris-loop-run.mjs +80 -20
- package/scripts/loop/oris-loop-stop.mjs +48 -9
- package/scripts/loop/oris-loop-templates.mjs +13 -10
- package/scripts/loop/oris-loop-verify.mjs +1 -2
- package/scripts/oris-skills.mjs +3 -2
- package/scripts/tests/run-all-tests.mjs +1 -2
- package/scripts/tests/test-oris-flow-scan.mjs +109 -1
- package/scripts/tests/test-oris-loop-document.mjs +39 -3
- package/scripts/tests/test-oris-loop-run.mjs +28 -2
- package/scripts/tests/test-oris-loop-smoke.mjs +36 -3
- package/scripts/tests/test-oris-loop-stop.mjs +65 -4
- package/scripts/tests/test-routing-lifecycle.mjs +38 -23
- package/scripts/tests/test-skill-style.mjs +64 -0
- package/skills/oris-flow/SKILL.md +53 -25
- package/skills/oris-flow/references/architecture.md +77 -0
- package/skills/oris-flow/references/change.md +60 -0
- package/skills/oris-flow/references/criteria.md +60 -0
- package/skills/oris-flow/references/discover.md +66 -0
- package/skills/oris-flow/references/docs.md +33 -0
- package/skills/oris-flow/references/fix.md +49 -0
- package/skills/oris-flow/references/help.md +34 -0
- package/skills/oris-flow/references/implement.md +48 -0
- package/skills/oris-flow/references/loop-craft.md +59 -0
- package/skills/oris-flow/references/loop-improve.md +32 -0
- package/skills/oris-flow/references/loop-run.md +47 -0
- package/skills/oris-flow/references/loop.md +56 -0
- package/skills/oris-flow/references/new.md +64 -0
- package/skills/oris-flow/references/plan.md +58 -0
- package/skills/oris-flow/references/setup.md +100 -0
- package/skills/oris-flow/references/verify.md +57 -0
- package/skills/{oris-loop → oris-flow}/templates/debriefer.md +2 -1
- package/skills/oris-flow/templates/doctor.md +22 -0
- package/skills/{oris-loop → oris-flow}/templates/executor.md +8 -2
- package/skills/{oris-loop → oris-flow}/templates/orchestrator.md +10 -7
- package/skills/{oris-loop → oris-flow}/templates/verifier.md +6 -3
- package/references/questions.md +0 -38
- package/skills/oris-flow-criteria/SKILL.md +0 -49
- package/skills/oris-flow-discover/SKILL.md +0 -58
- package/skills/oris-flow-docs/SKILL.md +0 -31
- package/skills/oris-flow-fix/SKILL.md +0 -42
- package/skills/oris-flow-implement/SKILL.md +0 -43
- package/skills/oris-flow-plan/SKILL.md +0 -51
- package/skills/oris-flow-setup/SKILL.md +0 -49
- package/skills/oris-help/SKILL.md +0 -30
- package/skills/oris-help/agents/openai.yaml +0 -4
- package/skills/oris-loop/SKILL.md +0 -66
- package/skills/oris-loop/agents/openai.yaml +0 -4
- package/skills/oris-loop/references/craft.md +0 -48
- package/skills/oris-loop/references/improve.md +0 -23
- package/skills/oris-loop/references/run.md +0 -30
- 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:
|
|
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:
|
|
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
|
-
"
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
166
|
-
export function
|
|
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
|
|
170
|
-
const full = path.join(promptsDir,
|
|
171
|
-
if (!fs.existsSync(full) || !fs.readFileSync(full, "utf8").trim()) missing.push(`prompts/${
|
|
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
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
No-progress streak: 0
|
|
115
116
|
Updated: ${new Date().toISOString()}
|
|
116
117
|
|
|
117
118
|
## Objective
|