oris-skills 2.2.3 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +2 -3
- package/CHANGELOG.md +24 -6
- package/README.md +28 -60
- package/docs/architecture.md +15 -11
- package/docs/distribution.md +3 -3
- package/docs/maintainer-guide.md +4 -4
- package/docs/user-guide.md +12 -20
- package/package.json +3 -6
- package/references/clean-code-checklist.md +2 -2
- package/references/conventions.md +36 -16
- package/references/doc-policy.md +5 -5
- package/references/research.md +45 -0
- package/references/settings.md +11 -5
- package/references/settings.schema.json +10 -0
- package/scripts/flow/oris-flow-layout.mjs +0 -16
- package/scripts/flow/oris-flow-scan.mjs +32 -317
- package/scripts/flow/oris-gitignore.mjs +1 -5
- package/scripts/install/install-user-skills.mjs +3 -3
- package/scripts/install/uninstall-user-skills.mjs +2 -29
- package/scripts/oris-skills.mjs +0 -47
- package/scripts/tests/run-all-tests.mjs +1 -9
- package/scripts/tests/test-cleanliness.mjs +54 -0
- package/scripts/tests/test-oris-flow-scan.mjs +21 -91
- package/scripts/tests/test-routing-lifecycle.mjs +24 -54
- package/scripts/tests/test-schemas.mjs +17 -31
- package/scripts/tests/test-skill-style.mjs +5 -2
- package/skills/oris-flow/SKILL.md +21 -11
- package/skills/oris-flow/agents/openai.yaml +1 -1
- package/skills/oris-flow/references/architecture.md +6 -3
- package/skills/oris-flow/references/change.md +3 -0
- package/skills/oris-flow/references/commit.md +9 -8
- package/skills/oris-flow/references/criteria.md +9 -5
- package/skills/oris-flow/references/discover.md +2 -2
- package/skills/oris-flow/references/docs.md +5 -1
- package/skills/oris-flow/references/fix.md +10 -5
- package/skills/oris-flow/references/handoff.md +67 -0
- package/skills/oris-flow/references/help.md +3 -6
- package/skills/oris-flow/references/implement.md +7 -4
- package/skills/oris-flow/references/new.md +6 -6
- package/skills/oris-flow/references/plan.md +7 -2
- package/skills/oris-flow/references/pr.md +2 -2
- package/skills/oris-flow/references/setup.md +33 -39
- package/skills/oris-flow/references/triage.md +69 -0
- package/skills/oris-flow/references/verify.md +5 -5
- package/agents/oris-loop-debriefer.md +0 -30
- package/agents/oris-loop-doctor.md +0 -32
- package/agents/oris-loop-executor.md +0 -35
- package/agents/oris-loop-verifier.md +0 -35
- package/references/loop-adapter.schema.json +0 -93
- package/references/loop-contract.md +0 -126
- package/references/loop.schema.json +0 -156
- package/references/repo-map.md +0 -51
- package/references/repo-map.schema.json +0 -213
- package/scripts/flow/oris-flow-clean-runtime.mjs +0 -182
- package/scripts/install/generate-agent-adapters.mjs +0 -81
- package/scripts/loop/oris-loop-bootstrap.mjs +0 -383
- package/scripts/loop/oris-loop-bundle.mjs +0 -22
- package/scripts/loop/oris-loop-chat.mjs +0 -396
- package/scripts/loop/oris-loop-demo.mjs +0 -171
- package/scripts/loop/oris-loop-document.mjs +0 -196
- package/scripts/loop/oris-loop-dry-run.mjs +0 -114
- package/scripts/loop/oris-loop-fixtures.mjs +0 -149
- package/scripts/loop/oris-loop-list.mjs +0 -81
- package/scripts/loop/oris-loop-paths.mjs +0 -232
- package/scripts/loop/oris-loop-run.mjs +0 -301
- package/scripts/loop/oris-loop-stop.mjs +0 -358
- package/scripts/loop/oris-loop-templates.mjs +0 -80
- package/scripts/loop/oris-loop-verify.mjs +0 -205
- package/scripts/tests/test-agent-adapters.mjs +0 -70
- package/scripts/tests/test-oris-1-0-cleanliness.mjs +0 -58
- package/scripts/tests/test-oris-flow-clean-runtime.mjs +0 -75
- package/scripts/tests/test-oris-loop-bootstrap.mjs +0 -94
- package/scripts/tests/test-oris-loop-document.mjs +0 -148
- package/scripts/tests/test-oris-loop-list.mjs +0 -74
- package/scripts/tests/test-oris-loop-run.mjs +0 -71
- package/scripts/tests/test-oris-loop-smoke.mjs +0 -120
- package/scripts/tests/test-oris-loop-stop.mjs +0 -432
- package/skills/oris-flow/references/loop-craft.md +0 -59
- package/skills/oris-flow/references/loop-improve.md +0 -32
- package/skills/oris-flow/references/loop-run.md +0 -47
- package/skills/oris-flow/references/loop.md +0 -56
- package/skills/oris-flow/templates/debriefer.md +0 -19
- package/skills/oris-flow/templates/doctor.md +0 -22
- package/skills/oris-flow/templates/executor.md +0 -25
- package/skills/oris-flow/templates/orchestrator.md +0 -36
- package/skills/oris-flow/templates/verifier.md +0 -24
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import childProcess from "node:child_process";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import process from "node:process";
|
|
7
|
-
import {
|
|
8
|
-
isTaskPathUnderAdapterRoots,
|
|
9
|
-
readAdapter,
|
|
10
|
-
resolveLoopDocumentPaths,
|
|
11
|
-
resolveLoopDocumentPathsBySlug,
|
|
12
|
-
runtimeRelativePath,
|
|
13
|
-
} from "./oris-loop-paths.mjs";
|
|
14
|
-
import {
|
|
15
|
-
ORIS_FLOW_ADAPTER,
|
|
16
|
-
ORIS_LOOP_SCHEMA,
|
|
17
|
-
ORIS_LOOP_SCHEMA_VERSION,
|
|
18
|
-
} from "../flow/oris-flow-layout.mjs";
|
|
19
|
-
import {
|
|
20
|
-
activeRoles,
|
|
21
|
-
assessPromptFiles,
|
|
22
|
-
improveMode,
|
|
23
|
-
normalizeModels,
|
|
24
|
-
validateLoopDocumentText,
|
|
25
|
-
} from "./oris-loop-document.mjs";
|
|
26
|
-
|
|
27
|
-
const terminalStates = new Set(["complete", "blocked", "cancelled"]);
|
|
28
|
-
|
|
29
|
-
function fail(message) {
|
|
30
|
-
throw new Error(message);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseArgs(argv) {
|
|
34
|
-
const options = {
|
|
35
|
-
action: "status",
|
|
36
|
-
loopSlug: "",
|
|
37
|
-
taskPath: "",
|
|
38
|
-
state: "continue",
|
|
39
|
-
progress: "",
|
|
40
|
-
repositoryRoot: "",
|
|
41
|
-
runner: "chat",
|
|
42
|
-
maxIterations: 10,
|
|
43
|
-
maxMinutes: 240,
|
|
44
|
-
};
|
|
45
|
-
const readValue = (index, raw) => {
|
|
46
|
-
if (raw.includes("=")) return [index, raw.slice(raw.indexOf("=") + 1)];
|
|
47
|
-
if (index + 1 >= argv.length) fail(`Missing value for ${raw}.`);
|
|
48
|
-
return [index + 1, argv[index + 1]];
|
|
49
|
-
};
|
|
50
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
51
|
-
const raw = argv[i];
|
|
52
|
-
const key = raw.toLowerCase();
|
|
53
|
-
if (key === "--action" || key.startsWith("--action=")) [i, options.action] = readValue(i, raw);
|
|
54
|
-
else if (key === "--loop" || key === "--loop-slug" || key.startsWith("--loop=") || key.startsWith("--loop-slug=")) [i, options.loopSlug] = readValue(i, raw);
|
|
55
|
-
else if (key === "--task" || key === "--task-path" || key.startsWith("--task=") || key.startsWith("--task-path=")) [i, options.taskPath] = readValue(i, raw);
|
|
56
|
-
else if (key === "--state" || key.startsWith("--state=")) [i, options.state] = readValue(i, raw);
|
|
57
|
-
else if (key === "--progress" || key.startsWith("--progress=")) [i, options.progress] = readValue(i, raw);
|
|
58
|
-
else if (key === "--repository-root" || key.startsWith("--repository-root=")) [i, options.repositoryRoot] = readValue(i, raw);
|
|
59
|
-
else if (key === "--runner" || key.startsWith("--runner=")) [i, options.runner] = readValue(i, raw);
|
|
60
|
-
else if (key === "--max-iterations" || key.startsWith("--max-iterations=")) {
|
|
61
|
-
const parsed = readValue(i, raw);
|
|
62
|
-
i = parsed[0];
|
|
63
|
-
options.maxIterations = Number.parseInt(parsed[1], 10);
|
|
64
|
-
} else if (key === "--max-minutes" || key.startsWith("--max-minutes=")) {
|
|
65
|
-
const parsed = readValue(i, raw);
|
|
66
|
-
i = parsed[0];
|
|
67
|
-
options.maxMinutes = Number.parseInt(parsed[1], 10);
|
|
68
|
-
} else fail(`Unknown argument: ${raw}`);
|
|
69
|
-
}
|
|
70
|
-
options.action = options.action.toLowerCase();
|
|
71
|
-
options.state = options.state.toLowerCase();
|
|
72
|
-
options.progress = options.progress.toLowerCase();
|
|
73
|
-
options.runner = options.runner.toLowerCase();
|
|
74
|
-
if (!["start", "set-state", "stop", "status", "repair"].includes(options.action)) fail("Unsupported --action.");
|
|
75
|
-
if (!["continue", "advance", "complete", "blocked", "cancelled", "ask-user"].includes(options.state)) fail("Unsupported --state.");
|
|
76
|
-
if (options.progress && !["yes", "no"].includes(options.progress)) fail("Unsupported --progress (yes|no).");
|
|
77
|
-
if (!["chat", "headless"].includes(options.runner)) fail("Unsupported --runner (chat|headless).");
|
|
78
|
-
return options;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function run(command, args, cwd) {
|
|
82
|
-
return childProcess.spawnSync(command, args, {
|
|
83
|
-
cwd,
|
|
84
|
-
shell: process.platform === "win32",
|
|
85
|
-
encoding: "utf8",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getRoot(options) {
|
|
90
|
-
if (options.repositoryRoot) return path.resolve(options.repositoryRoot);
|
|
91
|
-
const result = run("git", ["rev-parse", "--show-toplevel"], process.cwd());
|
|
92
|
-
if (result.status !== 0 || !result.stdout.trim()) fail("Run inside a Git worktree or pass --repository-root.");
|
|
93
|
-
return path.resolve(result.stdout.trim());
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function writeJson(filePath, value) {
|
|
97
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
98
|
-
const temporary = `${filePath}.${process.pid}.tmp`;
|
|
99
|
-
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
|
100
|
-
encoding: "utf8",
|
|
101
|
-
mode: 0o600,
|
|
102
|
-
});
|
|
103
|
-
fs.renameSync(temporary, filePath);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function setContextField(content, name, value) {
|
|
107
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
108
|
-
const pattern = new RegExp(`^${escaped}:\\s*.*$`, "m");
|
|
109
|
-
const replacement = `${name}: ${value}`;
|
|
110
|
-
return pattern.test(content) ? content.replace(pattern, replacement) : `${content.trimEnd()}\n${replacement}\n`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function reconcileContext(root, state) {
|
|
114
|
-
if (!state.taskPath) return;
|
|
115
|
-
const contextPath = state.contextPath ? path.resolve(root, state.contextPath) : "";
|
|
116
|
-
if (!contextPath) return;
|
|
117
|
-
const contextRelative = path.relative(root, contextPath);
|
|
118
|
-
if (contextRelative.startsWith("..") || path.isAbsolute(contextRelative)) return;
|
|
119
|
-
if (!fs.existsSync(contextPath)) return;
|
|
120
|
-
let content = fs.readFileSync(contextPath, "utf8");
|
|
121
|
-
const phases = Array.isArray(state.phases) ? state.phases : ["execute"];
|
|
122
|
-
const phase = currentPhase({ ...state, phases });
|
|
123
|
-
for (const [name, value] of Object.entries({
|
|
124
|
-
Run: state.active === true ? "active" : "inactive",
|
|
125
|
-
State: state.state,
|
|
126
|
-
"Current program": state.program,
|
|
127
|
-
"Current phase": phase,
|
|
128
|
-
"Current phase index": Number(state.phaseIndex ?? 0),
|
|
129
|
-
"Current iteration": Number(state.iteration ?? 0),
|
|
130
|
-
"Phase iteration": Number(state.phaseIteration ?? 0),
|
|
131
|
-
"No-progress streak": Number(state.noProgressStreak ?? 0),
|
|
132
|
-
Updated: new Date().toISOString(),
|
|
133
|
-
})) {
|
|
134
|
-
content = setContextField(content, name, value);
|
|
135
|
-
}
|
|
136
|
-
const temporary = `${contextPath}.${process.pid}.tmp`;
|
|
137
|
-
fs.writeFileSync(temporary, content, { encoding: "utf8", mode: 0o600 });
|
|
138
|
-
fs.renameSync(temporary, contextPath);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** A same-chat loop needs at least one armed stop hook: Cursor or Claude Code. */
|
|
142
|
-
function assessStopHooks(root) {
|
|
143
|
-
const registered = [];
|
|
144
|
-
const cursorHooks = path.join(root, ".cursor", "hooks.json");
|
|
145
|
-
if (fs.existsSync(cursorHooks)) {
|
|
146
|
-
try {
|
|
147
|
-
const hooks = JSON.parse(fs.readFileSync(cursorHooks, "utf8"));
|
|
148
|
-
const stops = hooks?.hooks?.stop ?? [];
|
|
149
|
-
if (stops.some((entry) => String(entry?.command ?? "").includes("oris-loop-stop"))) registered.push("cursor");
|
|
150
|
-
} catch {
|
|
151
|
-
// Unreadable hooks.json counts as not registered.
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const claudeSettings = path.join(root, ".claude", "settings.json");
|
|
155
|
-
if (fs.existsSync(claudeSettings)) {
|
|
156
|
-
try {
|
|
157
|
-
const settings = JSON.parse(fs.readFileSync(claudeSettings, "utf8"));
|
|
158
|
-
const stops = settings?.hooks?.Stop ?? [];
|
|
159
|
-
const flat = JSON.stringify(stops);
|
|
160
|
-
if (flat.includes("oris-loop-stop")) registered.push("claude");
|
|
161
|
-
} catch {
|
|
162
|
-
// Unreadable settings.json counts as not registered.
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return registered;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function currentPhase(state) {
|
|
169
|
-
const phases = Array.isArray(state.phases) && state.phases.length > 0 ? state.phases : ["execute"];
|
|
170
|
-
const index = Number.isInteger(Number(state.phaseIndex)) ? Number(state.phaseIndex) : 0;
|
|
171
|
-
return phases[Math.max(0, Math.min(index, phases.length - 1))] ?? phases[0];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function normalizeChatState(state) {
|
|
175
|
-
if (!state || typeof state !== "object") fail("Invalid Oris chat loop state.");
|
|
176
|
-
const normalized = { ...state };
|
|
177
|
-
const version = Number(normalized.schemaVersion ?? 0);
|
|
178
|
-
if (version !== 1) {
|
|
179
|
-
fail(`Unsupported chat loop schemaVersion: ${normalized.schemaVersion}`);
|
|
180
|
-
}
|
|
181
|
-
normalized.phases = Array.isArray(normalized.phases) && normalized.phases.length > 0
|
|
182
|
-
? normalized.phases
|
|
183
|
-
: ["execute"];
|
|
184
|
-
normalized.phaseName = currentPhase(normalized);
|
|
185
|
-
normalized.iteration = Number(normalized.iteration ?? 0);
|
|
186
|
-
normalized.phaseIteration = Number(normalized.phaseIteration ?? 0);
|
|
187
|
-
normalized.noProgressStreak = Number(normalized.noProgressStreak ?? 0);
|
|
188
|
-
normalized.phaseIndex = Number.isInteger(Number(normalized.phaseIndex)) ? Number(normalized.phaseIndex) : 0;
|
|
189
|
-
if (normalized.state === "running" && normalized.active === true) {
|
|
190
|
-
normalized.state = "continue";
|
|
191
|
-
}
|
|
192
|
-
return normalized;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function readChatState() {
|
|
196
|
-
return normalizeChatState(JSON.parse(fs.readFileSync(statePath, "utf8")));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const options = parseArgs(process.argv.slice(2));
|
|
200
|
-
const root = getRoot(options);
|
|
201
|
-
const adapterForRuntime = readAdapter(root, ORIS_FLOW_ADAPTER);
|
|
202
|
-
const runtimeRoot = path.join(root, runtimeRelativePath(adapterForRuntime));
|
|
203
|
-
const statePath = path.join(runtimeRoot, "chat-active.json");
|
|
204
|
-
|
|
205
|
-
if (options.action === "start") {
|
|
206
|
-
if (!options.taskPath && !options.loopSlug) fail("--task or --loop is required for start.");
|
|
207
|
-
if (options.taskPath && options.loopSlug) fail("Use either --task or --loop for start, not both.");
|
|
208
|
-
const adapterPath = path.join(root, ORIS_FLOW_ADAPTER);
|
|
209
|
-
if (!fs.existsSync(adapterPath)) fail(`Repository adapter is missing: ${adapterPath}. Run: oris-skills loop bootstrap`);
|
|
210
|
-
const adapter = JSON.parse(fs.readFileSync(adapterPath, "utf8"));
|
|
211
|
-
if (
|
|
212
|
-
adapter.$schema !== ORIS_LOOP_SCHEMA
|
|
213
|
-
|| adapter.schemaVersion !== ORIS_LOOP_SCHEMA_VERSION
|
|
214
|
-
|| !adapter.paths
|
|
215
|
-
|| !Array.isArray(adapter.paths.allowed)
|
|
216
|
-
) {
|
|
217
|
-
fail(`Repository adapter must use schemaVersion ${ORIS_LOOP_SCHEMA_VERSION}: ${adapterPath}`);
|
|
218
|
-
}
|
|
219
|
-
if (options.taskPath) {
|
|
220
|
-
const taskFull = path.resolve(root, options.taskPath);
|
|
221
|
-
const relative = path.relative(root, taskFull);
|
|
222
|
-
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) fail("--task must be below the repository root.");
|
|
223
|
-
if (!isTaskPathUnderAdapterRoots(root, options.taskPath, adapter)) {
|
|
224
|
-
fail("--task is outside adapter paths.taskRoots.");
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const loopDocs = options.loopSlug
|
|
228
|
-
? resolveLoopDocumentPathsBySlug(root, options.loopSlug, adapter)
|
|
229
|
-
: resolveLoopDocumentPaths(root, options, adapter);
|
|
230
|
-
const loopPath = loopDocs.loopFull;
|
|
231
|
-
const contextPath = loopDocs.contextFull;
|
|
232
|
-
for (const required of [adapterPath, loopPath, contextPath]) {
|
|
233
|
-
if (!fs.existsSync(required)) fail(`Required loop file is missing: ${required}`);
|
|
234
|
-
}
|
|
235
|
-
if (options.runner === "chat") {
|
|
236
|
-
const hooks = assessStopHooks(root);
|
|
237
|
-
if (hooks.length === 0) {
|
|
238
|
-
fail("No Oris stop hook is registered (.cursor/hooks.json or .claude/settings.json). Run: oris-skills loop bootstrap");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
const minimumFreeDiskGiB = Number(adapter.preflight?.minFreeDiskGiB ?? 0);
|
|
242
|
-
if (typeof fs.statfsSync === "function") {
|
|
243
|
-
const disk = fs.statfsSync(root);
|
|
244
|
-
const freeDiskGiB = Number(disk.bavail) * Number(disk.bsize) / 1024 ** 3;
|
|
245
|
-
if (freeDiskGiB < minimumFreeDiskGiB) {
|
|
246
|
-
fail(`Disk preflight failed: ${freeDiskGiB.toFixed(2)} GiB free; ${minimumFreeDiskGiB} GiB required.`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
const loopText = fs.readFileSync(loopPath, "utf8");
|
|
250
|
-
let loopDocument;
|
|
251
|
-
try {
|
|
252
|
-
loopDocument = validateLoopDocumentText(loopText, loopPath);
|
|
253
|
-
} catch (error) {
|
|
254
|
-
fail(error.message);
|
|
255
|
-
}
|
|
256
|
-
const prompts = assessPromptFiles(path.dirname(loopPath), activeRoles(loopDocument.data));
|
|
257
|
-
if (prompts.missing.length > 0) {
|
|
258
|
-
fail(`Loop prompt files are missing or empty: ${prompts.missing.join(", ")}. Craft or copy them from skills/oris-flow/templates/.`);
|
|
259
|
-
}
|
|
260
|
-
if (fs.existsSync(statePath)) {
|
|
261
|
-
const existing = normalizeChatState(JSON.parse(fs.readFileSync(statePath, "utf8")));
|
|
262
|
-
// ask-user is a pause awaiting input, not an active run — restart may take over.
|
|
263
|
-
if (existing.active === true && !terminalStates.has(existing.state) && existing.state !== "ask-user") {
|
|
264
|
-
fail(`An Oris chat loop is already active for this workspace: ${existing.taskPath}. Run --action stop before starting again.`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const loopMetadata = loopDocument.data;
|
|
268
|
-
const phases = [...loopMetadata.phases];
|
|
269
|
-
const nextState = {
|
|
270
|
-
schemaVersion: 1,
|
|
271
|
-
active: true,
|
|
272
|
-
state: "continue",
|
|
273
|
-
runner: options.runner,
|
|
274
|
-
program: loopMetadata.name,
|
|
275
|
-
phases,
|
|
276
|
-
phaseIndex: 0,
|
|
277
|
-
phaseName: phases[0],
|
|
278
|
-
phaseIteration: 0,
|
|
279
|
-
taskPath: (options.taskPath || loopDocs.taskPath).replaceAll("\\", "/"),
|
|
280
|
-
repositoryRoot: root.replaceAll("\\", "/"),
|
|
281
|
-
loopPath: loopDocs.loopRelative,
|
|
282
|
-
contextPath: loopDocs.contextRelative,
|
|
283
|
-
promptsPath: loopDocs.promptsRelative,
|
|
284
|
-
roles: activeRoles(loopMetadata),
|
|
285
|
-
models: normalizeModels(loopMetadata.models, adapter.models),
|
|
286
|
-
improveMode: improveMode(loopMetadata),
|
|
287
|
-
conversationId: null,
|
|
288
|
-
iteration: 0,
|
|
289
|
-
maxIterations: loopMetadata.limits.maxIterations,
|
|
290
|
-
noProgressStreak: 0,
|
|
291
|
-
maxNoProgress: loopMetadata.limits.maxNoProgress,
|
|
292
|
-
maxMinutes: loopMetadata.limits.maxMinutes ?? options.maxMinutes,
|
|
293
|
-
startedAt: new Date().toISOString(),
|
|
294
|
-
updatedAt: new Date().toISOString(),
|
|
295
|
-
};
|
|
296
|
-
writeJson(statePath, nextState);
|
|
297
|
-
reconcileContext(root, nextState);
|
|
298
|
-
console.log(`Oris loop armed (${options.runner}): ${loopMetadata.name} / ${(options.taskPath || loopDocs.taskPath).replaceAll("\\", "/")}`);
|
|
299
|
-
process.exit(0);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (!fs.existsSync(statePath)) {
|
|
303
|
-
if (options.action === "status") {
|
|
304
|
-
console.log("No Oris chat loop state.");
|
|
305
|
-
process.exit(0);
|
|
306
|
-
}
|
|
307
|
-
fail(`No Oris chat loop state exists at ${statePath}`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const current = readChatState();
|
|
311
|
-
|
|
312
|
-
if (options.action === "repair") {
|
|
313
|
-
if (current.active === true) {
|
|
314
|
-
current.startedAt = new Date().toISOString();
|
|
315
|
-
delete current.stopReason;
|
|
316
|
-
} else if (
|
|
317
|
-
current.state === "blocked"
|
|
318
|
-
&& /limit exhausted/.test(String(current.stopReason ?? ""))
|
|
319
|
-
&& Number(current.iteration ?? 0) < Number(current.maxIterations ?? 25)
|
|
320
|
-
) {
|
|
321
|
-
current.active = true;
|
|
322
|
-
current.state = "continue";
|
|
323
|
-
current.noProgressStreak = 0;
|
|
324
|
-
current.startedAt = new Date().toISOString();
|
|
325
|
-
delete current.stopReason;
|
|
326
|
-
}
|
|
327
|
-
current.conversationId = null;
|
|
328
|
-
current.updatedAt = new Date().toISOString();
|
|
329
|
-
writeJson(statePath, current);
|
|
330
|
-
reconcileContext(root, current);
|
|
331
|
-
console.log(`Oris chat loop repaired: schemaVersion ${current.schemaVersion}, state ${current.state}.`);
|
|
332
|
-
process.exit(0);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (options.action === "set-state") {
|
|
336
|
-
const phases = Array.isArray(current.phases) && current.phases.length > 0 ? current.phases : ["execute"];
|
|
337
|
-
if (options.state === "advance") {
|
|
338
|
-
current.phaseIndex = phases.length === 1 ? 0 : (Number(current.phaseIndex ?? 0) + 1) % phases.length;
|
|
339
|
-
current.phaseIteration = 0;
|
|
340
|
-
current.state = "advance";
|
|
341
|
-
current.active = true;
|
|
342
|
-
} else {
|
|
343
|
-
current.state = options.state;
|
|
344
|
-
current.active = !terminalStates.has(options.state);
|
|
345
|
-
}
|
|
346
|
-
if (options.progress) {
|
|
347
|
-
current.noProgressStreak = options.progress === "yes" ? 0 : Number(current.noProgressStreak ?? 0) + 1;
|
|
348
|
-
}
|
|
349
|
-
const maxNoProgress = Number(current.maxNoProgress ?? 0);
|
|
350
|
-
if (
|
|
351
|
-
maxNoProgress >= 1
|
|
352
|
-
&& Number(current.noProgressStreak ?? 0) >= maxNoProgress
|
|
353
|
-
&& (current.state === "continue" || current.state === "advance")
|
|
354
|
-
) {
|
|
355
|
-
current.state = "blocked";
|
|
356
|
-
current.active = false;
|
|
357
|
-
current.stopReason = "no-progress limit exhausted";
|
|
358
|
-
}
|
|
359
|
-
current.phases = phases;
|
|
360
|
-
current.phaseName = currentPhase(current);
|
|
361
|
-
current.schemaVersion = 1;
|
|
362
|
-
current.updatedAt = new Date().toISOString();
|
|
363
|
-
writeJson(statePath, current);
|
|
364
|
-
reconcileContext(root, current);
|
|
365
|
-
console.log(`Oris chat loop state: ${current.state}${current.stopReason ? ` (${current.stopReason})` : ""}`);
|
|
366
|
-
process.exit(0);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (options.action === "stop") {
|
|
370
|
-
current.state = "cancelled";
|
|
371
|
-
current.active = false;
|
|
372
|
-
current.stopReason = "manual stop";
|
|
373
|
-
current.updatedAt = new Date().toISOString();
|
|
374
|
-
writeJson(statePath, current);
|
|
375
|
-
reconcileContext(root, current);
|
|
376
|
-
console.log("Oris chat loop cancelled.");
|
|
377
|
-
process.exit(0);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
console.log(JSON.stringify({
|
|
381
|
-
active: Boolean(current.active),
|
|
382
|
-
state: current.state,
|
|
383
|
-
runner: current.runner ?? "chat",
|
|
384
|
-
program: current.program,
|
|
385
|
-
phase: currentPhase(current),
|
|
386
|
-
taskPath: current.taskPath,
|
|
387
|
-
iteration: Number(current.iteration ?? 0),
|
|
388
|
-
maxIterations: Number(current.maxIterations ?? 0),
|
|
389
|
-
noProgressStreak: Number(current.noProgressStreak ?? 0),
|
|
390
|
-
maxNoProgress: Number(current.maxNoProgress ?? 0),
|
|
391
|
-
roles: Array.isArray(current.roles) ? current.roles : ["executor", "verifier"],
|
|
392
|
-
improveMode: current.improveMode ?? "propose",
|
|
393
|
-
models: current.models ?? {},
|
|
394
|
-
conversationBound: Boolean(current.conversationId),
|
|
395
|
-
updatedAt: current.updatedAt,
|
|
396
|
-
}, null, 2));
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* `oris-skills loop demo` — create a safe tutorial loop the user can run to
|
|
5
|
-
* learn Oris loops end to end. The demo only writes inside its own loop
|
|
6
|
-
* folder, so it cannot touch product files.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import childProcess from "node:child_process";
|
|
10
|
-
import fs from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import process from "node:process";
|
|
13
|
-
import { bootstrapLoop } from "./oris-loop-bootstrap.mjs";
|
|
14
|
-
import { defaultLoopDocumentPaths, readAdapter } from "./oris-loop-paths.mjs";
|
|
15
|
-
|
|
16
|
-
const DEMO_SLUG = "oris-demo";
|
|
17
|
-
|
|
18
|
-
function fail(message) {
|
|
19
|
-
console.error(`[oris-skills] ERROR ${message}`);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function parseArgs(argv) {
|
|
24
|
-
const options = { repositoryRoot: "", force: false };
|
|
25
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
26
|
-
const raw = argv[i];
|
|
27
|
-
if (raw === "--force") options.force = true;
|
|
28
|
-
else if (raw.startsWith("--repository-root=")) options.repositoryRoot = raw.slice("--repository-root=".length);
|
|
29
|
-
else if (raw === "--repository-root" && argv[i + 1]) {
|
|
30
|
-
options.repositoryRoot = argv[i + 1];
|
|
31
|
-
i += 1;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return options;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getRoot(options) {
|
|
38
|
-
if (options.repositoryRoot) return path.resolve(options.repositoryRoot);
|
|
39
|
-
const result = childProcess.spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
40
|
-
cwd: process.cwd(),
|
|
41
|
-
shell: process.platform === "win32",
|
|
42
|
-
encoding: "utf8",
|
|
43
|
-
});
|
|
44
|
-
if (result.status !== 0 || !result.stdout.trim()) fail("Run inside a Git worktree or pass --repository-root.");
|
|
45
|
-
return path.resolve(result.stdout.trim());
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const options = parseArgs(process.argv.slice(2));
|
|
49
|
-
const root = getRoot(options);
|
|
50
|
-
|
|
51
|
-
const bootstrap = bootstrapLoop(root, {});
|
|
52
|
-
if (!bootstrap.ready) fail(`Loop bootstrap incomplete: ${bootstrap.after.missing.join(", ")}`);
|
|
53
|
-
|
|
54
|
-
const adapter = readAdapter(root);
|
|
55
|
-
const docs = defaultLoopDocumentPaths(root, DEMO_SLUG, adapter);
|
|
56
|
-
const loopDir = path.dirname(docs.loopFull);
|
|
57
|
-
if (fs.existsSync(docs.loopFull) && !options.force) {
|
|
58
|
-
fail(`Demo loop already exists: ${docs.loopRelative}. Re-run with --force to recreate it.`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const workspaceRelative = `${docs.loopDir}/workspace`.replace(/\\/g, "/");
|
|
62
|
-
fs.mkdirSync(path.join(loopDir, "workspace"), { recursive: true });
|
|
63
|
-
fs.mkdirSync(path.join(loopDir, "prompts"), { recursive: true });
|
|
64
|
-
fs.mkdirSync(path.join(loopDir, "receipts"), { recursive: true });
|
|
65
|
-
fs.mkdirSync(path.join(loopDir, "proposals"), { recursive: true });
|
|
66
|
-
fs.mkdirSync(path.join(loopDir, "history"), { recursive: true });
|
|
67
|
-
|
|
68
|
-
fs.writeFileSync(path.join(loopDir, "workspace", "counter.txt"), "0\n", "utf8");
|
|
69
|
-
|
|
70
|
-
fs.writeFileSync(docs.loopFull, `---
|
|
71
|
-
schemaVersion: 2
|
|
72
|
-
kind: oris-loop
|
|
73
|
-
name: ${DEMO_SLUG}
|
|
74
|
-
approved: true
|
|
75
|
-
phases: [work]
|
|
76
|
-
roles: [executor, verifier, debriefer]
|
|
77
|
-
limits:
|
|
78
|
-
maxIterations: 3
|
|
79
|
-
maxNoProgress: 2
|
|
80
|
-
maxMinutes: 30
|
|
81
|
-
models:
|
|
82
|
-
default: inherit
|
|
83
|
-
improve:
|
|
84
|
-
mode: auto
|
|
85
|
-
permissions:
|
|
86
|
-
allowedPaths: [${workspaceRelative}, ${docs.loopDir.replace(/\\/g, "/")}]
|
|
87
|
-
forbiddenActions: [commit, push, deploy, external-message]
|
|
88
|
-
verification:
|
|
89
|
-
commands: []
|
|
90
|
-
evidenceRequired: true
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
# Oris demo loop
|
|
94
|
-
|
|
95
|
-
Goal: increment the number in \`${workspaceRelative}/counter.txt\` by 1 per pass until it reaches 3.
|
|
96
|
-
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:
|
|
99
|
-
- it only touches files inside its own folder;
|
|
100
|
-
- it runs 3 short passes, then completes;
|
|
101
|
-
- roles are executor + verifier (the default) plus the debriefer;
|
|
102
|
-
- improve.mode is \`auto\`, so you can watch the debriefer tune \`prompts/\` between passes.
|
|
103
|
-
|
|
104
|
-
Try: edit \`prompts/executor.md\` and see the next pass obey your change.
|
|
105
|
-
`, "utf8");
|
|
106
|
-
|
|
107
|
-
fs.writeFileSync(path.join(loopDir, "prompts", "executor.md"), `# Executor prompt — demo loop (edit me and watch the next pass change)
|
|
108
|
-
|
|
109
|
-
ORIS LOOP ACTIVE — approved demo pass.
|
|
110
|
-
You are the executor of the Oris demo loop.
|
|
111
|
-
|
|
112
|
-
THIS PASS: iteration {{iteration}}.
|
|
113
|
-
DO: read ${workspaceRelative}/counter.txt, add 1, write the new value back.
|
|
114
|
-
ONLY touch files under ${workspaceRelative}.
|
|
115
|
-
|
|
116
|
-
RETURN compact JSON only (\`\`\`json fenced):
|
|
117
|
-
{ "role": "executor", "state": "claimed|blocked", "summary": "", "changedFiles": [], "claim": "counter is now N" }
|
|
118
|
-
`, "utf8");
|
|
119
|
-
|
|
120
|
-
fs.writeFileSync(path.join(loopDir, "prompts", "verifier.md"), `# Verifier prompt — demo loop
|
|
121
|
-
|
|
122
|
-
You are the independent verifier of the Oris demo loop.
|
|
123
|
-
NEVER trust the executor claim without reading the file yourself.
|
|
124
|
-
|
|
125
|
-
VERIFY: ${workspaceRelative}/counter.txt contains exactly the claimed number.
|
|
126
|
-
goalMet = true ONLY when the verified counter equals 3 (the loop goal).
|
|
127
|
-
|
|
128
|
-
RETURN compact JSON only (\`\`\`json fenced):
|
|
129
|
-
{ "role": "verifier", "state": "pass|fail|blocked|inconclusive", "goalMet": false, "summary": "", "evidence": "file content: N" }
|
|
130
|
-
`, "utf8");
|
|
131
|
-
|
|
132
|
-
fs.writeFileSync(path.join(loopDir, "prompts", "debriefer.md"), `# Debriefer prompt — demo loop (improve mode: auto)
|
|
133
|
-
|
|
134
|
-
You are the debriefer. After each pass, read the latest receipt.
|
|
135
|
-
IF a prompt caused friction: rewrite that file under prompts/ (copy the old one to history/ first) and note the change.
|
|
136
|
-
ONE small improvement max per pass. NEVER touch loop.md.
|
|
137
|
-
|
|
138
|
-
RETURN compact JSON only:
|
|
139
|
-
{ "role": "debriefer", "outcome": "keep|tuned", "insight": "", "changedFiles": [] }
|
|
140
|
-
`, "utf8");
|
|
141
|
-
|
|
142
|
-
fs.writeFileSync(docs.contextFull, `# Oris Loop Context
|
|
143
|
-
|
|
144
|
-
Run: inactive
|
|
145
|
-
State: ready
|
|
146
|
-
Current program: ${DEMO_SLUG}
|
|
147
|
-
Current phase: work
|
|
148
|
-
Current phase index: 0
|
|
149
|
-
Current iteration: 0
|
|
150
|
-
Phase iteration: 0
|
|
151
|
-
No-progress streak: 0
|
|
152
|
-
Updated: ${new Date().toISOString()}
|
|
153
|
-
|
|
154
|
-
## Objective
|
|
155
|
-
|
|
156
|
-
Learn the Oris loop system: counter 0 → 3, one increment per pass.
|
|
157
|
-
|
|
158
|
-
## Next action
|
|
159
|
-
|
|
160
|
-
Increment the counter from its current value.
|
|
161
|
-
`, "utf8");
|
|
162
|
-
|
|
163
|
-
console.log(`Demo loop created: ${docs.loopRelative}`);
|
|
164
|
-
console.log("");
|
|
165
|
-
console.log("Learn it in 3 steps:");
|
|
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}`);
|
|
168
|
-
console.log(" then end the turn — the stop hook schedules pass 1 automatically.");
|
|
169
|
-
console.log(` 3. Watch and tune: edit ${docs.promptsRelative}/executor.md between passes.`);
|
|
170
|
-
console.log("");
|
|
171
|
-
console.log(`Stop anytime: npx oris-skills loop chat --action stop`);
|