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,358 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Shared stop hook for same-chat Oris loops.
|
|
5
|
-
*
|
|
6
|
-
* Platforms:
|
|
7
|
-
* - cursor: reads Cursor stop-hook JSON on stdin, answers { followup_message }.
|
|
8
|
-
* - claude: reads Claude Code Stop-hook JSON on stdin, answers
|
|
9
|
-
* { decision: "block", reason } to continue the same session.
|
|
10
|
-
*
|
|
11
|
-
* The platform comes from --platform, else it is detected from input keys.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import fs from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import process from "node:process";
|
|
17
|
-
import { ORIS_FLOW_RUNTIME } from "../flow/oris-flow-layout.mjs";
|
|
18
|
-
import { readTemplate, stripTemplateHeading, substitutePlaceholders } from "./oris-loop-templates.mjs";
|
|
19
|
-
|
|
20
|
-
const terminalStates = new Set(["complete", "blocked", "cancelled"]);
|
|
21
|
-
const resumableStates = new Set(["continue", "advance", "running"]);
|
|
22
|
-
|
|
23
|
-
function emit(value) {
|
|
24
|
-
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Windows may pipe hook stdin with a UTF-8 BOM; JSON.parse rejects it. */
|
|
28
|
-
function stripUtf8Bom(text) {
|
|
29
|
-
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseJsonText(text) {
|
|
33
|
-
const trimmed = stripUtf8Bom(String(text ?? "").trim());
|
|
34
|
-
return trimmed ? JSON.parse(trimmed) : {};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function readInput() {
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
let raw = "";
|
|
40
|
-
process.stdin.setEncoding("utf8");
|
|
41
|
-
process.stdin.on("data", (chunk) => {
|
|
42
|
-
raw += chunk;
|
|
43
|
-
});
|
|
44
|
-
process.stdin.on("end", () => {
|
|
45
|
-
try {
|
|
46
|
-
resolve(parseJsonText(raw));
|
|
47
|
-
} catch (error) {
|
|
48
|
-
reject(error);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
process.stdin.on("error", reject);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function writeStateAtomic(statePath, state) {
|
|
56
|
-
const directory = path.dirname(statePath);
|
|
57
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
58
|
-
const temporary = `${statePath}.${process.pid}.tmp`;
|
|
59
|
-
fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, {
|
|
60
|
-
encoding: "utf8",
|
|
61
|
-
mode: 0o600,
|
|
62
|
-
});
|
|
63
|
-
fs.renameSync(temporary, statePath);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function stopState(statePath, state, reason) {
|
|
67
|
-
writeStateAtomic(statePath, {
|
|
68
|
-
...state,
|
|
69
|
-
active: false,
|
|
70
|
-
state: "blocked",
|
|
71
|
-
stopReason: reason,
|
|
72
|
-
updatedAt: new Date().toISOString(),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function normalizeState(state) {
|
|
77
|
-
if (!state || typeof state !== "object") return null;
|
|
78
|
-
const normalized = { ...state };
|
|
79
|
-
const version = Number(normalized.schemaVersion ?? 0);
|
|
80
|
-
if (version !== 1) return null;
|
|
81
|
-
return normalized;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function detectPlatform(argv, raw) {
|
|
85
|
-
const explicit = argv.find((arg) => arg.startsWith("--platform="))?.slice("--platform=".length)
|
|
86
|
-
?? (argv.includes("--platform") ? argv[argv.indexOf("--platform") + 1] : "");
|
|
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";
|
|
90
|
-
if (String(raw.hook_event_name ?? "") === "Stop" || raw.session_id !== undefined || raw.stop_hook_active !== undefined) {
|
|
91
|
-
return "claude";
|
|
92
|
-
}
|
|
93
|
-
return "cursor";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Normalize Cursor and Claude hook payloads to one shape. */
|
|
97
|
-
function readHookInput(raw, platform) {
|
|
98
|
-
if (platform === "claude") {
|
|
99
|
-
return {
|
|
100
|
-
status: "completed",
|
|
101
|
-
conversationId: raw.session_id ? String(raw.session_id) : "",
|
|
102
|
-
loopCount: 0,
|
|
103
|
-
workspaceRoots: raw.cwd ? [String(raw.cwd)] : [],
|
|
104
|
-
hookEventName: String(raw.hook_event_name ?? ""),
|
|
105
|
-
rawKeys: Object.keys(raw).sort(),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const status = String(raw.status ?? raw.agent_status ?? "").toLowerCase();
|
|
109
|
-
const conversationId = raw.conversation_id ?? raw.conversationId ?? null;
|
|
110
|
-
const loopCount = Number(raw.loop_count ?? raw.loopCount ?? 0);
|
|
111
|
-
const workspaceRoots = Array.isArray(raw.workspace_roots)
|
|
112
|
-
? raw.workspace_roots
|
|
113
|
-
: Array.isArray(raw.workspaceRoots)
|
|
114
|
-
? raw.workspaceRoots
|
|
115
|
-
: [];
|
|
116
|
-
return {
|
|
117
|
-
status,
|
|
118
|
-
conversationId: conversationId ? String(conversationId) : "",
|
|
119
|
-
loopCount: Number.isFinite(loopCount) ? loopCount : 0,
|
|
120
|
-
workspaceRoots,
|
|
121
|
-
hookEventName: String(raw.hook_event_name ?? raw.hookEventName ?? ""),
|
|
122
|
-
rawKeys: Object.keys(raw).sort(),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function resolveStatePath(input, workspaceRoots) {
|
|
127
|
-
if (process.env.ORIS_LOOP_CHAT_STATE) {
|
|
128
|
-
return path.resolve(process.env.ORIS_LOOP_CHAT_STATE);
|
|
129
|
-
}
|
|
130
|
-
for (const root of workspaceRoots) {
|
|
131
|
-
if (typeof root !== "string" || root.trim().length === 0) continue;
|
|
132
|
-
const candidate = path.join(root, ORIS_FLOW_RUNTIME, "chat-active.json");
|
|
133
|
-
if (fs.existsSync(candidate)) return path.resolve(candidate);
|
|
134
|
-
}
|
|
135
|
-
return path.resolve(process.cwd(), ORIS_FLOW_RUNTIME, "chat-active.json");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function resolveDebugPath(statePath) {
|
|
139
|
-
return path.join(path.dirname(statePath), "hook-debug.jsonl");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function appendDebug(debugPath, entry) {
|
|
143
|
-
try {
|
|
144
|
-
fs.mkdirSync(path.dirname(debugPath), { recursive: true });
|
|
145
|
-
fs.appendFileSync(debugPath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
146
|
-
} catch {
|
|
147
|
-
// Debug logging must never break the hook.
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
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 {};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function finish(debugPath, baseEntry, result) {
|
|
163
|
-
appendDebug(debugPath, {
|
|
164
|
-
...baseEntry,
|
|
165
|
-
at: new Date().toISOString(),
|
|
166
|
-
decision: result.decision,
|
|
167
|
-
reason: result.reason,
|
|
168
|
-
emittedFollowup: Boolean(result.followup),
|
|
169
|
-
});
|
|
170
|
-
emit(packageOutput(baseEntry.platform, result.followup, result.notice));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Resolve the orchestrator prompt: per-loop file > packaged template > embedded. */
|
|
174
|
-
function buildFollowup(state, repositoryRoot, values) {
|
|
175
|
-
let template = null;
|
|
176
|
-
if (state.promptsPath && repositoryRoot) {
|
|
177
|
-
const perLoop = path.join(repositoryRoot, state.promptsPath, "orchestrator.md");
|
|
178
|
-
if (fs.existsSync(perLoop)) {
|
|
179
|
-
template = stripTemplateHeading(fs.readFileSync(perLoop, "utf8"));
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (!template) template = readTemplate("orchestrator", import.meta.url);
|
|
183
|
-
return substitutePlaceholders(template, values);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const rawInput = await readInput();
|
|
188
|
-
const platform = detectPlatform(process.argv.slice(2), rawInput);
|
|
189
|
-
const input = readHookInput(rawInput, platform);
|
|
190
|
-
const statePath = resolveStatePath(rawInput, input.workspaceRoots);
|
|
191
|
-
const debugPath = resolveDebugPath(statePath);
|
|
192
|
-
const baseEntry = {
|
|
193
|
-
pid: process.pid,
|
|
194
|
-
cwd: process.cwd(),
|
|
195
|
-
platform,
|
|
196
|
-
statePath,
|
|
197
|
-
inputStatus: input.status,
|
|
198
|
-
conversationId: input.conversationId || null,
|
|
199
|
-
loopCount: input.loopCount,
|
|
200
|
-
hookEventName: input.hookEventName,
|
|
201
|
-
rawKeys: input.rawKeys,
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
if (input.status !== "completed") {
|
|
205
|
-
finish(debugPath, baseEntry, { decision: "noop", reason: `status-not-completed:${input.status || "missing"}` });
|
|
206
|
-
process.exit(0);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (!input.conversationId) {
|
|
210
|
-
finish(debugPath, baseEntry, { decision: "noop", reason: "missing-conversation-id" });
|
|
211
|
-
process.exit(0);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (!fs.existsSync(statePath)) {
|
|
215
|
-
finish(debugPath, baseEntry, { decision: "noop", reason: "chat-active-missing" });
|
|
216
|
-
process.exit(0);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
let state;
|
|
220
|
-
try {
|
|
221
|
-
state = normalizeState(parseJsonText(fs.readFileSync(statePath, "utf8")));
|
|
222
|
-
} catch {
|
|
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
|
-
});
|
|
228
|
-
process.exit(0);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
baseEntry.runtimeActive = state?.active ?? null;
|
|
232
|
-
baseEntry.runtimeState = state?.state ?? null;
|
|
233
|
-
baseEntry.runtimeIteration = state?.iteration ?? null;
|
|
234
|
-
|
|
235
|
-
if (!state) {
|
|
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
|
-
});
|
|
241
|
-
process.exit(0);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (state.active !== true || terminalStates.has(state.state) || !resumableStates.has(state.state)) {
|
|
245
|
-
finish(debugPath, baseEntry, { decision: "noop", reason: `runtime-not-resumable:${state.state}` });
|
|
246
|
-
process.exit(0);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
let boundConversation = false;
|
|
250
|
-
if (!state.conversationId) {
|
|
251
|
-
state.conversationId = input.conversationId;
|
|
252
|
-
boundConversation = true;
|
|
253
|
-
}
|
|
254
|
-
if (state.conversationId !== input.conversationId) {
|
|
255
|
-
finish(debugPath, baseEntry, { decision: "noop", reason: `conversation-mismatch:bound=${state.conversationId}` });
|
|
256
|
-
process.exit(0);
|
|
257
|
-
}
|
|
258
|
-
if (boundConversation) {
|
|
259
|
-
writeStateAtomic(statePath, { ...state, updatedAt: new Date().toISOString() });
|
|
260
|
-
baseEntry.boundConversation = true;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const iteration = Number(state.iteration ?? 0);
|
|
264
|
-
const maxIterations = Number(state.maxIterations ?? 25);
|
|
265
|
-
const maxMinutes = Number(state.maxMinutes ?? 240);
|
|
266
|
-
const startedAt = Date.parse(state.startedAt ?? "");
|
|
267
|
-
const elapsedMinutes = Number.isFinite(startedAt)
|
|
268
|
-
? (Date.now() - startedAt) / 60_000
|
|
269
|
-
: Number.POSITIVE_INFINITY;
|
|
270
|
-
|
|
271
|
-
if (!Number.isInteger(iteration) || !Number.isInteger(maxIterations) || maxIterations < 1) {
|
|
272
|
-
finish(debugPath, baseEntry, { decision: "noop", reason: "invalid-runtime-counters" });
|
|
273
|
-
process.exit(0);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (iteration >= maxIterations || input.loopCount >= maxIterations) {
|
|
277
|
-
stopState(statePath, state, "iteration limit exhausted");
|
|
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
|
-
});
|
|
283
|
-
process.exit(0);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (elapsedMinutes >= maxMinutes) {
|
|
287
|
-
stopState(statePath, state, "wall-clock limit exhausted");
|
|
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
|
-
});
|
|
304
|
-
process.exit(0);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const nextIteration = iteration + 1;
|
|
308
|
-
const phases = Array.isArray(state.phases) && state.phases.length > 0 ? state.phases : ["execute"];
|
|
309
|
-
const phaseIndex = Number.isInteger(Number(state.phaseIndex)) ? Number(state.phaseIndex) : 0;
|
|
310
|
-
const phaseName = state.phaseName ?? phases[Math.max(0, Math.min(phaseIndex, phases.length - 1))] ?? "execute";
|
|
311
|
-
const models = state.models && typeof state.models === "object" ? state.models : {};
|
|
312
|
-
const defaultModel = String(models.default ?? "inherit");
|
|
313
|
-
const repositoryRoot = state.repositoryRoot && fs.existsSync(state.repositoryRoot)
|
|
314
|
-
? state.repositoryRoot
|
|
315
|
-
: input.workspaceRoots.find((root) => typeof root === "string" && fs.existsSync(root)) ?? process.cwd();
|
|
316
|
-
|
|
317
|
-
const roles = Array.isArray(state.roles) && state.roles.length > 0
|
|
318
|
-
? state.roles
|
|
319
|
-
: ["executor", "verifier"];
|
|
320
|
-
|
|
321
|
-
const followup = buildFollowup(state, repositoryRoot, {
|
|
322
|
-
iteration: nextIteration,
|
|
323
|
-
maxIterations,
|
|
324
|
-
loop: state.program ?? "",
|
|
325
|
-
phase: phaseName,
|
|
326
|
-
phaseIndex: phaseIndex + 1,
|
|
327
|
-
phaseCount: phases.length,
|
|
328
|
-
instruction: state.state === "advance" ? "advance to the next declared phase" : "continue the current phase",
|
|
329
|
-
loopPath: state.loopPath ?? "",
|
|
330
|
-
contextPath: state.contextPath ?? "",
|
|
331
|
-
promptsDir: state.promptsPath ?? "",
|
|
332
|
-
roles: roles.join(", "),
|
|
333
|
-
executorModel: String(models.executor ?? defaultModel),
|
|
334
|
-
verifierModel: String(models.verifier ?? defaultModel),
|
|
335
|
-
doctorModel: String(models.doctor ?? defaultModel),
|
|
336
|
-
debrieferModel: String(models.debriefer ?? defaultModel),
|
|
337
|
-
improveMode: String(state.improveMode ?? "propose"),
|
|
338
|
-
taskPath: state.taskPath ?? "",
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
writeStateAtomic(statePath, {
|
|
342
|
-
...state,
|
|
343
|
-
state: "running",
|
|
344
|
-
iteration: nextIteration,
|
|
345
|
-
phaseIteration: Number(state.phaseIteration ?? 0) + 1,
|
|
346
|
-
updatedAt: new Date().toISOString(),
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
finish(debugPath, baseEntry, { decision: "followup", reason: "scheduled-next-iteration", followup });
|
|
350
|
-
} catch (error) {
|
|
351
|
-
const fallbackPath = path.resolve(process.cwd(), ORIS_FLOW_RUNTIME, "hook-debug.jsonl");
|
|
352
|
-
appendDebug(fallbackPath, {
|
|
353
|
-
at: new Date().toISOString(),
|
|
354
|
-
decision: "error",
|
|
355
|
-
reason: String(error?.message ?? error),
|
|
356
|
-
});
|
|
357
|
-
emit({});
|
|
358
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Prompt templates live in skills/oris-flow/templates/*.md so users and
|
|
7
|
-
* agents can read and copy them. Scripts fall back to an embedded
|
|
8
|
-
* orchestrator prompt so the stop hook can never fail on a missing file.
|
|
9
|
-
*/
|
|
10
|
-
export const TEMPLATE_NAMES = ["orchestrator", "executor", "verifier", "doctor", "debriefer"];
|
|
11
|
-
|
|
12
|
-
export const EMBEDDED_ORCHESTRATOR = `ORIS LOOP RUN — pass {{iteration}}/{{maxIterations}}
|
|
13
|
-
Loop: {{loop}} | Phase: {{phase}} ({{phaseIndex}}/{{phaseCount}}) | Do: {{instruction}}
|
|
14
|
-
Definition: {{loopPath}} | Context: {{contextPath}} | Prompts: {{promptsDir}}
|
|
15
|
-
Active roles: {{roles}}
|
|
16
|
-
Models: executor={{executorModel}} verifier={{verifierModel}} doctor={{doctorModel}} debriefer={{debrieferModel}} ("inherit" = current session model)
|
|
17
|
-
Improve mode: {{improveMode}}
|
|
18
|
-
|
|
19
|
-
ROLE: you are the orchestrator only. NEVER do executor, verifier, or doctor work in this chat.
|
|
20
|
-
|
|
21
|
-
EACH PASS:
|
|
22
|
-
1. READ {{contextPath}}; pick exactly ONE bounded next step.
|
|
23
|
-
2. SPAWN executor subagent with {{promptsDir}}/executor.md (fill runtime values), model {{executorModel}}.
|
|
24
|
-
3. SPAWN verifier subagent with {{promptsDir}}/verifier.md + the executor claim, model {{verifierModel}}.
|
|
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.
|
|
28
|
-
5. WRITE a compact receipt to the loop receipts/ folder; update context.md with durable facts only.
|
|
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>\`.
|
|
31
|
-
|
|
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/.
|
|
34
|
-
- propose → the debriefer writes proposals/ only; nothing changes until the user approves.
|
|
35
|
-
|
|
36
|
-
ALWAYS:
|
|
37
|
-
- Keep chat output compact; raw logs go to .oris-flow/runtime/evidence/.
|
|
38
|
-
- Tell the user in one line what this pass will do before spawning workers.
|
|
39
|
-
|
|
40
|
-
NEVER:
|
|
41
|
-
- Continue when the verdict is blocked, scope is violated, or the same failure repeats.
|
|
42
|
-
- Commit, push, deploy, edit .oris-flow/runtime/**, or touch paths outside the approved scope.
|
|
43
|
-
- Ask "run next pass?" after continue/advance — end the turn; the hook schedules the next pass.
|
|
44
|
-
|
|
45
|
-
IF prompt files are missing or subagents cannot be spawned: \`set-state --state blocked\`, say why, end the turn.`;
|
|
46
|
-
|
|
47
|
-
export function resolveTemplatesDir(fromImportMetaUrl) {
|
|
48
|
-
const scriptsLoop = path.dirname(fileURLToPath(fromImportMetaUrl));
|
|
49
|
-
const candidates = [
|
|
50
|
-
path.resolve(scriptsLoop, "..", "..", "skills", "oris-flow", "templates"),
|
|
51
|
-
path.resolve(scriptsLoop, "..", "skills", "oris-flow", "templates"),
|
|
52
|
-
];
|
|
53
|
-
for (const candidate of candidates) {
|
|
54
|
-
if (fs.existsSync(path.join(candidate, "orchestrator.md"))) return candidate;
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Strip the leading "# ..." comment line templates carry for humans. */
|
|
60
|
-
export function stripTemplateHeading(text) {
|
|
61
|
-
const lines = String(text ?? "").split(/\r?\n/);
|
|
62
|
-
while (lines.length > 0 && (lines[0].startsWith("# ") || !lines[0].trim())) lines.shift();
|
|
63
|
-
return lines.join("\n").trim();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function readTemplate(name, fromImportMetaUrl) {
|
|
67
|
-
const dir = resolveTemplatesDir(fromImportMetaUrl);
|
|
68
|
-
if (dir) {
|
|
69
|
-
const full = path.join(dir, `${name}.md`);
|
|
70
|
-
if (fs.existsSync(full)) return stripTemplateHeading(fs.readFileSync(full, "utf8"));
|
|
71
|
-
}
|
|
72
|
-
return name === "orchestrator" ? EMBEDDED_ORCHESTRATOR : null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Replace {{key}} for provided keys only; craft placeholders stay intact. */
|
|
76
|
-
export function substitutePlaceholders(text, values) {
|
|
77
|
-
return String(text ?? "").replace(/\{\{([A-Za-z0-9_]+)\}\}/g, (token, key) => (
|
|
78
|
-
Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : token
|
|
79
|
-
));
|
|
80
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* `oris-skills loop verify [--temp]` — self-check of the same-chat loop
|
|
5
|
-
* runtime. With --temp it builds a throwaway smoke repository, arms it, and
|
|
6
|
-
* simulates both the Cursor and the Claude Code stop hooks.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import childProcess from "node:child_process";
|
|
10
|
-
import fs from "node:fs";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import process from "node:process";
|
|
14
|
-
import { scriptPath } from "./oris-loop-bundle.mjs";
|
|
15
|
-
import { initSmokeRepository } from "./oris-loop-fixtures.mjs";
|
|
16
|
-
import { ORIS_FLOW_ADAPTER, ORIS_FLOW_RUNTIME } from "../flow/oris-flow-layout.mjs";
|
|
17
|
-
|
|
18
|
-
function readJson(filePath) {
|
|
19
|
-
if (!fs.existsSync(filePath)) return null;
|
|
20
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function tailJsonl(filePath, count = 5) {
|
|
24
|
-
if (!fs.existsSync(filePath)) return [];
|
|
25
|
-
return fs.readFileSync(filePath, "utf8")
|
|
26
|
-
.trim()
|
|
27
|
-
.split("\n")
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
.slice(-count)
|
|
30
|
-
.map((line) => JSON.parse(line));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function runHook(hookPath, repoRoot, statePath, input, { bom = false, platformArg = "" } = {}) {
|
|
34
|
-
const payload = JSON.stringify(input);
|
|
35
|
-
const stdin = bom ? `\uFEFF${payload}` : payload;
|
|
36
|
-
const args = [hookPath];
|
|
37
|
-
if (platformArg) args.push("--platform", platformArg);
|
|
38
|
-
const result = childProcess.spawnSync(process.execPath, args, {
|
|
39
|
-
input: stdin,
|
|
40
|
-
encoding: "utf8",
|
|
41
|
-
cwd: repoRoot,
|
|
42
|
-
env: { ...process.env, ORIS_LOOP_CHAT_STATE: statePath },
|
|
43
|
-
});
|
|
44
|
-
return {
|
|
45
|
-
status: result.status,
|
|
46
|
-
stderr: result.stderr?.trim() ?? "",
|
|
47
|
-
stdout: result.stdout?.trim() ?? "",
|
|
48
|
-
output: result.stdout?.trim() ? JSON.parse(result.stdout.trim()) : {},
|
|
49
|
-
state: readJson(statePath),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function check(name, ok, detail) {
|
|
54
|
-
const mark = ok ? "OK" : "FAIL";
|
|
55
|
-
console.log(`[${mark}] ${name}${detail ? `: ${detail}` : ""}`);
|
|
56
|
-
return ok;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function parseArgs(argv) {
|
|
60
|
-
const options = {
|
|
61
|
-
repositoryRoot: "",
|
|
62
|
-
temp: false,
|
|
63
|
-
simulateOnly: false,
|
|
64
|
-
statusOnly: false,
|
|
65
|
-
withBom: false,
|
|
66
|
-
conversation: "",
|
|
67
|
-
};
|
|
68
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
69
|
-
const raw = argv[i];
|
|
70
|
-
if (raw === "--temp") options.temp = true;
|
|
71
|
-
else if (raw === "--simulate") options.simulateOnly = true;
|
|
72
|
-
else if (raw === "--status-only") options.statusOnly = true;
|
|
73
|
-
else if (raw === "--bom") options.withBom = true;
|
|
74
|
-
else if (raw.startsWith("--conversation=")) options.conversation = raw.slice("--conversation=".length);
|
|
75
|
-
else if (raw.startsWith("--repository-root=")) options.repositoryRoot = raw.slice("--repository-root=".length);
|
|
76
|
-
else if (raw === "--repository-root" && argv[i + 1]) {
|
|
77
|
-
options.repositoryRoot = argv[i + 1];
|
|
78
|
-
i += 1;
|
|
79
|
-
} else if (!raw.startsWith("-") && !options.repositoryRoot) {
|
|
80
|
-
options.repositoryRoot = raw;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return options;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const options = parseArgs(process.argv.slice(2));
|
|
87
|
-
let repoRoot = options.repositoryRoot ? path.resolve(options.repositoryRoot) : process.cwd();
|
|
88
|
-
let tempRoot = null;
|
|
89
|
-
|
|
90
|
-
if (options.temp) {
|
|
91
|
-
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "oris-loop-verify-"));
|
|
92
|
-
const task = initSmokeRepository(tempRoot);
|
|
93
|
-
repoRoot = tempRoot;
|
|
94
|
-
if (!options.statusOnly && !options.simulateOnly) {
|
|
95
|
-
const chatScript = scriptPath(import.meta.url, "oris-loop-chat.mjs");
|
|
96
|
-
const start = childProcess.spawnSync(process.execPath, [
|
|
97
|
-
chatScript,
|
|
98
|
-
"--action", "start",
|
|
99
|
-
"--task", task.taskPath,
|
|
100
|
-
"--repository-root", repoRoot,
|
|
101
|
-
], { cwd: repoRoot, encoding: "utf8" });
|
|
102
|
-
if (start.status !== 0) {
|
|
103
|
-
console.error(start.stderr || start.stdout);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const runtimeDir = path.join(repoRoot, ORIS_FLOW_RUNTIME);
|
|
110
|
-
const statePath = path.join(runtimeDir, "chat-active.json");
|
|
111
|
-
const debugPath = path.join(runtimeDir, "hook-debug.jsonl");
|
|
112
|
-
const stopHookPath = scriptPath(import.meta.url, "oris-loop-stop.mjs");
|
|
113
|
-
const chatScript = scriptPath(import.meta.url, "oris-loop-chat.mjs");
|
|
114
|
-
const cursorWrapper = path.join(repoRoot, ".cursor", "hooks", "oris-loop-stop.mjs");
|
|
115
|
-
const claudeWrapper = path.join(repoRoot, ".claude", "hooks", "oris-loop-stop.mjs");
|
|
116
|
-
|
|
117
|
-
console.log("Oris loop verify (same-chat)");
|
|
118
|
-
console.log(`Repository: ${repoRoot}`);
|
|
119
|
-
if (tempRoot) console.log("(temporary smoke repository)");
|
|
120
|
-
console.log("");
|
|
121
|
-
|
|
122
|
-
const checks = [];
|
|
123
|
-
checks.push(check("adapter.json", fs.existsSync(path.join(repoRoot, ORIS_FLOW_ADAPTER))));
|
|
124
|
-
checks.push(check("shared stop hook", fs.existsSync(stopHookPath)));
|
|
125
|
-
checks.push(check("chat helper", fs.existsSync(chatScript)));
|
|
126
|
-
const hasCursor = fs.existsSync(path.join(repoRoot, ".cursor", "hooks.json")) && fs.existsSync(cursorWrapper);
|
|
127
|
-
const hasClaude = fs.existsSync(path.join(repoRoot, ".claude", "settings.json")) && fs.existsSync(claudeWrapper);
|
|
128
|
-
checks.push(check("stop hook armed (cursor or claude)", hasCursor || hasClaude, `cursor=${hasCursor} claude=${hasClaude}`));
|
|
129
|
-
|
|
130
|
-
const chat = readJson(statePath);
|
|
131
|
-
console.log("");
|
|
132
|
-
console.log("chat-active.json:");
|
|
133
|
-
if (!chat) {
|
|
134
|
-
console.log("(missing - run npx oris-skills loop chat --action start first)");
|
|
135
|
-
} else {
|
|
136
|
-
console.log(JSON.stringify(chat, null, 2));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
console.log("");
|
|
140
|
-
console.log(`hook-debug.jsonl (last ${Math.min(5, tailJsonl(debugPath).length)} events):`);
|
|
141
|
-
const events = tailJsonl(debugPath, 5);
|
|
142
|
-
if (events.length === 0) {
|
|
143
|
-
console.log("(none yet)");
|
|
144
|
-
} else {
|
|
145
|
-
for (const event of events) {
|
|
146
|
-
console.log(`- ${event.at} | ${event.decision} | ${event.reason}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!options.simulateOnly && !options.statusOnly) {
|
|
151
|
-
console.log("");
|
|
152
|
-
console.log("Simulating Cursor stop hook...");
|
|
153
|
-
if (!chat) {
|
|
154
|
-
check("simulate", false, "chat-active.json missing");
|
|
155
|
-
if (tempRoot) fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const conversationId = options.conversation || chat.conversationId || "debug-conversation";
|
|
160
|
-
|
|
161
|
-
const cursorInput = {
|
|
162
|
-
conversation_id: conversationId,
|
|
163
|
-
status: "completed",
|
|
164
|
-
loop_count: Number(chat.iteration ?? 0),
|
|
165
|
-
hook_event_name: "stop",
|
|
166
|
-
workspace_roots: [repoRoot],
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
console.log(`Conversation: ${conversationId}${options.withBom ? " (UTF-8 BOM stdin)" : ""}`);
|
|
170
|
-
const result = runHook(stopHookPath, repoRoot, statePath, cursorInput, { bom: options.withBom });
|
|
171
|
-
checks.push(check("cursor hook exit code 0", result.status === 0, result.stderr || undefined));
|
|
172
|
-
checks.push(check("cursor followup_message emitted", Boolean(result.output.followup_message)));
|
|
173
|
-
|
|
174
|
-
if (result.output.followup_message) {
|
|
175
|
-
const preview = result.output.followup_message.split("\n").slice(0, 3).join(" | ");
|
|
176
|
-
console.log(`Follow-up preview: ${preview}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (result.state) {
|
|
180
|
-
checks.push(check("conversation bound", result.state.conversationId === conversationId));
|
|
181
|
-
checks.push(check("iteration incremented", Number(result.state.iteration) > Number(chat.iteration ?? 0)));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
console.log("");
|
|
185
|
-
console.log("Simulating Claude Code stop hook...");
|
|
186
|
-
const beforeClaude = readJson(statePath);
|
|
187
|
-
const claudeInput = {
|
|
188
|
-
session_id: conversationId,
|
|
189
|
-
hook_event_name: "Stop",
|
|
190
|
-
stop_hook_active: false,
|
|
191
|
-
cwd: repoRoot,
|
|
192
|
-
};
|
|
193
|
-
const claudeResult = runHook(stopHookPath, repoRoot, statePath, claudeInput, { platformArg: "claude" });
|
|
194
|
-
checks.push(check("claude hook exit code 0", claudeResult.status === 0, claudeResult.stderr || undefined));
|
|
195
|
-
checks.push(check("claude decision block emitted", claudeResult.output.decision === "block" && Boolean(claudeResult.output.reason)));
|
|
196
|
-
if (claudeResult.state && beforeClaude) {
|
|
197
|
-
checks.push(check("claude iteration incremented", Number(claudeResult.state.iteration) > Number(beforeClaude.iteration ?? 0)));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
console.log("");
|
|
202
|
-
const passed = checks.every(Boolean);
|
|
203
|
-
console.log(passed ? "Same-chat verification passed." : "Same-chat verification failed.");
|
|
204
|
-
if (tempRoot) fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
205
|
-
process.exit(passed ? 0 : 1);
|