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
|
@@ -16,6 +16,7 @@ import process from "node:process";
|
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { readAdapter, resolveLoopDocumentPathsBySlug } from "./oris-loop-paths.mjs";
|
|
18
18
|
import {
|
|
19
|
+
activeRoles,
|
|
19
20
|
assessPromptFiles,
|
|
20
21
|
improveMode,
|
|
21
22
|
normalizeModels,
|
|
@@ -64,21 +65,24 @@ function getRoot(options) {
|
|
|
64
65
|
return path.resolve(result.stdout.trim());
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/** Roles flagged readonly must not get write access at the harness level. */
|
|
69
|
+
const READONLY_ROLES = new Set(["verifier", "doctor"]);
|
|
70
|
+
|
|
67
71
|
/** Build the CLI invocation for one role. Exported for tests. */
|
|
68
|
-
export function buildRoleInvocation(agent, { prompt, model, root }) {
|
|
72
|
+
export function buildRoleInvocation(agent, { prompt, model, root, readonly = false }) {
|
|
69
73
|
if (agent === "claude") {
|
|
70
|
-
const args = ["-p", prompt, "--permission-mode", "acceptEdits"];
|
|
74
|
+
const args = ["-p", prompt, "--permission-mode", readonly ? "plan" : "acceptEdits"];
|
|
71
75
|
if (model && model !== "inherit") args.push("--model", model);
|
|
72
76
|
return { command: "claude", args, cwd: root };
|
|
73
77
|
}
|
|
74
|
-
const args = ["exec", "--sandbox", "workspace-write", "--cd", root];
|
|
78
|
+
const args = ["exec", "--sandbox", readonly ? "read-only" : "workspace-write", "--cd", root];
|
|
75
79
|
if (model && model !== "inherit") args.push("-m", model);
|
|
76
80
|
args.push(prompt);
|
|
77
81
|
return { command: "codex", args, cwd: root };
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function runRole(agent, role, prompt, model, root, evidenceDir, pass) {
|
|
81
|
-
const invocation = buildRoleInvocation(agent, { prompt, model, root });
|
|
85
|
+
const invocation = buildRoleInvocation(agent, { prompt, model, root, readonly: READONLY_ROLES.has(role) });
|
|
82
86
|
process.stdout.write(` ${role} (${agent}${model && model !== "inherit" ? `, ${model}` : ""})... `);
|
|
83
87
|
const result = childProcess.spawnSync(invocation.command, invocation.args, {
|
|
84
88
|
cwd: invocation.cwd,
|
|
@@ -101,6 +105,15 @@ function runRole(agent, role, prompt, model, root, evidenceDir, pass) {
|
|
|
101
105
|
/** Pull the last JSON object out of an agent's free-form output. */
|
|
102
106
|
export function extractJson(output) {
|
|
103
107
|
const text = String(output ?? "");
|
|
108
|
+
// Prefer the last ```json fenced block — the format the role prompts request.
|
|
109
|
+
const fenced = [...text.matchAll(/```json\s*\n?([\s\S]*?)```/gi)];
|
|
110
|
+
for (let i = fenced.length - 1; i >= 0; i -= 1) {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(fenced[i][1]);
|
|
113
|
+
} catch {
|
|
114
|
+
// fall through to the brace scan
|
|
115
|
+
}
|
|
116
|
+
}
|
|
104
117
|
for (let start = text.lastIndexOf("{"); start !== -1; start = start === 0 ? -1 : text.lastIndexOf("{", start - 1)) {
|
|
105
118
|
for (let end = text.indexOf("}", start); end !== -1; end = text.indexOf("}", end + 1)) {
|
|
106
119
|
try {
|
|
@@ -113,14 +126,35 @@ export function extractJson(output) {
|
|
|
113
126
|
return null;
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Canonical doctor decisions (references/loop-contract.md):
|
|
131
|
+
* continue | advance | complete | ask-user | blocked | propose-patch.
|
|
132
|
+
* A proposed patch needs user approval, so it pauses like ask-user.
|
|
133
|
+
* Unknown decisions block — but loudly, never silently.
|
|
134
|
+
*/
|
|
116
135
|
export function doctorDecisionToState(decision) {
|
|
117
136
|
const value = String(decision ?? "").toLowerCase();
|
|
118
137
|
if (value === "complete") return "complete";
|
|
119
138
|
if (value === "advance") return "advance";
|
|
120
139
|
if (value === "continue" || value === "retry") return "continue";
|
|
140
|
+
if (value === "ask-user" || value === "ask_user" || value === "propose-patch") return "ask-user";
|
|
141
|
+
if (value !== "blocked" && value !== "block" && value !== "stop") {
|
|
142
|
+
console.warn(` WARN unrecognized doctor decision "${decision}" — treating as blocked.`);
|
|
143
|
+
}
|
|
121
144
|
return "blocked";
|
|
122
145
|
}
|
|
123
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Without a doctor, the verifier verdict drives the state directly:
|
|
149
|
+
* pass + goalMet → complete; pass|fail → continue; anything else pauses.
|
|
150
|
+
*/
|
|
151
|
+
export function verifierVerdictToState(verifierResult) {
|
|
152
|
+
const state = String(verifierResult?.state ?? "").toLowerCase();
|
|
153
|
+
if (state === "pass") return verifierResult?.goalMet === true ? "complete" : "continue";
|
|
154
|
+
if (state === "fail") return "continue";
|
|
155
|
+
return "ask-user";
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
function chatAction(root, args) {
|
|
125
159
|
const script = path.join(path.dirname(fileURLToPath(import.meta.url)), "oris-loop-chat.mjs");
|
|
126
160
|
const result = childProcess.spawnSync(process.execPath, [script, "--repository-root", root, ...args], {
|
|
@@ -156,7 +190,8 @@ try {
|
|
|
156
190
|
fail(error.message);
|
|
157
191
|
}
|
|
158
192
|
const metadata = loopDocument.data;
|
|
159
|
-
const
|
|
193
|
+
const roles = activeRoles(metadata);
|
|
194
|
+
const prompts = assessPromptFiles(path.dirname(docs.loopFull), roles);
|
|
160
195
|
if (prompts.missing.length > 0) fail(`Loop prompt files are missing: ${prompts.missing.join(", ")}`);
|
|
161
196
|
const models = normalizeModels(metadata.models, adapter.models);
|
|
162
197
|
const mode = improveMode(metadata);
|
|
@@ -166,9 +201,11 @@ const receiptsDir = path.join(path.dirname(docs.loopFull), "receipts");
|
|
|
166
201
|
fs.mkdirSync(evidenceDir, { recursive: true });
|
|
167
202
|
fs.mkdirSync(receiptsDir, { recursive: true });
|
|
168
203
|
|
|
169
|
-
console.log(`Oris loop headless run: ${metadata.name} (agent: ${options.agent}, improve: ${mode})`);
|
|
204
|
+
console.log(`Oris loop headless run: ${metadata.name} (agent: ${options.agent}, roles: ${roles.join("+")}, improve: ${mode})`);
|
|
170
205
|
chatAction(root, ["--action", "start", "--loop", options.loopSlug, "--runner", "headless"]);
|
|
171
206
|
|
|
207
|
+
const maxNoProgress = Number(metadata.limits.maxNoProgress ?? 0);
|
|
208
|
+
let noProgressStreak = 0;
|
|
172
209
|
let state = "continue";
|
|
173
210
|
for (let pass = 1; pass <= maxPasses && (state === "continue" || state === "advance"); pass += 1) {
|
|
174
211
|
console.log(`Pass ${pass}/${maxPasses}:`);
|
|
@@ -191,20 +228,35 @@ for (let pass = 1; pass <= maxPasses && (state === "continue" || state === "adva
|
|
|
191
228
|
models.verifier, root, evidenceDir, pass);
|
|
192
229
|
const verifierResult = extractJson(verifier.output);
|
|
193
230
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
231
|
+
let doctor = null;
|
|
232
|
+
let doctorResult = null;
|
|
233
|
+
if (roles.includes("doctor")) {
|
|
234
|
+
doctor = runRole(options.agent, "doctor",
|
|
235
|
+
rolePrompt(prompts.promptsDir, "doctor.md", runtimeValues,
|
|
236
|
+
`EXECUTOR:\n${JSON.stringify(executorResult, null, 2)}\nVERIFIER:\n${JSON.stringify(verifierResult, null, 2)}`),
|
|
237
|
+
models.doctor, root, evidenceDir, pass);
|
|
238
|
+
doctorResult = extractJson(doctor.output);
|
|
239
|
+
if (doctorResult === null) {
|
|
240
|
+
console.warn(" WARN doctor output was not parseable JSON — pausing for user review.");
|
|
241
|
+
state = "ask-user";
|
|
242
|
+
} else {
|
|
243
|
+
state = doctorDecisionToState(doctorResult.decision);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
state = verifierVerdictToState(verifierResult);
|
|
247
|
+
}
|
|
200
248
|
|
|
201
|
-
if (mode === "auto" && fs.existsSync(path.join(prompts.promptsDir, "debriefer.md"))) {
|
|
249
|
+
if (roles.includes("debriefer") && mode === "auto" && fs.existsSync(path.join(prompts.promptsDir, "debriefer.md"))) {
|
|
202
250
|
runRole(options.agent, "debriefer",
|
|
203
251
|
rolePrompt(prompts.promptsDir, "debriefer.md", { ...runtimeValues, IMPROVE_MODE: mode },
|
|
204
|
-
`
|
|
252
|
+
`VERDICT:\n${JSON.stringify(doctorResult ?? verifierResult, null, 2)}\nPROMPTS DIR: ${docs.promptsRelative}`),
|
|
205
253
|
models.debriefer, root, evidenceDir, pass);
|
|
206
254
|
}
|
|
207
255
|
|
|
256
|
+
const progressed = verifierResult?.state === "pass"
|
|
257
|
+
|| (Array.isArray(executorResult?.changedFiles) && executorResult.changedFiles.length > 0);
|
|
258
|
+
noProgressStreak = progressed ? 0 : noProgressStreak + 1;
|
|
259
|
+
|
|
208
260
|
const receiptPath = path.join(receiptsDir, `pass-${String(pass).padStart(3, "0")}.md`);
|
|
209
261
|
fs.writeFileSync(receiptPath, `# Oris Loop Receipt
|
|
210
262
|
|
|
@@ -215,16 +267,20 @@ Result: ${state}
|
|
|
215
267
|
|
|
216
268
|
Executor: ${executorResult?.summary ?? executorResult?.claim ?? "see evidence"}
|
|
217
269
|
Verifier: ${verifierResult?.state ?? "unknown"} — ${verifierResult?.summary ?? "see evidence"}
|
|
218
|
-
Doctor: ${doctorResult?.decision ?? "unknown"} — ${doctorResult?.diagnosis ?? "see evidence"}
|
|
219
|
-
|
|
270
|
+
${doctor ? `Doctor: ${doctorResult?.decision ?? "unknown"} — ${doctorResult?.diagnosis ?? "see evidence"}\n` : ""}
|
|
220
271
|
Evidence:
|
|
221
272
|
- ${path.relative(root, executor.evidencePath).replace(/\\/g, "/")}
|
|
222
273
|
- ${path.relative(root, verifier.evidencePath).replace(/\\/g, "/")}
|
|
223
|
-
|
|
224
|
-
`, "utf8");
|
|
274
|
+
${doctor ? `- ${path.relative(root, doctor.evidencePath).replace(/\\/g, "/")}\n` : ""}`, "utf8");
|
|
225
275
|
|
|
226
|
-
chatAction(root, ["--action", "set-state", "--state", state === "advance" ? "advance" : state
|
|
227
|
-
|
|
276
|
+
chatAction(root, ["--action", "set-state", "--state", state === "advance" ? "advance" : state,
|
|
277
|
+
"--progress", progressed ? "yes" : "no"]);
|
|
278
|
+
console.log(` state: ${state} | progress: ${progressed ? "yes" : "no"} | receipt: ${path.relative(root, receiptPath).replace(/\\/g, "/")}`);
|
|
279
|
+
|
|
280
|
+
if (maxNoProgress >= 1 && noProgressStreak >= maxNoProgress && (state === "continue" || state === "advance")) {
|
|
281
|
+
state = "blocked";
|
|
282
|
+
console.log(`No-progress limit exhausted (${maxNoProgress} passes without progress) — state set to blocked.`);
|
|
283
|
+
}
|
|
228
284
|
}
|
|
229
285
|
|
|
230
286
|
if (state === "continue" || state === "advance") {
|
|
@@ -232,6 +288,10 @@ if (state === "continue" || state === "advance") {
|
|
|
232
288
|
console.log("Pass budget exhausted before completion — state set to blocked.");
|
|
233
289
|
process.exit(1);
|
|
234
290
|
}
|
|
291
|
+
if (state === "ask-user") {
|
|
292
|
+
console.log("Headless run paused: the loop needs user input or approval. Review the last receipt, then resume with: npx oris-skills loop run --loop " + metadata.name);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
235
295
|
console.log(`Headless run finished: ${state}`);
|
|
236
296
|
process.exit(state === "complete" ? 0 : 1);
|
|
237
297
|
}
|
|
@@ -85,6 +85,8 @@ function detectPlatform(argv, raw) {
|
|
|
85
85
|
const explicit = argv.find((arg) => arg.startsWith("--platform="))?.slice("--platform=".length)
|
|
86
86
|
?? (argv.includes("--platform") ? argv[argv.indexOf("--platform") + 1] : "");
|
|
87
87
|
if (explicit === "cursor" || explicit === "claude") return explicit;
|
|
88
|
+
// Cursor payloads carry conversation_id (and may ALSO carry session_id) — check it first.
|
|
89
|
+
if (raw.conversation_id !== undefined || raw.conversationId !== undefined) return "cursor";
|
|
88
90
|
if (String(raw.hook_event_name ?? "") === "Stop" || raw.session_id !== undefined || raw.stop_hook_active !== undefined) {
|
|
89
91
|
return "claude";
|
|
90
92
|
}
|
|
@@ -146,10 +148,15 @@ function appendDebug(debugPath, entry) {
|
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
function packageOutput(platform, followup) {
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
function packageOutput(platform, followup, notice) {
|
|
152
|
+
if (followup) {
|
|
153
|
+
if (platform === "claude") return { decision: "block", reason: followup };
|
|
154
|
+
return { followup_message: followup };
|
|
155
|
+
}
|
|
156
|
+
// A stopped/broken loop must never wedge the chat, but the user should hear
|
|
157
|
+
// about it. Claude Code shows systemMessage; Cursor has no non-resuming channel.
|
|
158
|
+
if (notice && platform === "claude") return { systemMessage: notice };
|
|
159
|
+
return {};
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
function finish(debugPath, baseEntry, result) {
|
|
@@ -160,7 +167,7 @@ function finish(debugPath, baseEntry, result) {
|
|
|
160
167
|
reason: result.reason,
|
|
161
168
|
emittedFollowup: Boolean(result.followup),
|
|
162
169
|
});
|
|
163
|
-
emit(packageOutput(baseEntry.platform, result.followup));
|
|
170
|
+
emit(packageOutput(baseEntry.platform, result.followup, result.notice));
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
/** Resolve the orchestrator prompt: per-loop file > packaged template > embedded. */
|
|
@@ -213,7 +220,11 @@ try {
|
|
|
213
220
|
try {
|
|
214
221
|
state = normalizeState(parseJsonText(fs.readFileSync(statePath, "utf8")));
|
|
215
222
|
} catch {
|
|
216
|
-
finish(debugPath, baseEntry, {
|
|
223
|
+
finish(debugPath, baseEntry, {
|
|
224
|
+
decision: "noop",
|
|
225
|
+
reason: "chat-active-invalid-json",
|
|
226
|
+
notice: `Oris loop halted: ${statePath} is not valid JSON. Repair it with: npx oris-skills loop chat --action repair`,
|
|
227
|
+
});
|
|
217
228
|
process.exit(0);
|
|
218
229
|
}
|
|
219
230
|
|
|
@@ -222,7 +233,11 @@ try {
|
|
|
222
233
|
baseEntry.runtimeIteration = state?.iteration ?? null;
|
|
223
234
|
|
|
224
235
|
if (!state) {
|
|
225
|
-
finish(debugPath, baseEntry, {
|
|
236
|
+
finish(debugPath, baseEntry, {
|
|
237
|
+
decision: "noop",
|
|
238
|
+
reason: "chat-active-unsupported-schema",
|
|
239
|
+
notice: `Oris loop halted: ${statePath} uses an unsupported schema. Re-arm it with: npx oris-skills loop chat --action repair`,
|
|
240
|
+
});
|
|
226
241
|
process.exit(0);
|
|
227
242
|
}
|
|
228
243
|
|
|
@@ -260,13 +275,32 @@ try {
|
|
|
260
275
|
|
|
261
276
|
if (iteration >= maxIterations || input.loopCount >= maxIterations) {
|
|
262
277
|
stopState(statePath, state, "iteration limit exhausted");
|
|
263
|
-
finish(debugPath, baseEntry, {
|
|
278
|
+
finish(debugPath, baseEntry, {
|
|
279
|
+
decision: "stop",
|
|
280
|
+
reason: "iteration-limit-exhausted",
|
|
281
|
+
notice: `Oris loop "${state.program ?? ""}" stopped: iteration limit exhausted (${maxIterations}). Goal NOT confirmed met. Resume with: npx oris-skills loop chat --action repair`,
|
|
282
|
+
});
|
|
264
283
|
process.exit(0);
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
if (elapsedMinutes >= maxMinutes) {
|
|
268
287
|
stopState(statePath, state, "wall-clock limit exhausted");
|
|
269
|
-
finish(debugPath, baseEntry, {
|
|
288
|
+
finish(debugPath, baseEntry, {
|
|
289
|
+
decision: "stop",
|
|
290
|
+
reason: "wall-clock-limit-exhausted",
|
|
291
|
+
notice: `Oris loop "${state.program ?? ""}" stopped: wall-clock limit exhausted (${maxMinutes} min). Goal NOT confirmed met. Resume with: npx oris-skills loop chat --action repair`,
|
|
292
|
+
});
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const maxNoProgress = Number(state.maxNoProgress ?? 0);
|
|
297
|
+
if (maxNoProgress >= 1 && Number(state.noProgressStreak ?? 0) >= maxNoProgress) {
|
|
298
|
+
stopState(statePath, state, "no-progress limit exhausted");
|
|
299
|
+
finish(debugPath, baseEntry, {
|
|
300
|
+
decision: "stop",
|
|
301
|
+
reason: "no-progress-limit-exhausted",
|
|
302
|
+
notice: `Oris loop "${state.program ?? ""}" stopped: ${maxNoProgress} passes in a row without progress. Goal NOT confirmed met.`,
|
|
303
|
+
});
|
|
270
304
|
process.exit(0);
|
|
271
305
|
}
|
|
272
306
|
|
|
@@ -280,6 +314,10 @@ try {
|
|
|
280
314
|
? state.repositoryRoot
|
|
281
315
|
: input.workspaceRoots.find((root) => typeof root === "string" && fs.existsSync(root)) ?? process.cwd();
|
|
282
316
|
|
|
317
|
+
const roles = Array.isArray(state.roles) && state.roles.length > 0
|
|
318
|
+
? state.roles
|
|
319
|
+
: ["executor", "verifier"];
|
|
320
|
+
|
|
283
321
|
const followup = buildFollowup(state, repositoryRoot, {
|
|
284
322
|
iteration: nextIteration,
|
|
285
323
|
maxIterations,
|
|
@@ -291,6 +329,7 @@ try {
|
|
|
291
329
|
loopPath: state.loopPath ?? "",
|
|
292
330
|
contextPath: state.contextPath ?? "",
|
|
293
331
|
promptsDir: state.promptsPath ?? "",
|
|
332
|
+
roles: roles.join(", "),
|
|
294
333
|
executorModel: String(models.executor ?? defaultModel),
|
|
295
334
|
verifierModel: String(models.verifier ?? defaultModel),
|
|
296
335
|
doctorModel: String(models.doctor ?? defaultModel),
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Prompt templates live in skills/oris-
|
|
6
|
+
* Prompt templates live in skills/oris-flow/templates/*.md so users and
|
|
7
7
|
* agents can read and copy them. Scripts fall back to an embedded
|
|
8
8
|
* orchestrator prompt so the stop hook can never fail on a missing file.
|
|
9
9
|
*/
|
|
@@ -12,6 +12,7 @@ export const TEMPLATE_NAMES = ["orchestrator", "executor", "verifier", "doctor",
|
|
|
12
12
|
export const EMBEDDED_ORCHESTRATOR = `ORIS LOOP RUN — pass {{iteration}}/{{maxIterations}}
|
|
13
13
|
Loop: {{loop}} | Phase: {{phase}} ({{phaseIndex}}/{{phaseCount}}) | Do: {{instruction}}
|
|
14
14
|
Definition: {{loopPath}} | Context: {{contextPath}} | Prompts: {{promptsDir}}
|
|
15
|
+
Active roles: {{roles}}
|
|
15
16
|
Models: executor={{executorModel}} verifier={{verifierModel}} doctor={{doctorModel}} debriefer={{debrieferModel}} ("inherit" = current session model)
|
|
16
17
|
Improve mode: {{improveMode}}
|
|
17
18
|
|
|
@@ -21,13 +22,15 @@ EACH PASS:
|
|
|
21
22
|
1. READ {{contextPath}}; pick exactly ONE bounded next step.
|
|
22
23
|
2. SPAWN executor subagent with {{promptsDir}}/executor.md (fill runtime values), model {{executorModel}}.
|
|
23
24
|
3. SPAWN verifier subagent with {{promptsDir}}/verifier.md + the executor claim, model {{verifierModel}}.
|
|
24
|
-
4.
|
|
25
|
+
4. DECIDE the state:
|
|
26
|
+
- doctor in active roles → SPAWN doctor subagent with {{promptsDir}}/doctor.md + both compact summaries, model {{doctorModel}}; use its decision.
|
|
27
|
+
- no doctor → map the verifier verdict: pass + goalMet → complete; pass → continue; fail → continue; blocked or inconclusive → ask-user.
|
|
25
28
|
5. WRITE a compact receipt to the loop receipts/ folder; update context.md with durable facts only.
|
|
26
|
-
6. RUN: \`oris-skills loop chat --action set-state --state <continue|advance|complete|blocked
|
|
27
|
-
7. END the turn with exactly: \`ORIS_LOOP_STATE: continue
|
|
29
|
+
6. RUN: \`npx oris-skills loop chat --action set-state --state <continue|advance|complete|blocked|ask-user> --progress <yes|no>\` (progress yes = the verifier confirmed forward movement).
|
|
30
|
+
7. END the turn with exactly: \`ORIS_LOOP_STATE: <continue|advance|complete|blocked|ask-user>\`.
|
|
28
31
|
|
|
29
|
-
IMPROVE ({{improveMode}}):
|
|
30
|
-
- auto → after the
|
|
32
|
+
IMPROVE ({{improveMode}}, only when debriefer is in active roles):
|
|
33
|
+
- auto → after the verdict, the debriefer may rewrite files under {{promptsDir}} (archive the old version to history/ first). It must NEVER change scope, limits, permissions, or verification in loop.md — those go to proposals/.
|
|
31
34
|
- propose → the debriefer writes proposals/ only; nothing changes until the user approves.
|
|
32
35
|
|
|
33
36
|
ALWAYS:
|
|
@@ -35,17 +38,17 @@ ALWAYS:
|
|
|
35
38
|
- Tell the user in one line what this pass will do before spawning workers.
|
|
36
39
|
|
|
37
40
|
NEVER:
|
|
38
|
-
- Continue when the
|
|
41
|
+
- Continue when the verdict is blocked, scope is violated, or the same failure repeats.
|
|
39
42
|
- Commit, push, deploy, edit .oris-flow/runtime/**, or touch paths outside the approved scope.
|
|
40
43
|
- Ask "run next pass?" after continue/advance — end the turn; the hook schedules the next pass.
|
|
41
44
|
|
|
42
|
-
IF
|
|
45
|
+
IF prompt files are missing or subagents cannot be spawned: \`set-state --state blocked\`, say why, end the turn.`;
|
|
43
46
|
|
|
44
47
|
export function resolveTemplatesDir(fromImportMetaUrl) {
|
|
45
48
|
const scriptsLoop = path.dirname(fileURLToPath(fromImportMetaUrl));
|
|
46
49
|
const candidates = [
|
|
47
|
-
path.resolve(scriptsLoop, "..", "..", "skills", "oris-
|
|
48
|
-
path.resolve(scriptsLoop, "..", "skills", "oris-
|
|
50
|
+
path.resolve(scriptsLoop, "..", "..", "skills", "oris-flow", "templates"),
|
|
51
|
+
path.resolve(scriptsLoop, "..", "skills", "oris-flow", "templates"),
|
|
49
52
|
];
|
|
50
53
|
for (const candidate of candidates) {
|
|
51
54
|
if (fs.existsSync(path.join(candidate, "orchestrator.md"))) return candidate;
|
|
@@ -98,7 +98,6 @@ if (options.temp) {
|
|
|
98
98
|
"--action", "start",
|
|
99
99
|
"--task", task.taskPath,
|
|
100
100
|
"--repository-root", repoRoot,
|
|
101
|
-
"--max-repair-cycles", "0",
|
|
102
101
|
], { cwd: repoRoot, encoding: "utf8" });
|
|
103
102
|
if (start.status !== 0) {
|
|
104
103
|
console.error(start.stderr || start.stdout);
|
|
@@ -132,7 +131,7 @@ const chat = readJson(statePath);
|
|
|
132
131
|
console.log("");
|
|
133
132
|
console.log("chat-active.json:");
|
|
134
133
|
if (!chat) {
|
|
135
|
-
console.log("(missing - run oris-skills loop chat --action start first)");
|
|
134
|
+
console.log("(missing - run npx oris-skills loop chat --action start first)");
|
|
136
135
|
} else {
|
|
137
136
|
console.log(JSON.stringify(chat, null, 2));
|
|
138
137
|
}
|
package/scripts/oris-skills.mjs
CHANGED
|
@@ -32,7 +32,7 @@ function printHelp() {
|
|
|
32
32
|
console.log(`Oris Skills
|
|
33
33
|
|
|
34
34
|
Install for your agents (Cursor, Claude Code, Codex):
|
|
35
|
-
oris-skills install [--agents cursor,claude,codex] [--dry-run]
|
|
35
|
+
oris-skills install [--agents cursor,claude,codex,copilot] [--dry-run]
|
|
36
36
|
oris-skills uninstall [--dry-run]
|
|
37
37
|
oris-skills reinstall [--dry-run]
|
|
38
38
|
|
|
@@ -92,7 +92,8 @@ if (command === "doctor") {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
if (command === "validate") {
|
|
95
|
-
|
|
95
|
+
// Kept as an alias of the full test suite for older docs and muscle memory.
|
|
96
|
+
runNode("tests/run-all-tests.mjs", []);
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
if (command === "loop") {
|
|
@@ -14,6 +14,7 @@ const nodeTestFiles = [
|
|
|
14
14
|
"scripts/tests/test-oris-1-0-cleanliness.mjs",
|
|
15
15
|
"scripts/tests/test-oris-flow-scan.mjs",
|
|
16
16
|
"scripts/tests/test-routing-lifecycle.mjs",
|
|
17
|
+
"scripts/tests/test-skill-style.mjs",
|
|
17
18
|
"scripts/tests/test-oris-loop-stop.mjs",
|
|
18
19
|
"scripts/tests/test-oris-loop-smoke.mjs",
|
|
19
20
|
"scripts/tests/test-oris-loop-bootstrap.mjs",
|
|
@@ -33,6 +34,4 @@ function run(command, args, options = {}) {
|
|
|
33
34
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const nodeOnly = process.argv.includes("--node-only");
|
|
37
37
|
run(process.execPath, ["--test", ...nodeTestFiles]);
|
|
38
|
-
|
|
@@ -3,7 +3,8 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
-
import
|
|
6
|
+
import childProcess from "node:child_process";
|
|
7
|
+
import { checkFreshness, scanRepository, writeScanResult } from "../flow/oris-flow-scan.mjs";
|
|
7
8
|
import { ORIS_FLOW_MANIFEST } from "../flow/oris-flow-layout.mjs";
|
|
8
9
|
|
|
9
10
|
function write(filePath, content) {
|
|
@@ -47,3 +48,110 @@ test("scanner supports documentation-only repositories", () => {
|
|
|
47
48
|
}
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
test("scanner detects project type and derives inferred stack commands", () => {
|
|
52
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
53
|
+
try {
|
|
54
|
+
write(path.join(root, "App.sln"), "");
|
|
55
|
+
write(path.join(root, "src", "App", "App.csproj"), "<Project />\n");
|
|
56
|
+
write(path.join(root, "src", "App", "Controllers", "HomeController.cs"), "class HomeController {}\n");
|
|
57
|
+
|
|
58
|
+
const scan = scanRepository(root);
|
|
59
|
+
assert.equal(scan.projectType.id, "api");
|
|
60
|
+
assert.ok(scan.stacks.some((stack) => stack.id === "dotnet"));
|
|
61
|
+
const dotnetTest = scan.commands.find((command) => command.command.startsWith("dotnet test"));
|
|
62
|
+
assert.ok(dotnetTest, "expected an inferred dotnet test command");
|
|
63
|
+
assert.equal(dotnetTest.confidence, "inferred");
|
|
64
|
+
assert.equal(scan.manifest.setup.tasksRoot, ".oris-flow/tasks");
|
|
65
|
+
} finally {
|
|
66
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("refresh preserves confirmed sections, commands, and tasksRoot", () => {
|
|
71
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
72
|
+
try {
|
|
73
|
+
write(path.join(root, "README.md"), "# Demo\n");
|
|
74
|
+
write(path.join(root, "package.json"), `${JSON.stringify({ scripts: { test: "node --test" } })}\n`);
|
|
75
|
+
|
|
76
|
+
const first = scanRepository(root);
|
|
77
|
+
first.manifest.setup.tasksRoot = "docs/tasks";
|
|
78
|
+
first.manifest.setup.confirmedDecisions = ["tests live under scripts/tests"];
|
|
79
|
+
first.manifest.sections.docs.confidence = "confirmed";
|
|
80
|
+
first.manifest.commands[0].confidence = "confirmed";
|
|
81
|
+
writeScanResult(first);
|
|
82
|
+
const enriched = "# Docs\n\nUser-confirmed enrichment that must survive refresh.\n";
|
|
83
|
+
write(path.join(root, ".oris-flow", "maps", "docs.md"), enriched);
|
|
84
|
+
|
|
85
|
+
const second = scanRepository(root);
|
|
86
|
+
assert.equal(second.manifest.setup.tasksRoot, "docs/tasks");
|
|
87
|
+
assert.deepEqual(second.manifest.setup.confirmedDecisions, ["tests live under scripts/tests"]);
|
|
88
|
+
assert.equal(second.manifest.sections.docs.confidence, "confirmed");
|
|
89
|
+
assert.equal(second.manifest.sections.docs.stale, false);
|
|
90
|
+
assert.equal(second.manifest.commands[0].confidence, "confirmed");
|
|
91
|
+
assert.ok(second.preservedSections.has("docs"));
|
|
92
|
+
|
|
93
|
+
writeScanResult(second);
|
|
94
|
+
assert.equal(fs.readFileSync(path.join(root, ".oris-flow", "maps", "docs.md"), "utf8"), enriched);
|
|
95
|
+
} finally {
|
|
96
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("confirmed section without fresh evidence is marked stale, not deleted", () => {
|
|
101
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
102
|
+
try {
|
|
103
|
+
write(path.join(root, "README.md"), "# Demo\n");
|
|
104
|
+
const first = scanRepository(root);
|
|
105
|
+
first.manifest.sections.docs.confidence = "confirmed";
|
|
106
|
+
writeScanResult(first);
|
|
107
|
+
|
|
108
|
+
fs.rmSync(path.join(root, "README.md"));
|
|
109
|
+
const second = scanRepository(root);
|
|
110
|
+
assert.equal(second.manifest.sections.docs.confidence, "confirmed");
|
|
111
|
+
assert.equal(second.manifest.sections.docs.stale, true);
|
|
112
|
+
} finally {
|
|
113
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
test("scan anchors the map to HEAD; --check answers freshness from the SHA + changed files", () => {
|
|
119
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
120
|
+
const git = (...args) => childProcess.execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
121
|
+
try {
|
|
122
|
+
write(path.join(root, "README.md"), "# Demo\n");
|
|
123
|
+
write(path.join(root, "package.json"), `${JSON.stringify({ scripts: { test: "node --test" } })}\n`);
|
|
124
|
+
git("init", "-q");
|
|
125
|
+
git("-c", "user.email=t@t", "-c", "user.name=t", "add", ".");
|
|
126
|
+
git("-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "init");
|
|
127
|
+
|
|
128
|
+
const scan = scanRepository(root);
|
|
129
|
+
assert.equal(typeof scan.manifest.headSha, "string", "scan records the HEAD anchor");
|
|
130
|
+
writeScanResult(scan);
|
|
131
|
+
|
|
132
|
+
const fresh = checkFreshness(root);
|
|
133
|
+
assert.equal(fresh.fresh, true, "same SHA + clean tree → fresh, zero reads");
|
|
134
|
+
assert.deepEqual(fresh.changedFiles, []);
|
|
135
|
+
|
|
136
|
+
write(path.join(root, "src", "new.js"), "export const later = 1;\n");
|
|
137
|
+
const stale = checkFreshness(root);
|
|
138
|
+
assert.equal(stale.fresh, false, "working-tree change → stale");
|
|
139
|
+
assert.ok(stale.changedFiles.some((file) => file.includes("new.js")), "changed files name the delta");
|
|
140
|
+
} finally {
|
|
141
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("outside git the map still works — no anchor, never an error", () => {
|
|
146
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "oris-flow-scan-"));
|
|
147
|
+
try {
|
|
148
|
+
write(path.join(root, "README.md"), "# Demo\n");
|
|
149
|
+
const scan = scanRepository(root);
|
|
150
|
+
assert.equal(scan.manifest.headSha, null);
|
|
151
|
+
writeScanResult(scan);
|
|
152
|
+
const check = checkFreshness(root);
|
|
153
|
+
assert.equal(check.fresh, false, "no anchor → refresh advised, not an error");
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import {
|
|
7
|
+
activeRoles,
|
|
7
8
|
assessPromptFiles,
|
|
8
9
|
improveMode,
|
|
9
10
|
normalizeModels,
|
|
@@ -16,6 +17,7 @@ kind: oris-loop
|
|
|
16
17
|
name: bug-fix-loop
|
|
17
18
|
approved: true
|
|
18
19
|
phases: [observe, execute, verify, debrief]
|
|
20
|
+
roles: [executor, verifier, debriefer]
|
|
19
21
|
limits:
|
|
20
22
|
maxIterations: 10
|
|
21
23
|
maxNoProgress: 2
|
|
@@ -79,6 +81,35 @@ test("rejects unknown improve modes", () => {
|
|
|
79
81
|
);
|
|
80
82
|
});
|
|
81
83
|
|
|
84
|
+
test("roles default to executor + verifier and must always include both", () => {
|
|
85
|
+
assert.deepEqual(activeRoles({}), ["executor", "verifier"]);
|
|
86
|
+
assert.deepEqual(activeRoles({ roles: ["executor", "verifier", "doctor"] }), ["executor", "verifier", "doctor"]);
|
|
87
|
+
assert.throws(
|
|
88
|
+
() => validateLoopDocumentText(
|
|
89
|
+
validLoop.replace("roles: [executor, verifier, debriefer]", "roles: [executor, debriefer]"),
|
|
90
|
+
"no-verifier.loop.md",
|
|
91
|
+
),
|
|
92
|
+
/roles must include verifier/,
|
|
93
|
+
);
|
|
94
|
+
assert.throws(
|
|
95
|
+
() => validateLoopDocumentText(
|
|
96
|
+
validLoop.replace("roles: [executor, verifier, debriefer]", "roles: [executor, verifier, narrator]"),
|
|
97
|
+
"bad-role.loop.md",
|
|
98
|
+
),
|
|
99
|
+
/roles must be an array/,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("improve.mode auto requires the debriefer role", () => {
|
|
104
|
+
assert.throws(
|
|
105
|
+
() => validateLoopDocumentText(
|
|
106
|
+
validLoop.replace("roles: [executor, verifier, debriefer]", "roles: [executor, verifier]"),
|
|
107
|
+
"auto-without-debriefer.loop.md",
|
|
108
|
+
),
|
|
109
|
+
/improve\.mode auto requires the debriefer role/,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
82
113
|
test("normalizeModels resolves inherit, adapter aliases, and role fallbacks", () => {
|
|
83
114
|
const models = normalizeModels(
|
|
84
115
|
{ default: "inherit", verifier: "smart", doctor: "explicit-model-id" },
|
|
@@ -96,16 +127,21 @@ test("improveMode defaults to propose", () => {
|
|
|
96
127
|
assert.equal(improveMode({ improve: { mode: "auto" } }), "auto");
|
|
97
128
|
});
|
|
98
129
|
|
|
99
|
-
test("assessPromptFiles reports missing
|
|
130
|
+
test("assessPromptFiles reports missing prompts for the active roles only", () => {
|
|
100
131
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "oris-prompts-"));
|
|
101
132
|
try {
|
|
102
133
|
const empty = assessPromptFiles(dir);
|
|
103
|
-
assert.deepEqual(empty.missing, ["prompts/executor.md", "prompts/verifier.md"
|
|
134
|
+
assert.deepEqual(empty.missing, ["prompts/executor.md", "prompts/verifier.md"]);
|
|
104
135
|
fs.mkdirSync(path.join(dir, "prompts"), { recursive: true });
|
|
105
|
-
for (const file of ["executor.md", "verifier.md"
|
|
136
|
+
for (const file of ["executor.md", "verifier.md"]) {
|
|
106
137
|
fs.writeFileSync(path.join(dir, "prompts", file), "prompt content\n", "utf8");
|
|
107
138
|
}
|
|
108
139
|
assert.deepEqual(assessPromptFiles(dir).missing, []);
|
|
140
|
+
assert.deepEqual(
|
|
141
|
+
assessPromptFiles(dir, ["executor", "verifier", "doctor"]).missing,
|
|
142
|
+
["prompts/doctor.md"],
|
|
143
|
+
"opting in a role requires its prompt file",
|
|
144
|
+
);
|
|
109
145
|
} finally {
|
|
110
146
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
111
147
|
}
|