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.
Files changed (62) hide show
  1. package/.cursor-plugin/plugin.json +6 -1
  2. package/CHANGELOG.md +38 -0
  3. package/README.md +11 -10
  4. package/agents/oris-loop-doctor.md +1 -1
  5. package/agents/oris-loop-verifier.md +3 -0
  6. package/docs/architecture.md +4 -3
  7. package/docs/distribution.md +1 -1
  8. package/docs/maintainer-guide.md +2 -2
  9. package/docs/user-guide.md +9 -7
  10. package/package.json +2 -3
  11. package/references/clean-code-checklist.md +20 -107
  12. package/references/conventions.md +16 -2
  13. package/references/doc-policy.md +19 -3
  14. package/references/loop-contract.md +23 -16
  15. package/references/loop.schema.json +13 -0
  16. package/references/repo-map.md +3 -3
  17. package/references/repo-map.schema.json +37 -0
  18. package/scripts/flow/oris-flow-layout.mjs +1 -0
  19. package/scripts/flow/oris-flow-scan.mjs +195 -27
  20. package/scripts/install/generate-agent-adapters.mjs +19 -2
  21. package/scripts/install/install-user-skills.mjs +7 -1
  22. package/scripts/loop/oris-loop-bootstrap.mjs +7 -2
  23. package/scripts/loop/oris-loop-chat.mjs +32 -14
  24. package/scripts/loop/oris-loop-demo.mjs +11 -20
  25. package/scripts/loop/oris-loop-document.mjs +24 -7
  26. package/scripts/loop/oris-loop-dry-run.mjs +7 -3
  27. package/scripts/loop/oris-loop-fixtures.mjs +12 -11
  28. package/scripts/loop/oris-loop-run.mjs +80 -20
  29. package/scripts/loop/oris-loop-stop.mjs +48 -9
  30. package/scripts/loop/oris-loop-templates.mjs +10 -7
  31. package/scripts/loop/oris-loop-verify.mjs +1 -2
  32. package/scripts/oris-skills.mjs +2 -1
  33. package/scripts/tests/run-all-tests.mjs +0 -2
  34. package/scripts/tests/test-oris-flow-scan.mjs +66 -0
  35. package/scripts/tests/test-oris-loop-document.mjs +39 -3
  36. package/scripts/tests/test-oris-loop-run.mjs +28 -2
  37. package/scripts/tests/test-oris-loop-smoke.mjs +36 -3
  38. package/scripts/tests/test-oris-loop-stop.mjs +65 -4
  39. package/scripts/tests/test-routing-lifecycle.mjs +11 -9
  40. package/skills/oris-flow/SKILL.md +16 -3
  41. package/skills/oris-flow-architecture/SKILL.md +64 -0
  42. package/skills/oris-flow-change/SKILL.md +55 -0
  43. package/skills/oris-flow-criteria/SKILL.md +10 -4
  44. package/skills/oris-flow-discover/SKILL.md +10 -5
  45. package/skills/oris-flow-docs/SKILL.md +2 -2
  46. package/skills/oris-flow-fix/SKILL.md +1 -1
  47. package/skills/oris-flow-implement/SKILL.md +2 -2
  48. package/skills/oris-flow-merge/SKILL.md +45 -0
  49. package/skills/oris-flow-new/SKILL.md +58 -0
  50. package/skills/oris-flow-plan/SKILL.md +8 -4
  51. package/skills/oris-flow-setup/SKILL.md +54 -23
  52. package/skills/oris-flow-verify/SKILL.md +50 -0
  53. package/skills/oris-help/SKILL.md +4 -4
  54. package/skills/oris-loop/SKILL.md +12 -32
  55. package/skills/oris-loop/references/craft.md +10 -9
  56. package/skills/oris-loop/references/run.md +9 -7
  57. package/skills/oris-loop/templates/debriefer.md +1 -1
  58. package/skills/oris-loop/templates/doctor.md +9 -7
  59. package/skills/oris-loop/templates/executor.md +3 -2
  60. package/skills/oris-loop/templates/orchestrator.md +10 -7
  61. package/skills/oris-loop/templates/verifier.md +4 -2
  62. 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
- #!/usr/bin/env node
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 stack = [root];
84
- while (stack.length > 0 && files.length < maxFiles) {
85
- const current = stack.pop();
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
- stack.push(full);
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
- if (hasAny(files, (file) => file.relative.startsWith(".cursor/skills/") || file.relative.startsWith(".cursor/rules/"))) {
189
- const cursorFiles = topFiles.filter((file) => file.startsWith(".cursor/skills/") || file.startsWith(".cursor/rules/")).slice(0, 20);
190
- sections.cursorGuidance = makeSection(".oris-flow/maps/cursor-guidance.md", "Project-local Cursor skills or rules detected.", {
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
- "# Cursor Guidance",
291
+ "# Agent Guidance",
194
292
  "",
195
- "Detected local Cursor guidance:",
196
- ...cursorFiles.map((file) => `- ${file}`),
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} package command(s).`, {
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 files = walk(root, options);
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 commands = findPackageCommands(files);
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
- return {
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
- for (const section of Object.values(scan.sections)) {
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: Object.values(scan.sections).map((section) => section.pointer.path),
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
- export function generateAgentAdapters(root = repoRoot) {
56
- const agents = fs.readdirSync(sourceRoot)
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: fs.existsSync(path.join(repositoryRoot, "docs", "tasks")) ? ["docs/tasks"] : [],
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],