oris-skills 2.0.0 → 2.1.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 +6 -1
- package/CHANGELOG.md +38 -0
- package/README.md +11 -10
- package/agents/oris-loop-doctor.md +1 -1
- package/agents/oris-loop-verifier.md +3 -0
- package/docs/architecture.md +4 -3
- package/docs/distribution.md +1 -1
- package/docs/maintainer-guide.md +2 -2
- package/docs/user-guide.md +9 -7
- package/package.json +2 -3
- package/references/clean-code-checklist.md +20 -107
- package/references/conventions.md +16 -2
- package/references/doc-policy.md +19 -3
- package/references/loop-contract.md +23 -16
- package/references/loop.schema.json +13 -0
- package/references/repo-map.md +3 -3
- package/references/repo-map.schema.json +37 -0
- package/scripts/flow/oris-flow-layout.mjs +1 -0
- package/scripts/flow/oris-flow-scan.mjs +195 -27
- package/scripts/install/generate-agent-adapters.mjs +19 -2
- package/scripts/install/install-user-skills.mjs +7 -1
- package/scripts/loop/oris-loop-bootstrap.mjs +7 -2
- package/scripts/loop/oris-loop-chat.mjs +32 -14
- 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 +7 -3
- package/scripts/loop/oris-loop-fixtures.mjs +12 -11
- 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 +10 -7
- package/scripts/loop/oris-loop-verify.mjs +1 -2
- package/scripts/oris-skills.mjs +2 -1
- package/scripts/tests/run-all-tests.mjs +0 -2
- package/scripts/tests/test-oris-flow-scan.mjs +66 -0
- 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 +11 -9
- package/skills/oris-flow/SKILL.md +16 -3
- package/skills/oris-flow-architecture/SKILL.md +64 -0
- package/skills/oris-flow-change/SKILL.md +55 -0
- package/skills/oris-flow-criteria/SKILL.md +10 -4
- package/skills/oris-flow-discover/SKILL.md +10 -5
- package/skills/oris-flow-docs/SKILL.md +2 -2
- package/skills/oris-flow-fix/SKILL.md +1 -1
- package/skills/oris-flow-implement/SKILL.md +2 -2
- package/skills/oris-flow-merge/SKILL.md +45 -0
- package/skills/oris-flow-new/SKILL.md +58 -0
- package/skills/oris-flow-plan/SKILL.md +8 -4
- package/skills/oris-flow-setup/SKILL.md +54 -23
- package/skills/oris-flow-verify/SKILL.md +50 -0
- package/skills/oris-help/SKILL.md +4 -4
- package/skills/oris-loop/SKILL.md +12 -32
- package/skills/oris-loop/references/craft.md +10 -9
- package/skills/oris-loop/references/run.md +9 -7
- package/skills/oris-loop/templates/debriefer.md +1 -1
- package/skills/oris-loop/templates/doctor.md +9 -7
- package/skills/oris-loop/templates/executor.md +3 -2
- package/skills/oris-loop/templates/orchestrator.md +10 -7
- package/skills/oris-loop/templates/verifier.md +4 -2
- package/references/questions.md +0 -38
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
"type": "string",
|
|
37
37
|
"minLength": 1
|
|
38
38
|
},
|
|
39
|
+
"projectType": {
|
|
40
|
+
"$ref": "#/$defs/projectType"
|
|
41
|
+
},
|
|
42
|
+
"truncated": {
|
|
43
|
+
"type": "boolean"
|
|
44
|
+
},
|
|
39
45
|
"setup": {
|
|
40
46
|
"$ref": "#/$defs/setup"
|
|
41
47
|
},
|
|
@@ -132,6 +138,10 @@
|
|
|
132
138
|
"type": "string",
|
|
133
139
|
"enum": ["fast", "balanced", "manual-summary"]
|
|
134
140
|
},
|
|
141
|
+
"tasksRoot": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"minLength": 1
|
|
144
|
+
},
|
|
135
145
|
"confirmedDecisions": {
|
|
136
146
|
"type": "array",
|
|
137
147
|
"items": {
|
|
@@ -191,6 +201,33 @@
|
|
|
191
201
|
},
|
|
192
202
|
"evidence": {
|
|
193
203
|
"type": "string"
|
|
204
|
+
},
|
|
205
|
+
"confidence": {
|
|
206
|
+
"$ref": "#/$defs/confidence"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"projectType": {
|
|
211
|
+
"type": "object",
|
|
212
|
+
"additionalProperties": false,
|
|
213
|
+
"required": ["id", "label", "confidence"],
|
|
214
|
+
"properties": {
|
|
215
|
+
"id": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"enum": ["empty", "monorepo", "webapp", "api", "library", "cli", "unknown"]
|
|
218
|
+
},
|
|
219
|
+
"label": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"minLength": 1
|
|
222
|
+
},
|
|
223
|
+
"confidence": {
|
|
224
|
+
"$ref": "#/$defs/confidence"
|
|
225
|
+
},
|
|
226
|
+
"evidence": {
|
|
227
|
+
"type": "array",
|
|
228
|
+
"items": {
|
|
229
|
+
"type": "string"
|
|
230
|
+
}
|
|
194
231
|
}
|
|
195
232
|
}
|
|
196
233
|
}
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
export const ORIS_FLOW_ROOT = ".oris-flow";
|
|
5
5
|
export const ORIS_FLOW_MANIFEST = ".oris-flow/manifest.json";
|
|
6
6
|
export const ORIS_FLOW_MAPS_ROOT = ".oris-flow/maps";
|
|
7
|
+
export const ORIS_FLOW_TASKS_ROOT = ".oris-flow/tasks";
|
|
7
8
|
export const ORIS_FLOW_ADAPTER = ".oris-flow/adapter.json";
|
|
8
9
|
export const ORIS_FLOW_LOOPS_ROOT = ".oris-flow/loops";
|
|
9
10
|
export const ORIS_FLOW_RUNTIME = ".oris-flow/runtime";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
ORIS_FLOW_MAP_SCHEMA,
|
|
10
10
|
ORIS_FLOW_MAP_SCHEMA_VERSION,
|
|
11
11
|
ORIS_FLOW_MAPS_ROOT,
|
|
12
|
+
ORIS_FLOW_TASKS_ROOT,
|
|
12
13
|
relativeToRepository,
|
|
13
14
|
resolveInsideRepository,
|
|
14
15
|
toPosixPath,
|
|
15
16
|
} from "./oris-flow-layout.mjs";
|
|
16
17
|
|
|
17
18
|
const ignoredDirectories = new Set([
|
|
19
|
+
".oris-flow",
|
|
18
20
|
".git",
|
|
19
21
|
".hg",
|
|
20
22
|
".svn",
|
|
@@ -40,9 +42,17 @@ const stackDetectors = [
|
|
|
40
42
|
{ id: "java", label: "Java", match: (file) => ["pom.xml", "build.gradle", "build.gradle.kts"].includes(file.name) },
|
|
41
43
|
{ id: "go", label: "Go", match: (file) => file.name === "go.mod" },
|
|
42
44
|
{ id: "rust", label: "Rust", match: (file) => file.name === "Cargo.toml" },
|
|
45
|
+
{ id: "php", label: "PHP", match: (file) => file.name === "composer.json" },
|
|
46
|
+
{ id: "ruby", label: "Ruby", match: (file) => file.name === "Gemfile" },
|
|
47
|
+
{ id: "make", label: "Make", match: (file) => file.name === "Makefile" },
|
|
43
48
|
{ id: "playwright", label: "Playwright", match: (file) => /^playwright\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
44
49
|
{ id: "vitest", label: "Vitest", match: (file) => /^vitest\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
45
50
|
{ id: "jest", label: "Jest", match: (file) => /^jest\.config\.(ts|js|mjs|cjs)$/i.test(file.name) || file.name === "jest.config.json" },
|
|
51
|
+
{ id: "vite", label: "Vite", match: (file) => /^vite\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
52
|
+
{ id: "next", label: "Next.js", match: (file) => /^next\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
53
|
+
{ id: "angular", label: "Angular", match: (file) => file.name === "angular.json" },
|
|
54
|
+
{ id: "nx", label: "Nx", match: (file) => file.name === "nx.json" },
|
|
55
|
+
{ id: "turborepo", label: "Turborepo", match: (file) => file.name === "turbo.json" },
|
|
46
56
|
{ id: "docker", label: "Docker", match: (file) => file.name === "Dockerfile" || file.name === "docker-compose.yml" || file.name === "compose.yaml" },
|
|
47
57
|
{ id: "azure-pipelines", label: "Azure Pipelines", match: (file) => /^azure-pipelines.*\.ya?ml$/i.test(file.name) },
|
|
48
58
|
{ id: "github-actions", label: "GitHub Actions", match: (file) => file.relative.startsWith(".github/workflows/") && /\.ya?ml$/i.test(file.name) },
|
|
@@ -80,9 +90,16 @@ function parseArgs(argv) {
|
|
|
80
90
|
function walk(root, options = {}) {
|
|
81
91
|
const maxFiles = Number(options.maxFiles ?? 6000);
|
|
82
92
|
const files = [];
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
const queue = [root];
|
|
94
|
+
let truncated = false;
|
|
95
|
+
let index = 0;
|
|
96
|
+
while (index < queue.length) {
|
|
97
|
+
if (files.length >= maxFiles) {
|
|
98
|
+
truncated = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const current = queue[index];
|
|
102
|
+
index += 1;
|
|
86
103
|
let entries = [];
|
|
87
104
|
try {
|
|
88
105
|
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
@@ -95,14 +112,17 @@ function walk(root, options = {}) {
|
|
|
95
112
|
const full = path.join(current, entry.name);
|
|
96
113
|
const relative = relativeToRepository(root, full);
|
|
97
114
|
if (entry.isDirectory()) {
|
|
98
|
-
|
|
115
|
+
queue.push(full);
|
|
99
116
|
} else if (entry.isFile()) {
|
|
100
117
|
files.push({ full, relative, name: entry.name });
|
|
118
|
+
if (files.length >= maxFiles) {
|
|
119
|
+
truncated = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
101
122
|
}
|
|
102
|
-
if (files.length >= maxFiles) break;
|
|
103
123
|
}
|
|
104
124
|
}
|
|
105
|
-
return files;
|
|
125
|
+
return { files, truncated };
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
function readJson(filePath) {
|
|
@@ -130,6 +150,49 @@ function detectStacks(files) {
|
|
|
130
150
|
}));
|
|
131
151
|
}
|
|
132
152
|
|
|
153
|
+
function detectProjectType(files, stacks) {
|
|
154
|
+
const stackIds = new Set(stacks.map((stack) => stack.id));
|
|
155
|
+
const has = (predicate) => files.some(predicate);
|
|
156
|
+
const packageJsonCount = files.filter((file) => file.name === "package.json").length;
|
|
157
|
+
const projectFileCount = files.filter((file) => /\.(csproj|fsproj|vbproj)$/i.test(file.name)).length;
|
|
158
|
+
|
|
159
|
+
if (files.length === 0) {
|
|
160
|
+
return { id: "empty", label: "Empty repository", confidence: "detected", evidence: [] };
|
|
161
|
+
}
|
|
162
|
+
if (stackIds.has("nx") || stackIds.has("turborepo") || packageJsonCount > 2 || projectFileCount > 3
|
|
163
|
+
|| has((file) => /^packages\/[^/]+\/package\.json$/.test(file.relative))) {
|
|
164
|
+
return {
|
|
165
|
+
id: "monorepo",
|
|
166
|
+
label: "Monorepo / multi-project",
|
|
167
|
+
confidence: "detected",
|
|
168
|
+
evidence: files.filter((file) => file.name === "package.json" || /\.(sln|csproj)$/i.test(file.name)).map((file) => file.relative).slice(0, 8),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const webSignals = files.filter((file) =>
|
|
172
|
+
stackIds.has("next") && /^next\.config\./.test(file.name)
|
|
173
|
+
|| stackIds.has("angular") && file.name === "angular.json"
|
|
174
|
+
|| stackIds.has("vite") && /^vite\.config\./.test(file.name)
|
|
175
|
+
|| /(^|\/)(wwwroot|public)\//.test(file.relative)
|
|
176
|
+
|| /(^|\/)(Views|Pages|pages|app)\/.+\.(cshtml|razor|tsx|jsx|vue|svelte)$/i.test(file.relative));
|
|
177
|
+
if (webSignals.length > 0) {
|
|
178
|
+
return { id: "webapp", label: "Web application", confidence: "detected", evidence: webSignals.map((file) => file.relative).slice(0, 8) };
|
|
179
|
+
}
|
|
180
|
+
const apiSignals = files.filter((file) => /(^|\/)(Controllers|controllers|routes|api)\//.test(file.relative) || /openapi\.(ya?ml|json)$/i.test(file.name));
|
|
181
|
+
if (apiSignals.length > 0) {
|
|
182
|
+
return { id: "api", label: "API / backend service", confidence: "detected", evidence: apiSignals.map((file) => file.relative).slice(0, 8) };
|
|
183
|
+
}
|
|
184
|
+
const rootPackage = files.find((file) => file.relative === "package.json");
|
|
185
|
+
if (rootPackage) {
|
|
186
|
+
const pkg = readJson(rootPackage.full);
|
|
187
|
+
if (pkg?.bin) return { id: "cli", label: "Command-line tool", confidence: "detected", evidence: ["package.json (bin)"] };
|
|
188
|
+
if (pkg?.exports || pkg?.main) return { id: "library", label: "Library / package", confidence: "detected", evidence: ["package.json (main/exports)"] };
|
|
189
|
+
}
|
|
190
|
+
if (has((file) => /(^|\/)(src|lib)\//.test(file.relative))) {
|
|
191
|
+
return { id: "library", label: "Library / package", confidence: "inferred", evidence: [] };
|
|
192
|
+
}
|
|
193
|
+
return { id: "unknown", label: "Unclassified", confidence: "inferred", evidence: [] };
|
|
194
|
+
}
|
|
195
|
+
|
|
133
196
|
function findPackageCommands(files) {
|
|
134
197
|
const commands = [];
|
|
135
198
|
for (const file of files.filter((item) => item.name === "package.json")) {
|
|
@@ -143,6 +206,7 @@ function findPackageCommands(files) {
|
|
|
143
206
|
cwd,
|
|
144
207
|
command: cwd === "." ? `npm run ${name}` : `npm run ${name} --prefix ${cwd}`,
|
|
145
208
|
evidence: file.relative,
|
|
209
|
+
confidence: "detected",
|
|
146
210
|
});
|
|
147
211
|
}
|
|
148
212
|
}
|
|
@@ -150,6 +214,37 @@ function findPackageCommands(files) {
|
|
|
150
214
|
return commands;
|
|
151
215
|
}
|
|
152
216
|
|
|
217
|
+
/** Default commands for stacks whose runners are conventional; marked inferred until the user confirms them. */
|
|
218
|
+
function deriveStackCommands(stacks, files) {
|
|
219
|
+
const commands = [];
|
|
220
|
+
const stackIds = new Set(stacks.map((stack) => stack.id));
|
|
221
|
+
const firstEvidence = (id) => stacks.find((stack) => stack.id === id)?.evidence[0] ?? "";
|
|
222
|
+
if (stackIds.has("dotnet")) {
|
|
223
|
+
const solution = files.find((file) => /\.sln$/i.test(file.name));
|
|
224
|
+
const target = solution ? solution.relative : firstEvidence("dotnet");
|
|
225
|
+
commands.push(
|
|
226
|
+
{ name: "build", cwd: ".", command: `dotnet build ${target}`.trim(), evidence: target, confidence: "inferred" },
|
|
227
|
+
{ name: "test", cwd: ".", command: `dotnet test ${target}`.trim(), evidence: target, confidence: "inferred" },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (stackIds.has("python")) {
|
|
231
|
+
commands.push({ name: "test", cwd: ".", command: "python -m pytest", evidence: firstEvidence("python"), confidence: "inferred" });
|
|
232
|
+
}
|
|
233
|
+
if (stackIds.has("go")) {
|
|
234
|
+
commands.push(
|
|
235
|
+
{ name: "build", cwd: ".", command: "go build ./...", evidence: firstEvidence("go"), confidence: "inferred" },
|
|
236
|
+
{ name: "test", cwd: ".", command: "go test ./...", evidence: firstEvidence("go"), confidence: "inferred" },
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (stackIds.has("rust")) {
|
|
240
|
+
commands.push(
|
|
241
|
+
{ name: "build", cwd: ".", command: "cargo build", evidence: firstEvidence("rust"), confidence: "inferred" },
|
|
242
|
+
{ name: "test", cwd: ".", command: "cargo test", evidence: firstEvidence("rust"), confidence: "inferred" },
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return commands;
|
|
246
|
+
}
|
|
247
|
+
|
|
153
248
|
function hasAny(files, predicate) {
|
|
154
249
|
return files.some(predicate);
|
|
155
250
|
}
|
|
@@ -171,8 +266,8 @@ function buildSections(root, files, stacks, commands, scannedAt) {
|
|
|
171
266
|
const sections = {};
|
|
172
267
|
const topFiles = files.map((file) => file.relative);
|
|
173
268
|
|
|
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);
|
|
269
|
+
if (hasAny(files, (file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING|AGENTS|CLAUDE)\.md$/i.test(file.relative) || file.relative.startsWith("docs/"))) {
|
|
270
|
+
const docs = topFiles.filter((file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING|AGENTS|CLAUDE)\.md$/i.test(file) || file.startsWith("docs/")).slice(0, 20);
|
|
176
271
|
sections.docs = makeSection(".oris-flow/maps/docs.md", "Documentation and task context detected.", {
|
|
177
272
|
scannedAt,
|
|
178
273
|
markdown: [
|
|
@@ -185,15 +280,18 @@ function buildSections(root, files, stacks, commands, scannedAt) {
|
|
|
185
280
|
});
|
|
186
281
|
}
|
|
187
282
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
283
|
+
const guidancePaths = topFiles.filter((file) =>
|
|
284
|
+
file.startsWith(".cursor/skills/") || file.startsWith(".cursor/rules/")
|
|
285
|
+
|| file.startsWith(".claude/skills/") || /^AGENTS\.md$/i.test(file) || /^CLAUDE\.md$/i.test(file)
|
|
286
|
+
|| /^CONTEXT\.md$/i.test(file) || file.startsWith("docs/adr/")).slice(0, 20);
|
|
287
|
+
if (guidancePaths.length > 0) {
|
|
288
|
+
sections.agentGuidance = makeSection(".oris-flow/maps/agent-guidance.md", "Agent guidance detected (AGENTS/CLAUDE/CONTEXT, skills, rules, ADRs).", {
|
|
191
289
|
scannedAt,
|
|
192
290
|
markdown: [
|
|
193
|
-
"#
|
|
291
|
+
"# Agent Guidance",
|
|
194
292
|
"",
|
|
195
|
-
"Detected
|
|
196
|
-
...
|
|
293
|
+
"Detected agent-facing guidance:",
|
|
294
|
+
...guidancePaths.map((file) => `- ${file}`),
|
|
197
295
|
"",
|
|
198
296
|
].join("\n"),
|
|
199
297
|
});
|
|
@@ -216,12 +314,12 @@ function buildSections(root, files, stacks, commands, scannedAt) {
|
|
|
216
314
|
}
|
|
217
315
|
|
|
218
316
|
if (commands.length > 0) {
|
|
219
|
-
sections.commands = makeSection(".oris-flow/maps/commands.md", `Detected ${commands.length}
|
|
317
|
+
sections.commands = makeSection(".oris-flow/maps/commands.md", `Detected ${commands.length} command(s).`, {
|
|
220
318
|
scannedAt,
|
|
221
319
|
markdown: [
|
|
222
320
|
"# Commands",
|
|
223
321
|
"",
|
|
224
|
-
...commands.map((command) => `- \`${command.command}\` (${command.evidence})`),
|
|
322
|
+
...commands.map((command) => `- \`${command.command}\` (${command.evidence || command.confidence}${command.confidence === "inferred" ? ", inferred — confirm before relying on it" : ""})`),
|
|
225
323
|
"",
|
|
226
324
|
].join("\n"),
|
|
227
325
|
});
|
|
@@ -261,12 +359,67 @@ function buildSections(root, files, stacks, commands, scannedAt) {
|
|
|
261
359
|
return sections;
|
|
262
360
|
}
|
|
263
361
|
|
|
362
|
+
function resolveTasksRoot(root, existingManifest) {
|
|
363
|
+
const configured = existingManifest?.setup?.tasksRoot;
|
|
364
|
+
if (typeof configured === "string" && configured.length > 0) return configured;
|
|
365
|
+
const legacy = path.join(root, "docs", "tasks");
|
|
366
|
+
if (fs.existsSync(legacy) && fs.readdirSync(legacy).length > 0) return "docs/tasks";
|
|
367
|
+
return ORIS_FLOW_TASKS_ROOT;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Refresh keeps user truth: confirmed decisions, tasksRoot, version control,
|
|
372
|
+
* refresh policy, confirmed commands, and confirmed sections (whose map files
|
|
373
|
+
* are also left untouched). A confirmed section the scan no longer sees is
|
|
374
|
+
* marked stale, never deleted.
|
|
375
|
+
*/
|
|
376
|
+
function mergeWithExisting(scan, existingManifest) {
|
|
377
|
+
if (!existingManifest || typeof existingManifest !== "object") return scan;
|
|
378
|
+
const merged = scan;
|
|
379
|
+
merged.manifest.generatedAt = existingManifest.generatedAt ?? merged.manifest.generatedAt;
|
|
380
|
+
if (Array.isArray(existingManifest.setup?.confirmedDecisions)) {
|
|
381
|
+
merged.manifest.setup.confirmedDecisions = existingManifest.setup.confirmedDecisions;
|
|
382
|
+
}
|
|
383
|
+
if (existingManifest.setup?.versionControl?.orisFlowMap) {
|
|
384
|
+
merged.manifest.setup.versionControl.orisFlowMap = existingManifest.setup.versionControl.orisFlowMap;
|
|
385
|
+
}
|
|
386
|
+
if (existingManifest.refreshPolicy && typeof existingManifest.refreshPolicy === "object") {
|
|
387
|
+
merged.manifest.refreshPolicy = existingManifest.refreshPolicy;
|
|
388
|
+
}
|
|
389
|
+
if (existingManifest.projectType?.confidence === "confirmed") {
|
|
390
|
+
merged.manifest.projectType = existingManifest.projectType;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const confirmedCommands = (existingManifest.commands ?? []).filter((command) => command?.confidence === "confirmed");
|
|
394
|
+
if (confirmedCommands.length > 0) {
|
|
395
|
+
const scanned = merged.manifest.commands.filter((command) =>
|
|
396
|
+
!confirmedCommands.some((confirmed) => confirmed.name === command.name && confirmed.cwd === command.cwd));
|
|
397
|
+
merged.manifest.commands = [...confirmedCommands, ...scanned];
|
|
398
|
+
merged.commands = merged.manifest.commands;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
merged.preservedSections = new Set();
|
|
402
|
+
for (const [key, pointer] of Object.entries(existingManifest.sections ?? {})) {
|
|
403
|
+
if (pointer?.confidence !== "confirmed") continue;
|
|
404
|
+
const rescanned = merged.manifest.sections[key];
|
|
405
|
+
merged.manifest.sections[key] = {
|
|
406
|
+
...pointer,
|
|
407
|
+
stale: rescanned ? pointer.stale === true : true,
|
|
408
|
+
};
|
|
409
|
+
merged.preservedSections.add(key);
|
|
410
|
+
if (merged.sections[key]) delete merged.sections[key];
|
|
411
|
+
}
|
|
412
|
+
return merged;
|
|
413
|
+
}
|
|
414
|
+
|
|
264
415
|
export function scanRepository(repositoryRoot, options = {}) {
|
|
265
416
|
const root = path.resolve(repositoryRoot);
|
|
266
417
|
const scannedAt = new Date().toISOString();
|
|
267
|
-
const
|
|
418
|
+
const existingManifest = readJson(path.join(root, ...ORIS_FLOW_MANIFEST.split("/")));
|
|
419
|
+
const { files, truncated } = walk(root, options);
|
|
268
420
|
const stacks = detectStacks(files);
|
|
269
|
-
const
|
|
421
|
+
const projectType = detectProjectType(files, stacks);
|
|
422
|
+
const commands = [...findPackageCommands(files), ...deriveStackCommands(stacks, files)];
|
|
270
423
|
const sections = buildSections(root, files, stacks, commands, scannedAt);
|
|
271
424
|
const sectionPointers = Object.fromEntries(
|
|
272
425
|
Object.entries(sections).map(([key, value]) => [key, value.pointer]),
|
|
@@ -277,9 +430,12 @@ export function scanRepository(repositoryRoot, options = {}) {
|
|
|
277
430
|
repository: path.basename(root),
|
|
278
431
|
generatedAt: scannedAt,
|
|
279
432
|
updatedAt: scannedAt,
|
|
433
|
+
projectType,
|
|
434
|
+
truncated,
|
|
280
435
|
setup: {
|
|
281
|
-
mode: options.mode ?? "create",
|
|
436
|
+
mode: options.mode ?? (existingManifest ? "refresh" : "create"),
|
|
282
437
|
scanDepth: options.scanDepth ?? "balanced",
|
|
438
|
+
tasksRoot: options.tasksRoot ?? resolveTasksRoot(root, existingManifest),
|
|
283
439
|
confirmedDecisions: [],
|
|
284
440
|
versionControl: {
|
|
285
441
|
orisFlowMap: options.versionControl ?? "commit",
|
|
@@ -292,27 +448,35 @@ export function scanRepository(repositoryRoot, options = {}) {
|
|
|
292
448
|
commands,
|
|
293
449
|
sections: sectionPointers,
|
|
294
450
|
};
|
|
295
|
-
|
|
451
|
+
const scan = {
|
|
296
452
|
repositoryRoot: root,
|
|
297
453
|
scannedAt,
|
|
298
454
|
fileCount: files.length,
|
|
455
|
+
truncated,
|
|
456
|
+
projectType,
|
|
299
457
|
stacks,
|
|
300
458
|
commands,
|
|
301
459
|
sections,
|
|
302
460
|
manifest,
|
|
461
|
+
preservedSections: new Set(),
|
|
303
462
|
};
|
|
463
|
+
return mergeWithExisting(scan, existingManifest);
|
|
304
464
|
}
|
|
305
465
|
|
|
306
|
-
export function writeScanResult(scan) {
|
|
466
|
+
export function writeScanResult(scan, options = {}) {
|
|
307
467
|
const root = scan.repositoryRoot;
|
|
468
|
+
const area = options.area ?? "";
|
|
308
469
|
const manifestPath = resolveInsideRepository(root, ORIS_FLOW_MANIFEST, "Oris Flow manifest");
|
|
309
470
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
310
471
|
fs.writeFileSync(manifestPath, `${JSON.stringify(scan.manifest, null, 2)}\n`, "utf8");
|
|
311
472
|
|
|
312
|
-
|
|
473
|
+
const written = [];
|
|
474
|
+
for (const [key, section] of Object.entries(scan.sections)) {
|
|
475
|
+
if (area && key !== area) continue;
|
|
313
476
|
const sectionPath = resolveInsideRepository(root, section.pointer.path, "Oris Flow section");
|
|
314
477
|
fs.mkdirSync(path.dirname(sectionPath), { recursive: true });
|
|
315
478
|
fs.writeFileSync(sectionPath, section.markdown, "utf8");
|
|
479
|
+
written.push(section.pointer.path);
|
|
316
480
|
}
|
|
317
481
|
|
|
318
482
|
const readmePath = resolveInsideRepository(root, `${ORIS_FLOW_MAPS_ROOT}/README.md`, "Oris Flow map README");
|
|
@@ -322,17 +486,21 @@ export function writeScanResult(scan) {
|
|
|
322
486
|
}
|
|
323
487
|
return {
|
|
324
488
|
manifestPath: relativeToRepository(root, manifestPath),
|
|
325
|
-
sectionPaths:
|
|
489
|
+
sectionPaths: written,
|
|
490
|
+
preservedSections: [...(scan.preservedSections ?? [])],
|
|
326
491
|
};
|
|
327
492
|
}
|
|
328
493
|
|
|
329
494
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
330
495
|
const options = parseArgs(process.argv.slice(2));
|
|
331
496
|
const scan = scanRepository(options.repositoryRoot, options);
|
|
332
|
-
const written = options.write ? writeScanResult(scan) : null;
|
|
497
|
+
const written = options.write ? writeScanResult(scan, { area: options.area }) : null;
|
|
333
498
|
const payload = {
|
|
334
499
|
repositoryRoot: toPosixPath(scan.repositoryRoot),
|
|
335
500
|
fileCount: scan.fileCount,
|
|
501
|
+
truncated: scan.truncated,
|
|
502
|
+
projectType: scan.projectType,
|
|
503
|
+
tasksRoot: scan.manifest.setup.tasksRoot,
|
|
336
504
|
stacks: scan.stacks,
|
|
337
505
|
commands: scan.commands,
|
|
338
506
|
sections: scan.manifest.sections,
|
|
@@ -342,9 +510,9 @@ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToP
|
|
|
342
510
|
console.log(JSON.stringify(payload, null, 2));
|
|
343
511
|
} else {
|
|
344
512
|
console.log(`Oris Flow scan: ${payload.repositoryRoot}`);
|
|
345
|
-
console.log(`Files scanned: ${payload.fileCount}`);
|
|
513
|
+
console.log(`Files scanned: ${payload.fileCount}${payload.truncated ? " (truncated)" : ""}`);
|
|
514
|
+
console.log(`Project type: ${payload.projectType.label}`);
|
|
346
515
|
console.log(`Stacks: ${payload.stacks.map((stack) => stack.label).join(", ") || "none"}`);
|
|
347
516
|
console.log(`Sections: ${Object.keys(payload.sections).join(", ") || "none"}`);
|
|
348
517
|
}
|
|
349
518
|
}
|
|
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
|
|
|
@@ -19,6 +19,7 @@ import os from "node:os";
|
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import process from "node:process";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { writeAgentAdapters } from "./generate-agent-adapters.mjs";
|
|
22
23
|
|
|
23
24
|
const markerName = ".oris-skills-managed.json";
|
|
24
25
|
const installManifestName = ".oris-skills-install.json";
|
|
@@ -225,6 +226,7 @@ function main() {
|
|
|
225
226
|
for (const agent of agents) {
|
|
226
227
|
if (agent === "codex") log(`Would write Codex prompts: ${path.join(options.agentRoots.codex, "prompts")}`);
|
|
227
228
|
else log(`Would write ${agent} skills: ${path.join(options.agentRoots[agent], "skills")}`);
|
|
229
|
+
log(`Would write ${agent} loop agents: ${path.join(options.agentRoots[agent], "agents")}`);
|
|
228
230
|
}
|
|
229
231
|
log(`Would create settings if missing: ${options.settingsPath}`);
|
|
230
232
|
return;
|
|
@@ -251,6 +253,10 @@ function main() {
|
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
|
|
256
|
+
const adapterTargets = Object.fromEntries(agents.map((agent) => [agent, options.agentRoots[agent]]));
|
|
257
|
+
const loopAgents = writeAgentAdapters(adapterTargets);
|
|
258
|
+
log(`Installed ${loopAgents.length} loop agent adapters (${agents.join(", ")})`);
|
|
259
|
+
|
|
254
260
|
if (!fs.existsSync(options.settingsPath)) writeJson(options.settingsPath, defaultSettings());
|
|
255
261
|
writeJson(path.join(options.bundleRoot, installManifestName), {
|
|
256
262
|
package: manifest.name,
|
|
@@ -260,7 +266,7 @@ function main() {
|
|
|
260
266
|
installedAt: new Date().toISOString(),
|
|
261
267
|
});
|
|
262
268
|
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");
|
|
269
|
+
log("Next: type /oris-flow in your agent chat, or try the loop demo: npx oris-skills loop demo");
|
|
264
270
|
}
|
|
265
271
|
|
|
266
272
|
try {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ORIS_FLOW_LOOPS_ROOT,
|
|
8
8
|
ORIS_FLOW_MANIFEST,
|
|
9
9
|
ORIS_FLOW_RUNTIME,
|
|
10
|
+
ORIS_FLOW_TASKS_ROOT,
|
|
10
11
|
ORIS_LOOP_SCHEMA,
|
|
11
12
|
ORIS_LOOP_SCHEMA_VERSION,
|
|
12
13
|
} from "../flow/oris-flow-layout.mjs";
|
|
@@ -135,9 +136,10 @@ export function buildAdapterDraft(repositoryRoot, hints = {}) {
|
|
|
135
136
|
".oris-flow/maps",
|
|
136
137
|
...(Array.isArray(hints.allowed) ? hints.allowed : []),
|
|
137
138
|
]);
|
|
138
|
-
for (const optionalPath of ["docs", "docs/tasks", "specs", "src", "test", "tests"]) {
|
|
139
|
+
for (const optionalPath of ["docs", "docs/tasks", ORIS_FLOW_TASKS_ROOT, "specs", "src", "test", "tests"]) {
|
|
139
140
|
if (fs.existsSync(path.join(repositoryRoot, optionalPath))) allowed.add(optionalPath);
|
|
140
141
|
}
|
|
142
|
+
allowed.add(ORIS_FLOW_TASKS_ROOT);
|
|
141
143
|
if (testDir) allowed.add(testDir);
|
|
142
144
|
|
|
143
145
|
const commands = {
|
|
@@ -155,7 +157,10 @@ export function buildAdapterDraft(repositoryRoot, hints = {}) {
|
|
|
155
157
|
schemaVersion: ORIS_LOOP_SCHEMA_VERSION,
|
|
156
158
|
repository,
|
|
157
159
|
paths: {
|
|
158
|
-
taskRoots:
|
|
160
|
+
taskRoots: [
|
|
161
|
+
ORIS_FLOW_TASKS_ROOT,
|
|
162
|
+
...(fs.existsSync(path.join(repositoryRoot, "docs", "tasks")) ? ["docs/tasks"] : []),
|
|
163
|
+
],
|
|
159
164
|
runtime: ORIS_FLOW_RUNTIME,
|
|
160
165
|
loops: ORIS_FLOW_LOOPS_ROOT,
|
|
161
166
|
allowed: [...allowed],
|