oris-skills 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +2 -11
- package/CHANGELOG.md +48 -0
- package/README.md +79 -31
- package/agents/oris-loop-debriefer.md +1 -0
- package/agents/oris-loop-doctor.md +1 -1
- package/agents/oris-loop-executor.md +5 -2
- package/agents/oris-loop-verifier.md +6 -2
- package/docs/architecture.md +9 -7
- package/docs/distribution.md +1 -1
- package/docs/maintainer-guide.md +5 -5
- package/docs/user-guide.md +12 -9
- package/package.json +2 -3
- package/references/clean-code-checklist.md +20 -107
- package/references/conventions.md +43 -3
- package/references/doc-policy.md +19 -3
- package/references/loop-contract.md +24 -17
- package/references/loop.schema.json +13 -0
- package/references/repo-map.md +3 -3
- package/references/repo-map.schema.json +41 -0
- package/references/settings.md +2 -1
- package/references/settings.schema.json +5 -0
- package/scripts/flow/oris-flow-layout.mjs +1 -0
- package/scripts/flow/oris-flow-scan.mjs +265 -27
- package/scripts/install/generate-agent-adapters.mjs +19 -2
- package/scripts/install/install-user-skills.mjs +61 -3
- package/scripts/install/uninstall-user-skills.mjs +17 -1
- package/scripts/loop/oris-loop-bootstrap.mjs +7 -2
- package/scripts/loop/oris-loop-chat.mjs +34 -16
- package/scripts/loop/oris-loop-demo.mjs +11 -20
- package/scripts/loop/oris-loop-document.mjs +24 -7
- package/scripts/loop/oris-loop-dry-run.mjs +8 -4
- package/scripts/loop/oris-loop-fixtures.mjs +13 -12
- package/scripts/loop/oris-loop-run.mjs +80 -20
- package/scripts/loop/oris-loop-stop.mjs +48 -9
- package/scripts/loop/oris-loop-templates.mjs +13 -10
- package/scripts/loop/oris-loop-verify.mjs +1 -2
- package/scripts/oris-skills.mjs +3 -2
- package/scripts/tests/run-all-tests.mjs +1 -2
- package/scripts/tests/test-oris-flow-scan.mjs +109 -1
- package/scripts/tests/test-oris-loop-document.mjs +39 -3
- package/scripts/tests/test-oris-loop-run.mjs +28 -2
- package/scripts/tests/test-oris-loop-smoke.mjs +36 -3
- package/scripts/tests/test-oris-loop-stop.mjs +65 -4
- package/scripts/tests/test-routing-lifecycle.mjs +38 -23
- package/scripts/tests/test-skill-style.mjs +64 -0
- package/skills/oris-flow/SKILL.md +53 -25
- package/skills/oris-flow/references/architecture.md +77 -0
- package/skills/oris-flow/references/change.md +60 -0
- package/skills/oris-flow/references/criteria.md +60 -0
- package/skills/oris-flow/references/discover.md +66 -0
- package/skills/oris-flow/references/docs.md +33 -0
- package/skills/oris-flow/references/fix.md +49 -0
- package/skills/oris-flow/references/help.md +34 -0
- package/skills/oris-flow/references/implement.md +48 -0
- package/skills/oris-flow/references/loop-craft.md +59 -0
- package/skills/oris-flow/references/loop-improve.md +32 -0
- package/skills/oris-flow/references/loop-run.md +47 -0
- package/skills/oris-flow/references/loop.md +56 -0
- package/skills/oris-flow/references/new.md +64 -0
- package/skills/oris-flow/references/plan.md +58 -0
- package/skills/oris-flow/references/setup.md +100 -0
- package/skills/oris-flow/references/verify.md +57 -0
- package/skills/{oris-loop → oris-flow}/templates/debriefer.md +2 -1
- package/skills/oris-flow/templates/doctor.md +22 -0
- package/skills/{oris-loop → oris-flow}/templates/executor.md +8 -2
- package/skills/{oris-loop → oris-flow}/templates/orchestrator.md +10 -7
- package/skills/{oris-loop → oris-flow}/templates/verifier.md +6 -3
- package/references/questions.md +0 -38
- package/skills/oris-flow-criteria/SKILL.md +0 -49
- package/skills/oris-flow-discover/SKILL.md +0 -58
- package/skills/oris-flow-docs/SKILL.md +0 -31
- package/skills/oris-flow-fix/SKILL.md +0 -42
- package/skills/oris-flow-implement/SKILL.md +0 -43
- package/skills/oris-flow-plan/SKILL.md +0 -51
- package/skills/oris-flow-setup/SKILL.md +0 -49
- package/skills/oris-help/SKILL.md +0 -30
- package/skills/oris-help/agents/openai.yaml +0 -4
- package/skills/oris-loop/SKILL.md +0 -66
- package/skills/oris-loop/agents/openai.yaml +0 -4
- package/skills/oris-loop/references/craft.md +0 -48
- package/skills/oris-loop/references/improve.md +0 -23
- package/skills/oris-loop/references/run.md +0 -30
- package/skills/oris-loop/templates/doctor.md +0 -20
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import childProcess from "node:child_process";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import process from "node:process";
|
|
@@ -9,12 +10,14 @@ import {
|
|
|
9
10
|
ORIS_FLOW_MAP_SCHEMA,
|
|
10
11
|
ORIS_FLOW_MAP_SCHEMA_VERSION,
|
|
11
12
|
ORIS_FLOW_MAPS_ROOT,
|
|
13
|
+
ORIS_FLOW_TASKS_ROOT,
|
|
12
14
|
relativeToRepository,
|
|
13
15
|
resolveInsideRepository,
|
|
14
16
|
toPosixPath,
|
|
15
17
|
} from "./oris-flow-layout.mjs";
|
|
16
18
|
|
|
17
19
|
const ignoredDirectories = new Set([
|
|
20
|
+
".oris-flow",
|
|
18
21
|
".git",
|
|
19
22
|
".hg",
|
|
20
23
|
".svn",
|
|
@@ -40,9 +43,17 @@ const stackDetectors = [
|
|
|
40
43
|
{ id: "java", label: "Java", match: (file) => ["pom.xml", "build.gradle", "build.gradle.kts"].includes(file.name) },
|
|
41
44
|
{ id: "go", label: "Go", match: (file) => file.name === "go.mod" },
|
|
42
45
|
{ id: "rust", label: "Rust", match: (file) => file.name === "Cargo.toml" },
|
|
46
|
+
{ id: "php", label: "PHP", match: (file) => file.name === "composer.json" },
|
|
47
|
+
{ id: "ruby", label: "Ruby", match: (file) => file.name === "Gemfile" },
|
|
48
|
+
{ id: "make", label: "Make", match: (file) => file.name === "Makefile" },
|
|
43
49
|
{ id: "playwright", label: "Playwright", match: (file) => /^playwright\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
44
50
|
{ id: "vitest", label: "Vitest", match: (file) => /^vitest\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
45
51
|
{ id: "jest", label: "Jest", match: (file) => /^jest\.config\.(ts|js|mjs|cjs)$/i.test(file.name) || file.name === "jest.config.json" },
|
|
52
|
+
{ id: "vite", label: "Vite", match: (file) => /^vite\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
53
|
+
{ id: "next", label: "Next.js", match: (file) => /^next\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
54
|
+
{ id: "angular", label: "Angular", match: (file) => file.name === "angular.json" },
|
|
55
|
+
{ id: "nx", label: "Nx", match: (file) => file.name === "nx.json" },
|
|
56
|
+
{ id: "turborepo", label: "Turborepo", match: (file) => file.name === "turbo.json" },
|
|
46
57
|
{ id: "docker", label: "Docker", match: (file) => file.name === "Dockerfile" || file.name === "docker-compose.yml" || file.name === "compose.yaml" },
|
|
47
58
|
{ id: "azure-pipelines", label: "Azure Pipelines", match: (file) => /^azure-pipelines.*\.ya?ml$/i.test(file.name) },
|
|
48
59
|
{ id: "github-actions", label: "GitHub Actions", match: (file) => file.relative.startsWith(".github/workflows/") && /\.ya?ml$/i.test(file.name) },
|
|
@@ -53,6 +64,68 @@ function fail(message) {
|
|
|
53
64
|
process.exit(1);
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
function git(root, args) {
|
|
68
|
+
try {
|
|
69
|
+
const output = childProcess.execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
70
|
+
return output.trim();
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Commit SHA the scan anchors to; null outside git (the anchor is then unavailable, never an error). */
|
|
77
|
+
export function gitHeadSha(root) {
|
|
78
|
+
return git(root, ["rev-parse", "HEAD"]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Files changed since the anchored SHA: committed delta + working-tree changes,
|
|
83
|
+
* deduped. The map's own files (.oris-flow/**) never count — they describe the
|
|
84
|
+
* code, they are not the code.
|
|
85
|
+
*/
|
|
86
|
+
export function gitChangedFiles(root, sinceSha) {
|
|
87
|
+
const diff = git(root, ["diff", "--name-only", `${sinceSha}..HEAD`]);
|
|
88
|
+
const status = git(root, ["status", "--porcelain", "-uall"]);
|
|
89
|
+
if (diff === null && status === null) return null;
|
|
90
|
+
const changed = new Set();
|
|
91
|
+
const add = (file) => {
|
|
92
|
+
const clean = file.replace(/^"|"$/g, "");
|
|
93
|
+
if (clean && !clean.startsWith(".oris-flow/")) changed.add(clean);
|
|
94
|
+
};
|
|
95
|
+
for (const line of (diff ?? "").split(/\r?\n/)) if (line.trim()) add(line.trim());
|
|
96
|
+
for (const line of (status ?? "").split(/\r?\n/)) {
|
|
97
|
+
const file = line.slice(3).trim();
|
|
98
|
+
if (file) add(file.includes(" -> ") ? file.split(" -> ")[1] : file);
|
|
99
|
+
}
|
|
100
|
+
return [...changed].sort();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Freshness in one cheap call (the delta-scan anchor): compare the manifest's
|
|
105
|
+
* headSha with the current HEAD. Equal and clean → fresh, zero reads. Moved →
|
|
106
|
+
* the changed file list tells the caller which map areas to refresh in place.
|
|
107
|
+
*/
|
|
108
|
+
export function checkFreshness(repositoryRoot) {
|
|
109
|
+
const root = path.resolve(repositoryRoot);
|
|
110
|
+
const manifest = readJson(path.join(root, ...ORIS_FLOW_MANIFEST.split("/")));
|
|
111
|
+
if (!manifest) return { manifest: false, fresh: false, reason: "no manifest — run a full scan" };
|
|
112
|
+
const headSha = gitHeadSha(root);
|
|
113
|
+
const scannedSha = manifest.headSha ?? null;
|
|
114
|
+
if (!headSha || !scannedSha) {
|
|
115
|
+
return { manifest: true, fresh: false, reason: "no SHA anchor — refresh writes one", headSha, scannedSha, changedFiles: null };
|
|
116
|
+
}
|
|
117
|
+
const changedFiles = gitChangedFiles(root, scannedSha) ?? [];
|
|
118
|
+
const fresh = headSha === scannedSha && changedFiles.length === 0;
|
|
119
|
+
return {
|
|
120
|
+
manifest: true,
|
|
121
|
+
fresh,
|
|
122
|
+
headSha,
|
|
123
|
+
scannedSha,
|
|
124
|
+
changedFiles: fresh ? [] : changedFiles.slice(0, 200),
|
|
125
|
+
sections: Object.keys(manifest.sections ?? {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
56
129
|
function parseArgs(argv) {
|
|
57
130
|
const options = {
|
|
58
131
|
repositoryRoot: process.cwd(),
|
|
@@ -72,6 +145,7 @@ function parseArgs(argv) {
|
|
|
72
145
|
else if (key === "--write") options.write = true;
|
|
73
146
|
else if (key === "--json") options.json = true;
|
|
74
147
|
else if (key === "--area" || key.startsWith("--area=")) [i, options.area] = readValue(i, raw);
|
|
148
|
+
else if (key === "--check") options.check = true;
|
|
75
149
|
else fail(`Unknown argument: ${raw}`);
|
|
76
150
|
}
|
|
77
151
|
return options;
|
|
@@ -80,9 +154,16 @@ function parseArgs(argv) {
|
|
|
80
154
|
function walk(root, options = {}) {
|
|
81
155
|
const maxFiles = Number(options.maxFiles ?? 6000);
|
|
82
156
|
const files = [];
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
157
|
+
const queue = [root];
|
|
158
|
+
let truncated = false;
|
|
159
|
+
let index = 0;
|
|
160
|
+
while (index < queue.length) {
|
|
161
|
+
if (files.length >= maxFiles) {
|
|
162
|
+
truncated = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
const current = queue[index];
|
|
166
|
+
index += 1;
|
|
86
167
|
let entries = [];
|
|
87
168
|
try {
|
|
88
169
|
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
@@ -95,14 +176,17 @@ function walk(root, options = {}) {
|
|
|
95
176
|
const full = path.join(current, entry.name);
|
|
96
177
|
const relative = relativeToRepository(root, full);
|
|
97
178
|
if (entry.isDirectory()) {
|
|
98
|
-
|
|
179
|
+
queue.push(full);
|
|
99
180
|
} else if (entry.isFile()) {
|
|
100
181
|
files.push({ full, relative, name: entry.name });
|
|
182
|
+
if (files.length >= maxFiles) {
|
|
183
|
+
truncated = true;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
101
186
|
}
|
|
102
|
-
if (files.length >= maxFiles) break;
|
|
103
187
|
}
|
|
104
188
|
}
|
|
105
|
-
return files;
|
|
189
|
+
return { files, truncated };
|
|
106
190
|
}
|
|
107
191
|
|
|
108
192
|
function readJson(filePath) {
|
|
@@ -130,6 +214,49 @@ function detectStacks(files) {
|
|
|
130
214
|
}));
|
|
131
215
|
}
|
|
132
216
|
|
|
217
|
+
function detectProjectType(files, stacks) {
|
|
218
|
+
const stackIds = new Set(stacks.map((stack) => stack.id));
|
|
219
|
+
const has = (predicate) => files.some(predicate);
|
|
220
|
+
const packageJsonCount = files.filter((file) => file.name === "package.json").length;
|
|
221
|
+
const projectFileCount = files.filter((file) => /\.(csproj|fsproj|vbproj)$/i.test(file.name)).length;
|
|
222
|
+
|
|
223
|
+
if (files.length === 0) {
|
|
224
|
+
return { id: "empty", label: "Empty repository", confidence: "detected", evidence: [] };
|
|
225
|
+
}
|
|
226
|
+
if (stackIds.has("nx") || stackIds.has("turborepo") || packageJsonCount > 2 || projectFileCount > 3
|
|
227
|
+
|| has((file) => /^packages\/[^/]+\/package\.json$/.test(file.relative))) {
|
|
228
|
+
return {
|
|
229
|
+
id: "monorepo",
|
|
230
|
+
label: "Monorepo / multi-project",
|
|
231
|
+
confidence: "detected",
|
|
232
|
+
evidence: files.filter((file) => file.name === "package.json" || /\.(sln|csproj)$/i.test(file.name)).map((file) => file.relative).slice(0, 8),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const webSignals = files.filter((file) =>
|
|
236
|
+
stackIds.has("next") && /^next\.config\./.test(file.name)
|
|
237
|
+
|| stackIds.has("angular") && file.name === "angular.json"
|
|
238
|
+
|| stackIds.has("vite") && /^vite\.config\./.test(file.name)
|
|
239
|
+
|| /(^|\/)(wwwroot|public)\//.test(file.relative)
|
|
240
|
+
|| /(^|\/)(Views|Pages|pages|app)\/.+\.(cshtml|razor|tsx|jsx|vue|svelte)$/i.test(file.relative));
|
|
241
|
+
if (webSignals.length > 0) {
|
|
242
|
+
return { id: "webapp", label: "Web application", confidence: "detected", evidence: webSignals.map((file) => file.relative).slice(0, 8) };
|
|
243
|
+
}
|
|
244
|
+
const apiSignals = files.filter((file) => /(^|\/)(Controllers|controllers|routes|api)\//.test(file.relative) || /openapi\.(ya?ml|json)$/i.test(file.name));
|
|
245
|
+
if (apiSignals.length > 0) {
|
|
246
|
+
return { id: "api", label: "API / backend service", confidence: "detected", evidence: apiSignals.map((file) => file.relative).slice(0, 8) };
|
|
247
|
+
}
|
|
248
|
+
const rootPackage = files.find((file) => file.relative === "package.json");
|
|
249
|
+
if (rootPackage) {
|
|
250
|
+
const pkg = readJson(rootPackage.full);
|
|
251
|
+
if (pkg?.bin) return { id: "cli", label: "Command-line tool", confidence: "detected", evidence: ["package.json (bin)"] };
|
|
252
|
+
if (pkg?.exports || pkg?.main) return { id: "library", label: "Library / package", confidence: "detected", evidence: ["package.json (main/exports)"] };
|
|
253
|
+
}
|
|
254
|
+
if (has((file) => /(^|\/)(src|lib)\//.test(file.relative))) {
|
|
255
|
+
return { id: "library", label: "Library / package", confidence: "inferred", evidence: [] };
|
|
256
|
+
}
|
|
257
|
+
return { id: "unknown", label: "Unclassified", confidence: "inferred", evidence: [] };
|
|
258
|
+
}
|
|
259
|
+
|
|
133
260
|
function findPackageCommands(files) {
|
|
134
261
|
const commands = [];
|
|
135
262
|
for (const file of files.filter((item) => item.name === "package.json")) {
|
|
@@ -143,6 +270,7 @@ function findPackageCommands(files) {
|
|
|
143
270
|
cwd,
|
|
144
271
|
command: cwd === "." ? `npm run ${name}` : `npm run ${name} --prefix ${cwd}`,
|
|
145
272
|
evidence: file.relative,
|
|
273
|
+
confidence: "detected",
|
|
146
274
|
});
|
|
147
275
|
}
|
|
148
276
|
}
|
|
@@ -150,6 +278,37 @@ function findPackageCommands(files) {
|
|
|
150
278
|
return commands;
|
|
151
279
|
}
|
|
152
280
|
|
|
281
|
+
/** Default commands for stacks whose runners are conventional; marked inferred until the user confirms them. */
|
|
282
|
+
function deriveStackCommands(stacks, files) {
|
|
283
|
+
const commands = [];
|
|
284
|
+
const stackIds = new Set(stacks.map((stack) => stack.id));
|
|
285
|
+
const firstEvidence = (id) => stacks.find((stack) => stack.id === id)?.evidence[0] ?? "";
|
|
286
|
+
if (stackIds.has("dotnet")) {
|
|
287
|
+
const solution = files.find((file) => /\.sln$/i.test(file.name));
|
|
288
|
+
const target = solution ? solution.relative : firstEvidence("dotnet");
|
|
289
|
+
commands.push(
|
|
290
|
+
{ name: "build", cwd: ".", command: `dotnet build ${target}`.trim(), evidence: target, confidence: "inferred" },
|
|
291
|
+
{ name: "test", cwd: ".", command: `dotnet test ${target}`.trim(), evidence: target, confidence: "inferred" },
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (stackIds.has("python")) {
|
|
295
|
+
commands.push({ name: "test", cwd: ".", command: "python -m pytest", evidence: firstEvidence("python"), confidence: "inferred" });
|
|
296
|
+
}
|
|
297
|
+
if (stackIds.has("go")) {
|
|
298
|
+
commands.push(
|
|
299
|
+
{ name: "build", cwd: ".", command: "go build ./...", evidence: firstEvidence("go"), confidence: "inferred" },
|
|
300
|
+
{ name: "test", cwd: ".", command: "go test ./...", evidence: firstEvidence("go"), confidence: "inferred" },
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (stackIds.has("rust")) {
|
|
304
|
+
commands.push(
|
|
305
|
+
{ name: "build", cwd: ".", command: "cargo build", evidence: firstEvidence("rust"), confidence: "inferred" },
|
|
306
|
+
{ name: "test", cwd: ".", command: "cargo test", evidence: firstEvidence("rust"), confidence: "inferred" },
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return commands;
|
|
310
|
+
}
|
|
311
|
+
|
|
153
312
|
function hasAny(files, predicate) {
|
|
154
313
|
return files.some(predicate);
|
|
155
314
|
}
|
|
@@ -171,8 +330,8 @@ function buildSections(root, files, stacks, commands, scannedAt) {
|
|
|
171
330
|
const sections = {};
|
|
172
331
|
const topFiles = files.map((file) => file.relative);
|
|
173
332
|
|
|
174
|
-
if (hasAny(files, (file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING)\.md$/i.test(file.relative) || file.relative.startsWith("docs/"))) {
|
|
175
|
-
const docs = topFiles.filter((file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING)\.md$/i.test(file) || file.startsWith("docs/")).slice(0, 20);
|
|
333
|
+
if (hasAny(files, (file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING|AGENTS|CLAUDE)\.md$/i.test(file.relative) || file.relative.startsWith("docs/"))) {
|
|
334
|
+
const docs = topFiles.filter((file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING|AGENTS|CLAUDE)\.md$/i.test(file) || file.startsWith("docs/")).slice(0, 20);
|
|
176
335
|
sections.docs = makeSection(".oris-flow/maps/docs.md", "Documentation and task context detected.", {
|
|
177
336
|
scannedAt,
|
|
178
337
|
markdown: [
|
|
@@ -185,15 +344,18 @@ function buildSections(root, files, stacks, commands, scannedAt) {
|
|
|
185
344
|
});
|
|
186
345
|
}
|
|
187
346
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
347
|
+
const guidancePaths = topFiles.filter((file) =>
|
|
348
|
+
file.startsWith(".cursor/skills/") || file.startsWith(".cursor/rules/")
|
|
349
|
+
|| file.startsWith(".claude/skills/") || /^AGENTS\.md$/i.test(file) || /^CLAUDE\.md$/i.test(file)
|
|
350
|
+
|| /^CONTEXT\.md$/i.test(file) || file.startsWith("docs/adr/")).slice(0, 20);
|
|
351
|
+
if (guidancePaths.length > 0) {
|
|
352
|
+
sections.agentGuidance = makeSection(".oris-flow/maps/agent-guidance.md", "Agent guidance detected (AGENTS/CLAUDE/CONTEXT, skills, rules, ADRs).", {
|
|
191
353
|
scannedAt,
|
|
192
354
|
markdown: [
|
|
193
|
-
"#
|
|
355
|
+
"# Agent Guidance",
|
|
194
356
|
"",
|
|
195
|
-
"Detected
|
|
196
|
-
...
|
|
357
|
+
"Detected agent-facing guidance:",
|
|
358
|
+
...guidancePaths.map((file) => `- ${file}`),
|
|
197
359
|
"",
|
|
198
360
|
].join("\n"),
|
|
199
361
|
});
|
|
@@ -216,12 +378,12 @@ function buildSections(root, files, stacks, commands, scannedAt) {
|
|
|
216
378
|
}
|
|
217
379
|
|
|
218
380
|
if (commands.length > 0) {
|
|
219
|
-
sections.commands = makeSection(".oris-flow/maps/commands.md", `Detected ${commands.length}
|
|
381
|
+
sections.commands = makeSection(".oris-flow/maps/commands.md", `Detected ${commands.length} command(s).`, {
|
|
220
382
|
scannedAt,
|
|
221
383
|
markdown: [
|
|
222
384
|
"# Commands",
|
|
223
385
|
"",
|
|
224
|
-
...commands.map((command) => `- \`${command.command}\` (${command.evidence})`),
|
|
386
|
+
...commands.map((command) => `- \`${command.command}\` (${command.evidence || command.confidence}${command.confidence === "inferred" ? ", inferred — confirm before relying on it" : ""})`),
|
|
225
387
|
"",
|
|
226
388
|
].join("\n"),
|
|
227
389
|
});
|
|
@@ -261,12 +423,68 @@ function buildSections(root, files, stacks, commands, scannedAt) {
|
|
|
261
423
|
return sections;
|
|
262
424
|
}
|
|
263
425
|
|
|
426
|
+
function resolveTasksRoot(root, existingManifest) {
|
|
427
|
+
const configured = existingManifest?.setup?.tasksRoot;
|
|
428
|
+
if (typeof configured === "string" && configured.length > 0) return configured;
|
|
429
|
+
const legacy = path.join(root, "docs", "tasks");
|
|
430
|
+
if (fs.existsSync(legacy) && fs.readdirSync(legacy).length > 0) return "docs/tasks";
|
|
431
|
+
return ORIS_FLOW_TASKS_ROOT;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Refresh keeps user truth: confirmed decisions, tasksRoot, version control,
|
|
436
|
+
* refresh policy, confirmed commands, and confirmed sections (whose map files
|
|
437
|
+
* are also left untouched). A confirmed section the scan no longer sees is
|
|
438
|
+
* marked stale, never deleted.
|
|
439
|
+
*/
|
|
440
|
+
function mergeWithExisting(scan, existingManifest) {
|
|
441
|
+
if (!existingManifest || typeof existingManifest !== "object") return scan;
|
|
442
|
+
const merged = scan;
|
|
443
|
+
merged.manifest.generatedAt = existingManifest.generatedAt ?? merged.manifest.generatedAt;
|
|
444
|
+
if (Array.isArray(existingManifest.setup?.confirmedDecisions)) {
|
|
445
|
+
merged.manifest.setup.confirmedDecisions = existingManifest.setup.confirmedDecisions;
|
|
446
|
+
}
|
|
447
|
+
if (existingManifest.setup?.versionControl?.orisFlowMap) {
|
|
448
|
+
merged.manifest.setup.versionControl.orisFlowMap = existingManifest.setup.versionControl.orisFlowMap;
|
|
449
|
+
}
|
|
450
|
+
if (existingManifest.refreshPolicy && typeof existingManifest.refreshPolicy === "object") {
|
|
451
|
+
merged.manifest.refreshPolicy = existingManifest.refreshPolicy;
|
|
452
|
+
}
|
|
453
|
+
if (existingManifest.projectType?.confidence === "confirmed") {
|
|
454
|
+
merged.manifest.projectType = existingManifest.projectType;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const confirmedCommands = (existingManifest.commands ?? []).filter((command) => command?.confidence === "confirmed");
|
|
458
|
+
if (confirmedCommands.length > 0) {
|
|
459
|
+
const scanned = merged.manifest.commands.filter((command) =>
|
|
460
|
+
!confirmedCommands.some((confirmed) => confirmed.name === command.name && confirmed.cwd === command.cwd));
|
|
461
|
+
merged.manifest.commands = [...confirmedCommands, ...scanned];
|
|
462
|
+
merged.commands = merged.manifest.commands;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
merged.preservedSections = new Set();
|
|
466
|
+
for (const [key, pointer] of Object.entries(existingManifest.sections ?? {})) {
|
|
467
|
+
if (pointer?.confidence !== "confirmed") continue;
|
|
468
|
+
const rescanned = merged.manifest.sections[key];
|
|
469
|
+
merged.manifest.sections[key] = {
|
|
470
|
+
...pointer,
|
|
471
|
+
stale: rescanned ? pointer.stale === true : true,
|
|
472
|
+
};
|
|
473
|
+
merged.preservedSections.add(key);
|
|
474
|
+
if (merged.sections[key]) delete merged.sections[key];
|
|
475
|
+
}
|
|
476
|
+
return merged;
|
|
477
|
+
}
|
|
478
|
+
|
|
264
479
|
export function scanRepository(repositoryRoot, options = {}) {
|
|
265
480
|
const root = path.resolve(repositoryRoot);
|
|
266
481
|
const scannedAt = new Date().toISOString();
|
|
267
|
-
const
|
|
482
|
+
const headSha = gitHeadSha(root);
|
|
483
|
+
const existingManifest = readJson(path.join(root, ...ORIS_FLOW_MANIFEST.split("/")));
|
|
484
|
+
const { files, truncated } = walk(root, options);
|
|
268
485
|
const stacks = detectStacks(files);
|
|
269
|
-
const
|
|
486
|
+
const projectType = detectProjectType(files, stacks);
|
|
487
|
+
const commands = [...findPackageCommands(files), ...deriveStackCommands(stacks, files)];
|
|
270
488
|
const sections = buildSections(root, files, stacks, commands, scannedAt);
|
|
271
489
|
const sectionPointers = Object.fromEntries(
|
|
272
490
|
Object.entries(sections).map(([key, value]) => [key, value.pointer]),
|
|
@@ -277,9 +495,13 @@ export function scanRepository(repositoryRoot, options = {}) {
|
|
|
277
495
|
repository: path.basename(root),
|
|
278
496
|
generatedAt: scannedAt,
|
|
279
497
|
updatedAt: scannedAt,
|
|
498
|
+
headSha,
|
|
499
|
+
projectType,
|
|
500
|
+
truncated,
|
|
280
501
|
setup: {
|
|
281
|
-
mode: options.mode ?? "create",
|
|
502
|
+
mode: options.mode ?? (existingManifest ? "refresh" : "create"),
|
|
282
503
|
scanDepth: options.scanDepth ?? "balanced",
|
|
504
|
+
tasksRoot: options.tasksRoot ?? resolveTasksRoot(root, existingManifest),
|
|
283
505
|
confirmedDecisions: [],
|
|
284
506
|
versionControl: {
|
|
285
507
|
orisFlowMap: options.versionControl ?? "commit",
|
|
@@ -292,27 +514,35 @@ export function scanRepository(repositoryRoot, options = {}) {
|
|
|
292
514
|
commands,
|
|
293
515
|
sections: sectionPointers,
|
|
294
516
|
};
|
|
295
|
-
|
|
517
|
+
const scan = {
|
|
296
518
|
repositoryRoot: root,
|
|
297
519
|
scannedAt,
|
|
298
520
|
fileCount: files.length,
|
|
521
|
+
truncated,
|
|
522
|
+
projectType,
|
|
299
523
|
stacks,
|
|
300
524
|
commands,
|
|
301
525
|
sections,
|
|
302
526
|
manifest,
|
|
527
|
+
preservedSections: new Set(),
|
|
303
528
|
};
|
|
529
|
+
return mergeWithExisting(scan, existingManifest);
|
|
304
530
|
}
|
|
305
531
|
|
|
306
|
-
export function writeScanResult(scan) {
|
|
532
|
+
export function writeScanResult(scan, options = {}) {
|
|
307
533
|
const root = scan.repositoryRoot;
|
|
534
|
+
const area = options.area ?? "";
|
|
308
535
|
const manifestPath = resolveInsideRepository(root, ORIS_FLOW_MANIFEST, "Oris Flow manifest");
|
|
309
536
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
310
537
|
fs.writeFileSync(manifestPath, `${JSON.stringify(scan.manifest, null, 2)}\n`, "utf8");
|
|
311
538
|
|
|
312
|
-
|
|
539
|
+
const written = [];
|
|
540
|
+
for (const [key, section] of Object.entries(scan.sections)) {
|
|
541
|
+
if (area && key !== area) continue;
|
|
313
542
|
const sectionPath = resolveInsideRepository(root, section.pointer.path, "Oris Flow section");
|
|
314
543
|
fs.mkdirSync(path.dirname(sectionPath), { recursive: true });
|
|
315
544
|
fs.writeFileSync(sectionPath, section.markdown, "utf8");
|
|
545
|
+
written.push(section.pointer.path);
|
|
316
546
|
}
|
|
317
547
|
|
|
318
548
|
const readmePath = resolveInsideRepository(root, `${ORIS_FLOW_MAPS_ROOT}/README.md`, "Oris Flow map README");
|
|
@@ -322,17 +552,25 @@ export function writeScanResult(scan) {
|
|
|
322
552
|
}
|
|
323
553
|
return {
|
|
324
554
|
manifestPath: relativeToRepository(root, manifestPath),
|
|
325
|
-
sectionPaths:
|
|
555
|
+
sectionPaths: written,
|
|
556
|
+
preservedSections: [...(scan.preservedSections ?? [])],
|
|
326
557
|
};
|
|
327
558
|
}
|
|
328
559
|
|
|
329
560
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
330
561
|
const options = parseArgs(process.argv.slice(2));
|
|
562
|
+
if (options.check) {
|
|
563
|
+
console.log(JSON.stringify(checkFreshness(options.repositoryRoot), null, 2));
|
|
564
|
+
process.exit(0);
|
|
565
|
+
}
|
|
331
566
|
const scan = scanRepository(options.repositoryRoot, options);
|
|
332
|
-
const written = options.write ? writeScanResult(scan) : null;
|
|
567
|
+
const written = options.write ? writeScanResult(scan, { area: options.area }) : null;
|
|
333
568
|
const payload = {
|
|
334
569
|
repositoryRoot: toPosixPath(scan.repositoryRoot),
|
|
335
570
|
fileCount: scan.fileCount,
|
|
571
|
+
truncated: scan.truncated,
|
|
572
|
+
projectType: scan.projectType,
|
|
573
|
+
tasksRoot: scan.manifest.setup.tasksRoot,
|
|
336
574
|
stacks: scan.stacks,
|
|
337
575
|
commands: scan.commands,
|
|
338
576
|
sections: scan.manifest.sections,
|
|
@@ -342,9 +580,9 @@ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToP
|
|
|
342
580
|
console.log(JSON.stringify(payload, null, 2));
|
|
343
581
|
} else {
|
|
344
582
|
console.log(`Oris Flow scan: ${payload.repositoryRoot}`);
|
|
345
|
-
console.log(`Files scanned: ${payload.fileCount}`);
|
|
583
|
+
console.log(`Files scanned: ${payload.fileCount}${payload.truncated ? " (truncated)" : ""}`);
|
|
584
|
+
console.log(`Project type: ${payload.projectType.label}`);
|
|
346
585
|
console.log(`Stacks: ${payload.stacks.map((stack) => stack.label).join(", ") || "none"}`);
|
|
347
586
|
console.log(`Sections: ${Object.keys(payload.sections).join(", ") || "none"}`);
|
|
348
587
|
}
|
|
349
588
|
}
|
|
350
|
-
|
|
@@ -52,17 +52,34 @@ developer_instructions = ${instructions}
|
|
|
52
52
|
`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
function readAgents() {
|
|
56
|
+
return fs.readdirSync(sourceRoot)
|
|
57
57
|
.filter((name) => name.endsWith(".md"))
|
|
58
58
|
.map((name) => ({ name, ...readAgent(path.join(sourceRoot, name)) }));
|
|
59
|
+
}
|
|
59
60
|
|
|
61
|
+
/** Write adapters into repo-local .cursor/.claude/.codex dirs under root. */
|
|
62
|
+
export function generateAgentAdapters(root = repoRoot) {
|
|
63
|
+
const agents = readAgents();
|
|
60
64
|
for (const agent of agents) {
|
|
61
65
|
writeFile(path.join(root, ".cursor", "agents", agent.name), renderMarkdownAgent(agent));
|
|
62
66
|
writeFile(path.join(root, ".claude", "agents", agent.name), renderMarkdownAgent(agent));
|
|
63
67
|
writeFile(path.join(root, ".codex", "agents", agent.name.replace(/\.md$/, ".toml")), renderCodexAgent(agent));
|
|
64
68
|
}
|
|
69
|
+
return agents.map((agent) => agent.frontMatter.name);
|
|
70
|
+
}
|
|
65
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Write adapters into explicit agent homes, e.g. { cursor: "~/.cursor", codex: "~/.codex" }.
|
|
74
|
+
* Used by the installer so the generated adapters actually reach the user's agents.
|
|
75
|
+
*/
|
|
76
|
+
export function writeAgentAdapters(targets) {
|
|
77
|
+
const agents = readAgents();
|
|
78
|
+
for (const agent of agents) {
|
|
79
|
+
if (targets.cursor) writeFile(path.join(targets.cursor, "agents", agent.name), renderMarkdownAgent(agent));
|
|
80
|
+
if (targets.claude) writeFile(path.join(targets.claude, "agents", agent.name), renderMarkdownAgent(agent));
|
|
81
|
+
if (targets.codex) writeFile(path.join(targets.codex, "agents", agent.name.replace(/\.md$/, ".toml")), renderCodexAgent(agent));
|
|
82
|
+
}
|
|
66
83
|
return agents.map((agent) => agent.frontMatter.name);
|
|
67
84
|
}
|
|
68
85
|
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* - Cursor skills: ~/.cursor/skills/<name>/
|
|
9
9
|
* - Claude Code skills: ~/.claude/skills/<name>/
|
|
10
10
|
* - Codex prompts: ~/.codex/prompts/<name>.md (pointer to the bundled SKILL.md)
|
|
11
|
+
* - Copilot prompts: VS Code user prompts dir /<name>.prompt.md (same pointer)
|
|
12
|
+
* - Any other agent: ~/.oris/oris-flow.md — a universal pointer any file-reading
|
|
13
|
+
* agent can be told to follow.
|
|
11
14
|
*
|
|
12
15
|
* Agents are auto-detected from existing home directories; override with
|
|
13
|
-
* --agents cursor,claude,codex.
|
|
16
|
+
* --agents cursor,claude,codex,copilot.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import childProcess from "node:child_process";
|
|
@@ -19,11 +22,19 @@ import os from "node:os";
|
|
|
19
22
|
import path from "node:path";
|
|
20
23
|
import process from "node:process";
|
|
21
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { writeAgentAdapters } from "./generate-agent-adapters.mjs";
|
|
22
26
|
|
|
23
27
|
const markerName = ".oris-skills-managed.json";
|
|
24
28
|
const installManifestName = ".oris-skills-install.json";
|
|
25
29
|
const nodeDownloadUrl = "https://nodejs.org/en/download";
|
|
26
|
-
const KNOWN_AGENTS = ["cursor", "claude", "codex"];
|
|
30
|
+
const KNOWN_AGENTS = ["cursor", "claude", "codex", "copilot"];
|
|
31
|
+
|
|
32
|
+
/** VS Code user dir — where Copilot Chat reads user prompt files (chat.promptFiles). */
|
|
33
|
+
function vscodeUserDir(home) {
|
|
34
|
+
if (process.platform === "win32") return path.join(home, "AppData", "Roaming", "Code", "User");
|
|
35
|
+
if (process.platform === "darwin") return path.join(home, "Library", "Application Support", "Code", "User");
|
|
36
|
+
return path.join(home, ".config", "Code", "User");
|
|
37
|
+
}
|
|
27
38
|
|
|
28
39
|
function fail(message) {
|
|
29
40
|
throw new Error(message);
|
|
@@ -42,6 +53,7 @@ function defaultPaths() {
|
|
|
42
53
|
cursor: path.join(home, ".cursor"),
|
|
43
54
|
claude: path.join(home, ".claude"),
|
|
44
55
|
codex: path.join(home, ".codex"),
|
|
56
|
+
copilot: vscodeUserDir(home),
|
|
45
57
|
},
|
|
46
58
|
};
|
|
47
59
|
}
|
|
@@ -74,6 +86,7 @@ function parseArgs(argv) {
|
|
|
74
86
|
else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.agentRoots.cursor = value();
|
|
75
87
|
else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.agentRoots.claude = value();
|
|
76
88
|
else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.agentRoots.codex = value();
|
|
89
|
+
else if (key === "--copilot-root" || key.startsWith("--copilot-root=")) options.agentRoots.copilot = value();
|
|
77
90
|
else fail(`Unknown argument: ${raw}`);
|
|
78
91
|
}
|
|
79
92
|
for (const agent of options.agents) {
|
|
@@ -154,6 +167,7 @@ function defaultSettings() {
|
|
|
154
167
|
orisFlow: {
|
|
155
168
|
uiLanguage: "en",
|
|
156
169
|
artifactLanguage: "en",
|
|
170
|
+
responseStyle: "standard",
|
|
157
171
|
},
|
|
158
172
|
defaultProfiles: {
|
|
159
173
|
testing: "default",
|
|
@@ -204,6 +218,39 @@ User request: $ARGUMENTS
|
|
|
204
218
|
}
|
|
205
219
|
}
|
|
206
220
|
|
|
221
|
+
/** Copilot Chat user prompt files: /<name> in chat once chat.promptFiles is enabled. */
|
|
222
|
+
function installCopilotPrompts(skills, vscodeUserRoot, bundleRoot) {
|
|
223
|
+
const promptsRoot = path.join(vscodeUserRoot, "prompts");
|
|
224
|
+
fs.mkdirSync(promptsRoot, { recursive: true });
|
|
225
|
+
for (const skill of skills) {
|
|
226
|
+
const bundledSkill = path.join(bundleRoot, skill.relativePath, "SKILL.md").replace(/\\/g, "/");
|
|
227
|
+
const description = skillDescription(skill.sourceDir);
|
|
228
|
+
fs.writeFileSync(path.join(promptsRoot, `${skill.name}.prompt.md`), `---
|
|
229
|
+
description: ${description || "Oris entry point"}
|
|
230
|
+
---
|
|
231
|
+
<!-- ${markerName} -->
|
|
232
|
+
Read the Oris skill file at \`${bundledSkill}\` and follow it exactly.
|
|
233
|
+
Then handle the user's request in this conversation.
|
|
234
|
+
`, "utf8");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Universal pointer for any file-reading agent that has no install target of its own. */
|
|
239
|
+
function installUniversalPointer(skills, bundleRoot) {
|
|
240
|
+
const primary = skills[0];
|
|
241
|
+
const bundledSkill = path.join(bundleRoot, primary.relativePath, "SKILL.md").replace(/\\/g, "/");
|
|
242
|
+
fs.writeFileSync(path.join(path.dirname(bundleRoot), "oris-flow.md"), `<!-- ${markerName} -->
|
|
243
|
+
# Oris — universal entry point
|
|
244
|
+
|
|
245
|
+
Tell any agent that can read files:
|
|
246
|
+
|
|
247
|
+
> Read \`${bundledSkill}\` and follow it exactly.
|
|
248
|
+
|
|
249
|
+
That file routes every Oris flow (setup, discover, criteria, plan, implement, fix,
|
|
250
|
+
verify, change, loop, architecture, docs, help) in the same chat.
|
|
251
|
+
`, "utf8");
|
|
252
|
+
}
|
|
253
|
+
|
|
207
254
|
function main() {
|
|
208
255
|
requireNode();
|
|
209
256
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -224,8 +271,11 @@ function main() {
|
|
|
224
271
|
log(`Would write bundle: ${options.bundleRoot}`);
|
|
225
272
|
for (const agent of agents) {
|
|
226
273
|
if (agent === "codex") log(`Would write Codex prompts: ${path.join(options.agentRoots.codex, "prompts")}`);
|
|
274
|
+
else if (agent === "copilot") log(`Would write Copilot prompts: ${path.join(options.agentRoots.copilot, "prompts")}`);
|
|
227
275
|
else log(`Would write ${agent} skills: ${path.join(options.agentRoots[agent], "skills")}`);
|
|
276
|
+
if (agent !== "copilot") log(`Would write ${agent} loop agents: ${path.join(options.agentRoots[agent], "agents")}`);
|
|
228
277
|
}
|
|
278
|
+
log(`Would write universal pointer: ${path.join(path.dirname(options.bundleRoot), "oris-flow.md")}`);
|
|
229
279
|
log(`Would create settings if missing: ${options.settingsPath}`);
|
|
230
280
|
return;
|
|
231
281
|
}
|
|
@@ -245,11 +295,19 @@ function main() {
|
|
|
245
295
|
if (agent === "codex") {
|
|
246
296
|
installCodexPrompts(skills, options.agentRoots.codex, options.bundleRoot);
|
|
247
297
|
log(`Installed ${skills.length} Codex prompts`);
|
|
298
|
+
} else if (agent === "copilot") {
|
|
299
|
+
installCopilotPrompts(skills, options.agentRoots.copilot, options.bundleRoot);
|
|
300
|
+
log(`Installed ${skills.length} Copilot prompts (enable VS Code setting chat.promptFiles)`);
|
|
248
301
|
} else {
|
|
249
302
|
installSkillDirs(skills, path.join(options.agentRoots[agent], "skills"), manifest, options.bundleRoot, options);
|
|
250
303
|
log(`Installed ${skills.length} ${agent} skills`);
|
|
251
304
|
}
|
|
252
305
|
}
|
|
306
|
+
installUniversalPointer(skills, options.bundleRoot);
|
|
307
|
+
|
|
308
|
+
const adapterTargets = Object.fromEntries(agents.filter((agent) => agent !== "copilot").map((agent) => [agent, options.agentRoots[agent]]));
|
|
309
|
+
const loopAgents = writeAgentAdapters(adapterTargets);
|
|
310
|
+
log(`Installed ${loopAgents.length} loop agent adapters (${agents.join(", ")})`);
|
|
253
311
|
|
|
254
312
|
if (!fs.existsSync(options.settingsPath)) writeJson(options.settingsPath, defaultSettings());
|
|
255
313
|
writeJson(path.join(options.bundleRoot, installManifestName), {
|
|
@@ -260,7 +318,7 @@ function main() {
|
|
|
260
318
|
installedAt: new Date().toISOString(),
|
|
261
319
|
});
|
|
262
320
|
log(`Done for agents: ${agents.join(", ")}. Reload your agent environment.`);
|
|
263
|
-
log("Next: type /oris-flow in your agent chat, or try the loop demo: oris-skills loop demo");
|
|
321
|
+
log("Next: type /oris-flow in your agent chat, or try the loop demo: npx oris-skills loop demo");
|
|
264
322
|
}
|
|
265
323
|
|
|
266
324
|
try {
|