oris-skills 2.0.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 +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/agents/oris-loop-debriefer.md +29 -0
- package/agents/oris-loop-doctor.md +32 -0
- package/agents/oris-loop-executor.md +32 -0
- package/agents/oris-loop-verifier.md +31 -0
- package/docs/architecture.md +26 -0
- package/docs/distribution.md +38 -0
- package/docs/maintainer-guide.md +32 -0
- package/docs/user-guide.md +42 -0
- package/package.json +49 -0
- package/references/clean-code-checklist.md +107 -0
- package/references/conventions.md +39 -0
- package/references/doc-policy.md +24 -0
- package/references/loop-adapter.schema.json +126 -0
- package/references/loop-contract.md +119 -0
- package/references/loop.schema.json +143 -0
- package/references/questions.md +38 -0
- package/references/repo-map.md +53 -0
- package/references/repo-map.schema.json +198 -0
- package/references/settings.md +35 -0
- package/references/settings.schema.json +75 -0
- package/scripts/flow/oris-flow-clean-runtime.mjs +182 -0
- package/scripts/flow/oris-flow-layout.mjs +53 -0
- package/scripts/flow/oris-flow-scan.mjs +350 -0
- package/scripts/flow/oris-flow-version-control.mjs +42 -0
- package/scripts/flow/oris-gitignore.mjs +79 -0
- package/scripts/install/generate-agent-adapters.mjs +74 -0
- package/scripts/install/install-user-skills.mjs +271 -0
- package/scripts/install/uninstall-user-skills.mjs +163 -0
- package/scripts/loop/oris-loop-bootstrap.mjs +383 -0
- package/scripts/loop/oris-loop-bundle.mjs +22 -0
- package/scripts/loop/oris-loop-chat.mjs +408 -0
- package/scripts/loop/oris-loop-demo.mjs +180 -0
- package/scripts/loop/oris-loop-document.mjs +179 -0
- package/scripts/loop/oris-loop-dry-run.mjs +110 -0
- package/scripts/loop/oris-loop-fixtures.mjs +148 -0
- package/scripts/loop/oris-loop-list.mjs +81 -0
- package/scripts/loop/oris-loop-paths.mjs +232 -0
- package/scripts/loop/oris-loop-run.mjs +241 -0
- package/scripts/loop/oris-loop-stop.mjs +319 -0
- package/scripts/loop/oris-loop-templates.mjs +77 -0
- package/scripts/loop/oris-loop-verify.mjs +206 -0
- package/scripts/oris-skills.mjs +116 -0
- package/scripts/package-oris-skills.mjs +53 -0
- package/scripts/tests/run-all-tests.mjs +38 -0
- package/scripts/tests/test-agent-adapters.mjs +19 -0
- package/scripts/tests/test-oris-1-0-cleanliness.mjs +58 -0
- package/scripts/tests/test-oris-flow-clean-runtime.mjs +75 -0
- package/scripts/tests/test-oris-flow-scan.mjs +49 -0
- package/scripts/tests/test-oris-gitignore.mjs +35 -0
- package/scripts/tests/test-oris-loop-bootstrap.mjs +94 -0
- package/scripts/tests/test-oris-loop-document.mjs +112 -0
- package/scripts/tests/test-oris-loop-list.mjs +74 -0
- package/scripts/tests/test-oris-loop-run.mjs +45 -0
- package/scripts/tests/test-oris-loop-smoke.mjs +130 -0
- package/scripts/tests/test-oris-loop-stop.mjs +371 -0
- package/scripts/tests/test-routing-lifecycle.mjs +181 -0
- package/scripts/tests/test-schemas.mjs +55 -0
- package/skills/oris-flow/SKILL.md +55 -0
- package/skills/oris-flow/agents/openai.yaml +4 -0
- package/skills/oris-flow-criteria/SKILL.md +49 -0
- package/skills/oris-flow-discover/SKILL.md +58 -0
- package/skills/oris-flow-docs/SKILL.md +31 -0
- package/skills/oris-flow-fix/SKILL.md +42 -0
- package/skills/oris-flow-implement/SKILL.md +43 -0
- package/skills/oris-flow-plan/SKILL.md +51 -0
- package/skills/oris-flow-setup/SKILL.md +49 -0
- package/skills/oris-help/SKILL.md +30 -0
- package/skills/oris-help/agents/openai.yaml +4 -0
- package/skills/oris-loop/SKILL.md +66 -0
- package/skills/oris-loop/agents/openai.yaml +4 -0
- package/skills/oris-loop/references/craft.md +48 -0
- package/skills/oris-loop/references/improve.md +23 -0
- package/skills/oris-loop/references/run.md +30 -0
- package/skills/oris-loop/templates/debriefer.md +18 -0
- package/skills/oris-loop/templates/doctor.md +20 -0
- package/skills/oris-loop/templates/executor.md +19 -0
- package/skills/oris-loop/templates/orchestrator.md +33 -0
- package/skills/oris-loop/templates/verifier.md +21 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ORIS_LOOP_DOCUMENT_VERSION } from "../flow/oris-flow-layout.mjs";
|
|
4
|
+
|
|
5
|
+
const slugPattern = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
|
|
6
|
+
const phasePattern = /^[a-z][a-z0-9-]*$/;
|
|
7
|
+
|
|
8
|
+
export const INHERIT_MODEL = "inherit";
|
|
9
|
+
export const ROLE_NAMES = ["executor", "verifier", "doctor", "debriefer"];
|
|
10
|
+
export const REQUIRED_PROMPT_FILES = ["executor.md", "verifier.md", "doctor.md"];
|
|
11
|
+
export const OPTIONAL_PROMPT_FILES = ["orchestrator.md", "debriefer.md"];
|
|
12
|
+
export const IMPROVE_MODES = new Set(["propose", "auto"]);
|
|
13
|
+
|
|
14
|
+
export function parseFrontMatter(text, filePath = "loop.md") {
|
|
15
|
+
const normalized = String(text ?? "").replace(/^\uFEFF/, "");
|
|
16
|
+
if (!normalized.startsWith("---\n") && !normalized.startsWith("---\r\n")) {
|
|
17
|
+
throw new Error(`${filePath} must start with front matter.`);
|
|
18
|
+
}
|
|
19
|
+
const match = normalized.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
20
|
+
if (!match) throw new Error(`${filePath} front matter is not closed.`);
|
|
21
|
+
return {
|
|
22
|
+
data: parseSimpleYaml(match[1], filePath),
|
|
23
|
+
body: normalized.slice(match[0].length),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseSimpleYaml(source, filePath) {
|
|
28
|
+
const root = {};
|
|
29
|
+
const stack = [{ indent: -1, value: root }];
|
|
30
|
+
const lines = source.split(/\r?\n/);
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
33
|
+
const raw = lines[index];
|
|
34
|
+
if (!raw.trim() || raw.trimStart().startsWith("#")) continue;
|
|
35
|
+
const indent = raw.match(/^ */)?.[0].length ?? 0;
|
|
36
|
+
const trimmed = raw.trim();
|
|
37
|
+
const match = trimmed.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
|
|
38
|
+
if (!match) throw new Error(`${filePath} front matter line ${index + 1} is not supported: ${raw}`);
|
|
39
|
+
|
|
40
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
|
|
41
|
+
const parent = stack[stack.length - 1].value;
|
|
42
|
+
const key = match[1];
|
|
43
|
+
const valueText = match[2] ?? "";
|
|
44
|
+
if (valueText === "") {
|
|
45
|
+
const value = {};
|
|
46
|
+
parent[key] = value;
|
|
47
|
+
stack.push({ indent, value });
|
|
48
|
+
} else {
|
|
49
|
+
parent[key] = parseScalar(valueText);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return root;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseScalar(valueText) {
|
|
57
|
+
const value = valueText.trim();
|
|
58
|
+
if (value === "true") return true;
|
|
59
|
+
if (value === "false") return false;
|
|
60
|
+
if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
|
|
61
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
62
|
+
const inner = value.slice(1, -1).trim();
|
|
63
|
+
if (!inner) return [];
|
|
64
|
+
return inner.split(",").map((item) => stripQuotes(item.trim())).filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
return stripQuotes(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stripQuotes(value) {
|
|
70
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
71
|
+
return value.slice(1, -1);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function readLoopDocument(filePath) {
|
|
77
|
+
return parseFrontMatter(fs.readFileSync(filePath, "utf8"), filePath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function validateLoopDocumentText(text, filePath = "loop.md") {
|
|
81
|
+
const parsed = parseFrontMatter(text, filePath);
|
|
82
|
+
validateLoopMetadata(parsed.data, filePath);
|
|
83
|
+
if (!parsed.body.trim()) {
|
|
84
|
+
throw new Error(`${filePath} body must describe the loop goal and stop condition.`);
|
|
85
|
+
}
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function validateLoopMetadata(data, filePath = "loop.md") {
|
|
90
|
+
const errors = [];
|
|
91
|
+
if (data.schemaVersion !== ORIS_LOOP_DOCUMENT_VERSION) {
|
|
92
|
+
errors.push(`schemaVersion must be ${ORIS_LOOP_DOCUMENT_VERSION}`);
|
|
93
|
+
}
|
|
94
|
+
if (data.kind !== "oris-loop") errors.push("kind must be oris-loop");
|
|
95
|
+
if (typeof data.name !== "string" || !slugPattern.test(data.name)) errors.push("name must be a stable slug");
|
|
96
|
+
if (data.approved !== true) errors.push("approved must be true (the user approves the loop before it can run)");
|
|
97
|
+
if (!Array.isArray(data.phases) || data.phases.length < 1 || data.phases.some((phase) => typeof phase !== "string" || !phasePattern.test(phase))) {
|
|
98
|
+
errors.push("phases must contain valid phase slugs");
|
|
99
|
+
}
|
|
100
|
+
if (!data.limits || !Number.isInteger(data.limits.maxIterations) || data.limits.maxIterations < 1 || data.limits.maxIterations > 500) {
|
|
101
|
+
errors.push("limits.maxIterations must be 1..500");
|
|
102
|
+
}
|
|
103
|
+
if (!data.limits || !Number.isInteger(data.limits.maxNoProgress) || data.limits.maxNoProgress < 1 || data.limits.maxNoProgress > 50) {
|
|
104
|
+
errors.push("limits.maxNoProgress must be 1..50");
|
|
105
|
+
}
|
|
106
|
+
if (data.models !== undefined) {
|
|
107
|
+
if (!data.models || typeof data.models !== "object" || Array.isArray(data.models)) {
|
|
108
|
+
errors.push("models must be an object when present");
|
|
109
|
+
} else {
|
|
110
|
+
for (const key of ["default", ...ROLE_NAMES]) {
|
|
111
|
+
const value = data.models[key];
|
|
112
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim())) {
|
|
113
|
+
errors.push(`models.${key} must be a non-empty string (model id, adapter alias, or "inherit")`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (data.improve !== undefined) {
|
|
119
|
+
if (!data.improve || typeof data.improve !== "object" || Array.isArray(data.improve)) {
|
|
120
|
+
errors.push("improve must be an object when present");
|
|
121
|
+
} else if (data.improve.mode !== undefined && !IMPROVE_MODES.has(data.improve.mode)) {
|
|
122
|
+
errors.push('improve.mode must be "propose" or "auto"');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!data.permissions || !Array.isArray(data.permissions.allowedPaths)) errors.push("permissions.allowedPaths must be an array");
|
|
126
|
+
if (!data.permissions || !Array.isArray(data.permissions.forbiddenActions) || data.permissions.forbiddenActions.length === 0) {
|
|
127
|
+
errors.push("permissions.forbiddenActions must be a non-empty array");
|
|
128
|
+
}
|
|
129
|
+
if (!data.verification || !Array.isArray(data.verification.commands)) errors.push("verification.commands must be an array");
|
|
130
|
+
if (!data.verification || typeof data.verification.evidenceRequired !== "boolean") {
|
|
131
|
+
errors.push("verification.evidenceRequired must be boolean");
|
|
132
|
+
}
|
|
133
|
+
if (errors.length > 0) throw new Error(`${filePath} is not a valid Oris Loop v${ORIS_LOOP_DOCUMENT_VERSION} document: ${errors.join("; ")}.`);
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve per-role models. `inherit` means "use the model of the current
|
|
139
|
+
* session"; any other value is an adapter alias or a platform model id.
|
|
140
|
+
*/
|
|
141
|
+
export function normalizeModels(models = {}, adapterModels = {}) {
|
|
142
|
+
const fallback = resolveModelAlias(models.default ?? INHERIT_MODEL, adapterModels);
|
|
143
|
+
const resolved = { default: fallback };
|
|
144
|
+
for (const role of ROLE_NAMES) {
|
|
145
|
+
resolved[role] = models[role] !== undefined
|
|
146
|
+
? resolveModelAlias(models[role], adapterModels)
|
|
147
|
+
: fallback;
|
|
148
|
+
}
|
|
149
|
+
return resolved;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveModelAlias(value, adapterModels) {
|
|
153
|
+
const name = String(value ?? INHERIT_MODEL).trim() || INHERIT_MODEL;
|
|
154
|
+
if (name === INHERIT_MODEL) return INHERIT_MODEL;
|
|
155
|
+
if (adapterModels && typeof adapterModels === "object" && typeof adapterModels[name] === "string") {
|
|
156
|
+
return adapterModels[name];
|
|
157
|
+
}
|
|
158
|
+
return name;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function improveMode(data) {
|
|
162
|
+
return data?.improve?.mode === "auto" ? "auto" : "propose";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Check the per-loop prompts/ directory. Returns { promptsDir, missing }. */
|
|
166
|
+
export function assessPromptFiles(loopDir) {
|
|
167
|
+
const promptsDir = path.join(loopDir, "prompts");
|
|
168
|
+
const missing = [];
|
|
169
|
+
for (const file of REQUIRED_PROMPT_FILES) {
|
|
170
|
+
const full = path.join(promptsDir, file);
|
|
171
|
+
if (!fs.existsSync(full) || !fs.readFileSync(full, "utf8").trim()) missing.push(`prompts/${file}`);
|
|
172
|
+
}
|
|
173
|
+
return { promptsDir, missing };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function loopPhasesFromMetadata(data) {
|
|
177
|
+
validateLoopMetadata(data);
|
|
178
|
+
return [...data.phases];
|
|
179
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `oris-skills loop dry-run --loop <slug>` — show exactly what the next pass
|
|
5
|
+
* would inject into the chat, without arming the runtime or changing state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import childProcess from "node:child_process";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
import { readAdapter, resolveLoopDocumentPathsBySlug } from "./oris-loop-paths.mjs";
|
|
13
|
+
import {
|
|
14
|
+
assessPromptFiles,
|
|
15
|
+
improveMode,
|
|
16
|
+
normalizeModels,
|
|
17
|
+
validateLoopDocumentText,
|
|
18
|
+
} from "./oris-loop-document.mjs";
|
|
19
|
+
import { readTemplate, stripTemplateHeading, substitutePlaceholders } from "./oris-loop-templates.mjs";
|
|
20
|
+
|
|
21
|
+
function fail(message) {
|
|
22
|
+
console.error(`[oris-skills] ERROR ${message}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const options = { repositoryRoot: "", loopSlug: "" };
|
|
28
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
29
|
+
const raw = argv[i];
|
|
30
|
+
if (raw.startsWith("--loop=")) options.loopSlug = raw.slice("--loop=".length);
|
|
31
|
+
else if (raw === "--loop" && argv[i + 1]) {
|
|
32
|
+
options.loopSlug = argv[i + 1];
|
|
33
|
+
i += 1;
|
|
34
|
+
} else if (raw.startsWith("--repository-root=")) options.repositoryRoot = raw.slice("--repository-root=".length);
|
|
35
|
+
else if (raw === "--repository-root" && argv[i + 1]) {
|
|
36
|
+
options.repositoryRoot = argv[i + 1];
|
|
37
|
+
i += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return options;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getRoot(options) {
|
|
44
|
+
if (options.repositoryRoot) return path.resolve(options.repositoryRoot);
|
|
45
|
+
const result = childProcess.spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
shell: process.platform === "win32",
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
});
|
|
50
|
+
if (result.status !== 0 || !result.stdout.trim()) fail("Run inside a Git worktree or pass --repository-root.");
|
|
51
|
+
return path.resolve(result.stdout.trim());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const options = parseArgs(process.argv.slice(2));
|
|
55
|
+
if (!options.loopSlug) fail("--loop <slug> is required. List loops with: oris-skills loop list");
|
|
56
|
+
const root = getRoot(options);
|
|
57
|
+
const adapter = readAdapter(root);
|
|
58
|
+
const docs = resolveLoopDocumentPathsBySlug(root, options.loopSlug, adapter);
|
|
59
|
+
if (!fs.existsSync(docs.loopFull)) fail(`Loop not found: ${docs.loopRelative}`);
|
|
60
|
+
|
|
61
|
+
let loopDocument;
|
|
62
|
+
try {
|
|
63
|
+
loopDocument = validateLoopDocumentText(fs.readFileSync(docs.loopFull, "utf8"), docs.loopRelative);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
fail(error.message);
|
|
66
|
+
}
|
|
67
|
+
const metadata = loopDocument.data;
|
|
68
|
+
const prompts = assessPromptFiles(path.dirname(docs.loopFull));
|
|
69
|
+
const models = normalizeModels(metadata.models, adapter.models);
|
|
70
|
+
|
|
71
|
+
const perLoopOrchestrator = path.join(docs.promptsFull, "orchestrator.md");
|
|
72
|
+
const template = fs.existsSync(perLoopOrchestrator)
|
|
73
|
+
? stripTemplateHeading(fs.readFileSync(perLoopOrchestrator, "utf8"))
|
|
74
|
+
: readTemplate("orchestrator", import.meta.url);
|
|
75
|
+
|
|
76
|
+
const followup = substitutePlaceholders(template, {
|
|
77
|
+
iteration: 1,
|
|
78
|
+
maxIterations: metadata.limits.maxIterations,
|
|
79
|
+
loop: metadata.name,
|
|
80
|
+
phase: metadata.phases[0],
|
|
81
|
+
phaseIndex: 1,
|
|
82
|
+
phaseCount: metadata.phases.length,
|
|
83
|
+
instruction: "continue the current phase",
|
|
84
|
+
loopPath: docs.loopRelative,
|
|
85
|
+
contextPath: docs.contextRelative,
|
|
86
|
+
promptsDir: docs.promptsRelative,
|
|
87
|
+
executorModel: models.executor,
|
|
88
|
+
verifierModel: models.verifier,
|
|
89
|
+
doctorModel: models.doctor,
|
|
90
|
+
debrieferModel: models.debriefer,
|
|
91
|
+
improveMode: improveMode(metadata),
|
|
92
|
+
taskPath: docs.taskPath,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log(`Oris loop dry run: ${metadata.name}`);
|
|
96
|
+
console.log(`Definition: ${docs.loopRelative} (schemaVersion ${metadata.schemaVersion}, approved: ${metadata.approved})`);
|
|
97
|
+
console.log(`Prompts: ${docs.promptsRelative} ${prompts.missing.length === 0 ? "(complete)" : `(MISSING: ${prompts.missing.join(", ")})`}`);
|
|
98
|
+
console.log(`Models: executor=${models.executor} verifier=${models.verifier} doctor=${models.doctor} debriefer=${models.debriefer}`);
|
|
99
|
+
console.log(`Improve mode: ${improveMode(metadata)}`);
|
|
100
|
+
console.log(`Limits: ${metadata.limits.maxIterations} iterations, ${metadata.limits.maxNoProgress} no-progress, ${metadata.limits.maxMinutes ?? 240} minutes`);
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log("=== Message the stop hook would inject for pass 1 ===");
|
|
103
|
+
console.log(followup);
|
|
104
|
+
console.log("=== End of injected message ===");
|
|
105
|
+
console.log("");
|
|
106
|
+
if (prompts.missing.length > 0) {
|
|
107
|
+
console.log("NOT runnable yet — create the missing prompt files first (templates: skills/oris-loop/templates/).");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
console.log(`Runnable. Start it with: oris-skills loop chat --action start --loop ${metadata.name}`);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
bootstrapLoop,
|
|
6
|
+
buildAdapterDraft,
|
|
7
|
+
writeHooksRegistration,
|
|
8
|
+
writeStopHookWrapper,
|
|
9
|
+
} from "./oris-loop-bootstrap.mjs";
|
|
10
|
+
import { defaultLoopDocumentPaths, readAdapter } from "./oris-loop-paths.mjs";
|
|
11
|
+
import { ORIS_FLOW_ADAPTER } from "../flow/oris-flow-layout.mjs";
|
|
12
|
+
|
|
13
|
+
export function writeMinimalAdapter(root) {
|
|
14
|
+
const adapterPath = path.join(root, ORIS_FLOW_ADAPTER);
|
|
15
|
+
fs.mkdirSync(path.dirname(adapterPath), { recursive: true });
|
|
16
|
+
const adapter = buildAdapterDraft(root);
|
|
17
|
+
adapter.preflight.minFreeDiskGiB = 0;
|
|
18
|
+
fs.writeFileSync(adapterPath, `${JSON.stringify(adapter, null, 2)}\n`, "utf8");
|
|
19
|
+
return adapterPath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loopDocumentSource({
|
|
23
|
+
program = "oris-loop-smoke",
|
|
24
|
+
phases = ["work"],
|
|
25
|
+
improve = "propose",
|
|
26
|
+
models = {},
|
|
27
|
+
goal = "Run one deterministic smoke action, verify it, and record evidence.",
|
|
28
|
+
stop = "Stop when the smoke check passes once, or when blocked.",
|
|
29
|
+
} = {}) {
|
|
30
|
+
const modelLines = ["models:", ` default: ${models.default ?? "inherit"}`];
|
|
31
|
+
for (const role of ["executor", "verifier", "doctor", "debriefer"]) {
|
|
32
|
+
if (models[role]) modelLines.push(` ${role}: ${models[role]}`);
|
|
33
|
+
}
|
|
34
|
+
return `---
|
|
35
|
+
schemaVersion: 2
|
|
36
|
+
kind: oris-loop
|
|
37
|
+
name: ${program}
|
|
38
|
+
approved: true
|
|
39
|
+
phases: [${phases.join(", ")}]
|
|
40
|
+
limits:
|
|
41
|
+
maxIterations: 25
|
|
42
|
+
maxNoProgress: 2
|
|
43
|
+
maxMinutes: 240
|
|
44
|
+
${modelLines.join("\n")}
|
|
45
|
+
improve:
|
|
46
|
+
mode: ${improve}
|
|
47
|
+
permissions:
|
|
48
|
+
allowedPaths: [.oris-flow/loops, .oris-flow/runtime]
|
|
49
|
+
forbiddenActions: [commit, push, deploy, external-message]
|
|
50
|
+
verification:
|
|
51
|
+
commands: [node --version]
|
|
52
|
+
evidenceRequired: true
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
# ${program}
|
|
56
|
+
|
|
57
|
+
Goal: ${goal}
|
|
58
|
+
Stop: ${stop}
|
|
59
|
+
|
|
60
|
+
Role prompts live in \`prompts/\`. Edit them there; the loop picks changes up on the next pass.
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function rolePromptFixtures() {
|
|
65
|
+
return {
|
|
66
|
+
"executor.md": `# Executor prompt
|
|
67
|
+
|
|
68
|
+
Perform exactly one bounded smoke action inside allowed paths.
|
|
69
|
+
Runtime: iteration {{iteration}}, phase {{phase}}, target {{currentTarget}}.
|
|
70
|
+
RETURN compact JSON: { "role": "executor", "state": "claimed|blocked", "summary": "", "changedFiles": [], "claim": "" }
|
|
71
|
+
`,
|
|
72
|
+
"verifier.md": `# Verifier prompt
|
|
73
|
+
|
|
74
|
+
Verify independently: \`node --version\` exits 0. NEVER trust the executor without running the check.
|
|
75
|
+
Runtime: iteration {{iteration}}; executor claim supplied by orchestrator.
|
|
76
|
+
RETURN compact JSON: { "role": "verifier", "state": "pass|fail|blocked", "summary": "", "evidence": "" }
|
|
77
|
+
`,
|
|
78
|
+
"doctor.md": `# Doctor prompt
|
|
79
|
+
|
|
80
|
+
Diagnose from evidence. Stop rules: success when the smoke check passes; blocked on missing tools.
|
|
81
|
+
RETURN compact JSON: { "role": "doctor", "decision": "continue|complete|stop", "diagnosis": "" }
|
|
82
|
+
`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function writeLoopTask(root, {
|
|
87
|
+
taskSlug = "oris-loop-smoke",
|
|
88
|
+
program = "oris-loop-smoke",
|
|
89
|
+
phases = ["work"],
|
|
90
|
+
phase = phases[0],
|
|
91
|
+
improve = "propose",
|
|
92
|
+
models = {},
|
|
93
|
+
} = {}) {
|
|
94
|
+
const taskPath = taskSlug;
|
|
95
|
+
const adapter = readAdapter(root);
|
|
96
|
+
const loopDocs = defaultLoopDocumentPaths(root, taskPath, adapter);
|
|
97
|
+
const loopPath = loopDocs.loopFull;
|
|
98
|
+
const contextPath = loopDocs.contextFull;
|
|
99
|
+
fs.mkdirSync(path.dirname(loopPath), { recursive: true });
|
|
100
|
+
fs.writeFileSync(loopPath, loopDocumentSource({ program, phases, improve, models }), "utf8");
|
|
101
|
+
fs.mkdirSync(loopDocs.promptsFull, { recursive: true });
|
|
102
|
+
for (const [name, content] of Object.entries(rolePromptFixtures())) {
|
|
103
|
+
fs.writeFileSync(path.join(loopDocs.promptsFull, name), content, "utf8");
|
|
104
|
+
}
|
|
105
|
+
fs.writeFileSync(contextPath, `# Oris Loop Context
|
|
106
|
+
|
|
107
|
+
Run: inactive
|
|
108
|
+
State: ready
|
|
109
|
+
Current program: ${program}
|
|
110
|
+
Current phase: ${phase}
|
|
111
|
+
Current phase index: 0
|
|
112
|
+
Current iteration: 0
|
|
113
|
+
Phase iteration: 0
|
|
114
|
+
Repair cycles: 0
|
|
115
|
+
Updated: ${new Date().toISOString()}
|
|
116
|
+
|
|
117
|
+
## Objective
|
|
118
|
+
|
|
119
|
+
Verify the Oris Loop same-chat stop hook.
|
|
120
|
+
|
|
121
|
+
## Next action
|
|
122
|
+
|
|
123
|
+
Run one smoke pass.
|
|
124
|
+
`, "utf8");
|
|
125
|
+
return {
|
|
126
|
+
taskPath,
|
|
127
|
+
taskRoot: path.join(root, ".oris-flow", "loops", taskSlug),
|
|
128
|
+
loopPath,
|
|
129
|
+
contextPath,
|
|
130
|
+
promptsPath: loopDocs.promptsFull,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function writeStopHookRegistration(root) {
|
|
135
|
+
return writeHooksRegistration(root, "cursor");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { writeStopHookWrapper };
|
|
139
|
+
|
|
140
|
+
export function initSmokeRepository(root, options = {}) {
|
|
141
|
+
fs.mkdirSync(root, { recursive: true });
|
|
142
|
+
const git = spawnSync("git", ["init", "-q"], { cwd: root, encoding: "utf8" });
|
|
143
|
+
if (git.status !== 0) {
|
|
144
|
+
throw new Error(`git init failed: ${git.stderr}`);
|
|
145
|
+
}
|
|
146
|
+
bootstrapLoop(root);
|
|
147
|
+
return writeLoopTask(root, options);
|
|
148
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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 { discoverExistingLoops, readAdapter } from "./oris-loop-paths.mjs";
|
|
8
|
+
import { ORIS_FLOW_ADAPTER, ORIS_FLOW_RUNTIME } from "../flow/oris-flow-layout.mjs";
|
|
9
|
+
|
|
10
|
+
function fail(message) {
|
|
11
|
+
throw new Error(message);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const options = {
|
|
16
|
+
repositoryRoot: "",
|
|
17
|
+
json: false,
|
|
18
|
+
};
|
|
19
|
+
const readValue = (index, raw) => {
|
|
20
|
+
if (raw.includes("=")) return [index, raw.slice(raw.indexOf("=") + 1)];
|
|
21
|
+
if (index + 1 >= argv.length) fail(`Missing value for ${raw}.`);
|
|
22
|
+
return [index + 1, argv[index + 1]];
|
|
23
|
+
};
|
|
24
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
25
|
+
const raw = argv[i];
|
|
26
|
+
const key = raw.toLowerCase();
|
|
27
|
+
if (key === "--repository-root" || key.startsWith("--repository-root=")) [i, options.repositoryRoot] = readValue(i, raw);
|
|
28
|
+
else if (key === "--json") options.json = true;
|
|
29
|
+
else fail(`Unknown argument: ${raw}`);
|
|
30
|
+
}
|
|
31
|
+
return options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function run(command, args, cwd) {
|
|
35
|
+
return childProcess.spawnSync(command, args, {
|
|
36
|
+
cwd,
|
|
37
|
+
shell: process.platform === "win32",
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getRoot(options) {
|
|
43
|
+
if (options.repositoryRoot) return path.resolve(options.repositoryRoot);
|
|
44
|
+
const result = run("git", ["rev-parse", "--show-toplevel"], process.cwd());
|
|
45
|
+
if (result.status !== 0 || !result.stdout.trim()) fail("Run inside a Git worktree or pass --repository-root.");
|
|
46
|
+
return path.resolve(result.stdout.trim());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const options = parseArgs(process.argv.slice(2));
|
|
50
|
+
const root = getRoot(options);
|
|
51
|
+
const adapterPath = path.join(root, ORIS_FLOW_ADAPTER);
|
|
52
|
+
const adapter = fs.existsSync(adapterPath) ? readAdapter(root, adapterPath) : readAdapter(root);
|
|
53
|
+
const loops = discoverExistingLoops(root, adapter);
|
|
54
|
+
const payload = {
|
|
55
|
+
repositoryRoot: root.replace(/\\/g, "/"),
|
|
56
|
+
runtimeRelative: adapter?.paths?.runtime ?? ORIS_FLOW_RUNTIME,
|
|
57
|
+
count: loops.length,
|
|
58
|
+
loops,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (options.json) {
|
|
62
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (loops.length === 0) {
|
|
67
|
+
console.log("No existing Oris loops found.");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`Existing Oris loops (${loops.length}):`);
|
|
72
|
+
for (const loop of loops) {
|
|
73
|
+
console.log("");
|
|
74
|
+
console.log(`- slug: ${loop.slug}`);
|
|
75
|
+
console.log(` task: ${loop.taskPath}`);
|
|
76
|
+
console.log(` program: ${loop.program || "custom"}`);
|
|
77
|
+
console.log(` run/state: ${loop.run || "inactive"} / ${loop.state || "unknown"}`);
|
|
78
|
+
console.log(` loop: ${loop.loopRelative}`);
|
|
79
|
+
console.log(` context: ${loop.contextRelative}`);
|
|
80
|
+
if (loop.objective) console.log(` objective: ${loop.objective}`);
|
|
81
|
+
}
|