oris-skills 2.2.2 → 3.0.1

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 (86) hide show
  1. package/.cursor-plugin/plugin.json +2 -3
  2. package/CHANGELOG.md +24 -6
  3. package/README.md +23 -59
  4. package/docs/architecture.md +15 -11
  5. package/docs/distribution.md +3 -3
  6. package/docs/maintainer-guide.md +4 -4
  7. package/docs/user-guide.md +12 -20
  8. package/package.json +3 -6
  9. package/references/conventions.md +23 -13
  10. package/references/devops.md +58 -0
  11. package/references/devops.schema.json +54 -0
  12. package/references/doc-policy.md +5 -5
  13. package/references/research.md +45 -0
  14. package/references/settings.md +11 -5
  15. package/references/settings.schema.json +10 -0
  16. package/scripts/flow/oris-flow-devops.mjs +77 -0
  17. package/scripts/flow/oris-flow-layout.mjs +0 -16
  18. package/scripts/flow/oris-flow-scan.mjs +32 -317
  19. package/scripts/flow/oris-gitignore.mjs +1 -5
  20. package/scripts/install/install-user-skills.mjs +17 -3
  21. package/scripts/install/oris-mcp.mjs +68 -0
  22. package/scripts/install/uninstall-user-skills.mjs +2 -29
  23. package/scripts/oris-skills.mjs +0 -47
  24. package/scripts/tests/run-all-tests.mjs +3 -9
  25. package/scripts/tests/test-cleanliness.mjs +54 -0
  26. package/scripts/tests/test-install-mcp.mjs +66 -0
  27. package/scripts/tests/test-oris-flow-devops.mjs +64 -0
  28. package/scripts/tests/test-oris-flow-scan.mjs +21 -91
  29. package/scripts/tests/test-routing-lifecycle.mjs +22 -53
  30. package/scripts/tests/test-schemas.mjs +17 -31
  31. package/scripts/tests/test-skill-style.mjs +5 -2
  32. package/skills/oris-flow/SKILL.md +10 -6
  33. package/skills/oris-flow/references/architecture.md +1 -1
  34. package/skills/oris-flow/references/commit.md +46 -0
  35. package/skills/oris-flow/references/criteria.md +2 -2
  36. package/skills/oris-flow/references/discover.md +2 -2
  37. package/skills/oris-flow/references/fix.md +7 -3
  38. package/skills/oris-flow/references/help.md +1 -6
  39. package/skills/oris-flow/references/implement.md +5 -3
  40. package/skills/oris-flow/references/new.md +6 -6
  41. package/skills/oris-flow/references/plan.md +7 -2
  42. package/skills/oris-flow/references/pr.md +44 -0
  43. package/skills/oris-flow/references/setup.md +28 -35
  44. package/skills/oris-flow/references/verify.md +4 -5
  45. package/agents/oris-loop-debriefer.md +0 -30
  46. package/agents/oris-loop-doctor.md +0 -32
  47. package/agents/oris-loop-executor.md +0 -35
  48. package/agents/oris-loop-verifier.md +0 -35
  49. package/references/loop-adapter.schema.json +0 -93
  50. package/references/loop-contract.md +0 -126
  51. package/references/loop.schema.json +0 -156
  52. package/references/repo-map.md +0 -51
  53. package/references/repo-map.schema.json +0 -213
  54. package/scripts/flow/oris-flow-clean-runtime.mjs +0 -182
  55. package/scripts/install/generate-agent-adapters.mjs +0 -81
  56. package/scripts/loop/oris-loop-bootstrap.mjs +0 -383
  57. package/scripts/loop/oris-loop-bundle.mjs +0 -22
  58. package/scripts/loop/oris-loop-chat.mjs +0 -396
  59. package/scripts/loop/oris-loop-demo.mjs +0 -171
  60. package/scripts/loop/oris-loop-document.mjs +0 -196
  61. package/scripts/loop/oris-loop-dry-run.mjs +0 -114
  62. package/scripts/loop/oris-loop-fixtures.mjs +0 -149
  63. package/scripts/loop/oris-loop-list.mjs +0 -81
  64. package/scripts/loop/oris-loop-paths.mjs +0 -232
  65. package/scripts/loop/oris-loop-run.mjs +0 -301
  66. package/scripts/loop/oris-loop-stop.mjs +0 -358
  67. package/scripts/loop/oris-loop-templates.mjs +0 -80
  68. package/scripts/loop/oris-loop-verify.mjs +0 -205
  69. package/scripts/tests/test-agent-adapters.mjs +0 -70
  70. package/scripts/tests/test-oris-1-0-cleanliness.mjs +0 -58
  71. package/scripts/tests/test-oris-flow-clean-runtime.mjs +0 -75
  72. package/scripts/tests/test-oris-loop-bootstrap.mjs +0 -94
  73. package/scripts/tests/test-oris-loop-document.mjs +0 -148
  74. package/scripts/tests/test-oris-loop-list.mjs +0 -74
  75. package/scripts/tests/test-oris-loop-run.mjs +0 -71
  76. package/scripts/tests/test-oris-loop-smoke.mjs +0 -120
  77. package/scripts/tests/test-oris-loop-stop.mjs +0 -432
  78. package/skills/oris-flow/references/loop-craft.md +0 -59
  79. package/skills/oris-flow/references/loop-improve.md +0 -32
  80. package/skills/oris-flow/references/loop-run.md +0 -47
  81. package/skills/oris-flow/references/loop.md +0 -56
  82. package/skills/oris-flow/templates/debriefer.md +0 -19
  83. package/skills/oris-flow/templates/doctor.md +0 -22
  84. package/skills/oris-flow/templates/executor.md +0 -25
  85. package/skills/oris-flow/templates/orchestrator.md +0 -36
  86. package/skills/oris-flow/templates/verifier.md +0 -24
@@ -1,24 +1,30 @@
1
1
  # Oris Settings
2
2
 
3
- Per-project preferences. Path: `.oris-flow/settings.json`. Small and non-secret
4
- chat verbosity and document language, nothing else. Missing file defaults below.
5
- Created/edited via `/oris-flow` → Setup, or by hand.
3
+ Per-project preferences. Path: `.oris-flow/settings.json`. Small and non-secret.
4
+ Missing file defaults below. Created/edited via `/oris-flow`Setup, or by hand.
6
5
 
7
6
  ## Shape (v1)
8
7
 
9
8
  ```json
10
9
  {
11
10
  "version": 1,
12
- "orisFlow": { "responseStyle": "standard", "artifactLanguage": "en" }
11
+ "orisFlow": {
12
+ "responseStyle": "standard",
13
+ "artifactLanguage": "en",
14
+ "tasksRoot": ".oris-flow/tasks",
15
+ "versionControl": "commit"
16
+ }
13
17
  }
14
18
  ```
15
19
 
16
20
  - `responseStyle` → chat verbosity (`standard` | `caveman`, default `standard`). Caveman = maximum token density in chat; contract in `references/conventions.md` `## Response style`. Documents and gates are never compressed.
17
21
  - `artifactLanguage` → language for generated documents (ISO code, default `en`). Chat/UI language is NOT configured here — it is deduced from the conversation.
22
+ - `tasksRoot` → where task documents live (default `.oris-flow/tasks`; legacy `docs/tasks` supported when a team already treats task docs as project docs).
23
+ - `versionControl` → whether `.oris-flow/` is committed (`commit`, team default) or ignored (`gitignore`, personal). Applied via `npx oris-skills flow version-control --mode <choice>`.
18
24
 
19
25
  ## Rules
20
26
 
21
27
  1. READ settings before the first workflow decision; missing/invalid → defaults above.
22
28
  2. Current user instructions override settings for the turn.
23
- 3. Oris stores no credentials. Test/CI secrets belong to each project's own tooling — never Oris files, docs, loop state, receipts, or logs.
29
+ 3. Oris stores no credentials. Test/CI secrets belong to each project's own tooling — never Oris files, docs, or logs.
24
30
  4. CONFIRM before writing settings. Update via `/oris-flow` → Setup.
@@ -23,6 +23,16 @@
23
23
  "type": "string",
24
24
  "minLength": 2,
25
25
  "description": "Language for generated documents (ISO code, e.g. en, it). Default en. Chat language is deduced from the conversation, not configured here."
26
+ },
27
+ "tasksRoot": {
28
+ "type": "string",
29
+ "minLength": 1,
30
+ "description": "Repository-relative root for task documents. Default .oris-flow/tasks; legacy docs/tasks supported."
31
+ },
32
+ "versionControl": {
33
+ "type": "string",
34
+ "enum": ["commit", "gitignore"],
35
+ "description": "Whether .oris-flow/ is committed (team default) or gitignored (personal). Default commit."
26
36
  }
27
37
  }
28
38
  }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ /**
8
+ * Built-in Oris default DevOps provider. Non-secret only — org and repo layers override it.
9
+ * The provider is swappable: move off Azure DevOps by overriding `provider` and `mcp`.
10
+ */
11
+ export const DEFAULT_DEVOPS = {
12
+ version: 1,
13
+ devops: {
14
+ provider: "azure-devops",
15
+ organization: "orislineweb",
16
+ workItemSyntax: "#{id}",
17
+ commitLanguage: "en",
18
+ prLanguage: "en",
19
+ mcp: {
20
+ name: "oris-devops",
21
+ package: "@azure-devops/mcp@2.7.0",
22
+ domains: ["core", "work", "work-items", "repositories"],
23
+ },
24
+ },
25
+ };
26
+
27
+ function isPlainObject(value) {
28
+ return value !== null && typeof value === "object" && !Array.isArray(value);
29
+ }
30
+
31
+ /** Deep-merge source over target; arrays and scalars replace, plain objects merge. */
32
+ function deepMerge(target, source) {
33
+ if (!isPlainObject(source)) return source;
34
+ const out = isPlainObject(target) ? { ...target } : {};
35
+ for (const [key, value] of Object.entries(source)) {
36
+ out[key] = isPlainObject(value) ? deepMerge(out[key], value) : value;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function readJson(filePath) {
42
+ try {
43
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Resolve the effective DevOps config: built-in default → org (`~/.oris/devops.json`) →
51
+ * repository (`.oris-flow/devops.json`). Missing or invalid layers are skipped, never fatal.
52
+ */
53
+ export function resolveDevops(repositoryRoot, options = {}) {
54
+ const home = options.home ?? os.homedir();
55
+ const orgFile = readJson(path.join(home, ".oris", "devops.json"));
56
+ const repoFile = repositoryRoot ? readJson(path.join(repositoryRoot, ".oris-flow", "devops.json")) : null;
57
+ let resolved = DEFAULT_DEVOPS;
58
+ for (const layer of [orgFile, repoFile]) {
59
+ if (isPlainObject(layer)) resolved = deepMerge(resolved, layer);
60
+ }
61
+ return resolved;
62
+ }
63
+
64
+ /**
65
+ * Build the MCP server entry for an agent config from a resolved `devops` block. Azure
66
+ * DevOps command shape by default; any provider can supply explicit `mcp.args` instead.
67
+ */
68
+ export function buildMcpServer(devops) {
69
+ const mcp = devops.mcp ?? {};
70
+ if (Array.isArray(mcp.args)) return { command: mcp.command ?? "npx", args: [...mcp.args] };
71
+ const domains = Array.isArray(mcp.domains) ? mcp.domains : [];
72
+ return { command: "npx", args: ["-y", mcp.package, devops.organization, "-d", ...domains] };
73
+ }
74
+
75
+ export function mcpServerName(devops) {
76
+ return devops.mcp?.name ?? "oris-devops";
77
+ }
@@ -2,31 +2,15 @@ import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
4
  export const ORIS_FLOW_ROOT = ".oris-flow";
5
- export const ORIS_FLOW_MANIFEST = ".oris-flow/manifest.json";
6
- export const ORIS_FLOW_MAPS_ROOT = ".oris-flow/maps";
7
5
  export const ORIS_FLOW_TASKS_ROOT = ".oris-flow/tasks";
8
- export const ORIS_FLOW_ADAPTER = ".oris-flow/adapter.json";
9
- export const ORIS_FLOW_LOOPS_ROOT = ".oris-flow/loops";
10
- export const ORIS_FLOW_RUNTIME = ".oris-flow/runtime";
11
6
  export const ORIS_FLOW_SETTINGS = ".oris-flow/settings.json";
12
7
  export const ORIS_HOME = path.join(os.homedir(), ".oris");
13
8
  export const ORIS_BUNDLE_ROOT = path.join(ORIS_HOME, "oris-skills");
14
- export const ORIS_LOOP_SCHEMA = "https://oris.dev/schemas/oris-loop-adapter-v1.json";
15
- export const ORIS_LOOP_SCHEMA_VERSION = 1;
16
- export const ORIS_LOOP_DOCUMENT_VERSION = 2;
17
- export const ORIS_FLOW_MAP_SCHEMA = "https://oris.dev/schemas/oris-flow-map-v1.json";
18
- export const ORIS_FLOW_MAP_SCHEMA_VERSION = 1;
19
9
 
20
10
  export function toPosixPath(value) {
21
11
  return String(value).replace(/\\/g, "/");
22
12
  }
23
13
 
24
- export function safeSlug(value, fallback = "loop") {
25
- const raw = path.basename(toPosixPath(value || fallback));
26
- const slug = raw.replace(/[^A-Za-z0-9_.-]/g, "-").replace(/^-+|-+$/g, "");
27
- return slug || fallback;
28
- }
29
-
30
14
  export function assertRepositoryRelative(value, label) {
31
15
  const normalized = toPosixPath(value ?? "");
32
16
  if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) {
@@ -1,20 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import childProcess from "node:child_process";
3
+ /**
4
+ * One-shot repository detector for the setup route. Reports evidence — project
5
+ * type, stacks, commands, notable areas — and writes NOTHING. Durable facts the
6
+ * user confirms belong in AGENTS.md; preferences belong in .oris-flow/settings.json.
7
+ */
8
+
4
9
  import fs from "node:fs";
5
10
  import path from "node:path";
6
11
  import process from "node:process";
7
12
  import { fileURLToPath } from "node:url";
8
- import {
9
- ORIS_FLOW_MANIFEST,
10
- ORIS_FLOW_MAP_SCHEMA,
11
- ORIS_FLOW_MAP_SCHEMA_VERSION,
12
- ORIS_FLOW_MAPS_ROOT,
13
- ORIS_FLOW_TASKS_ROOT,
14
- relativeToRepository,
15
- resolveInsideRepository,
16
- toPosixPath,
17
- } from "./oris-flow-layout.mjs";
13
+ import { relativeToRepository, toPosixPath } from "./oris-flow-layout.mjs";
18
14
 
19
15
  const ignoredDirectories = new Set([
20
16
  ".oris-flow",
@@ -64,74 +60,10 @@ function fail(message) {
64
60
  process.exit(1);
65
61
  }
66
62
 
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
-
129
63
  function parseArgs(argv) {
130
64
  const options = {
131
65
  repositoryRoot: process.cwd(),
132
- write: false,
133
66
  json: false,
134
- area: "",
135
67
  };
136
68
  const readValue = (index, raw) => {
137
69
  if (raw.includes("=")) return [index, raw.slice(raw.indexOf("=") + 1)];
@@ -142,10 +74,7 @@ function parseArgs(argv) {
142
74
  const raw = argv[i];
143
75
  const key = raw.toLowerCase();
144
76
  if (key === "--repository-root" || key.startsWith("--repository-root=")) [i, options.repositoryRoot] = readValue(i, raw);
145
- else if (key === "--write") options.write = true;
146
77
  else if (key === "--json") options.json = true;
147
- else if (key === "--area" || key.startsWith("--area=")) [i, options.area] = readValue(i, raw);
148
- else if (key === "--check") options.check = true;
149
78
  else fail(`Unknown argument: ${raw}`);
150
79
  }
151
80
  return options;
@@ -309,274 +238,60 @@ function deriveStackCommands(stacks, files) {
309
238
  return commands;
310
239
  }
311
240
 
312
- function hasAny(files, predicate) {
313
- return files.some(predicate);
314
- }
241
+ /** Notable areas the setup interview walks through — evidence pointers, never a stored map. */
242
+ function detectAreas(files) {
243
+ const paths = files.map((file) => file.relative);
244
+ const areas = {};
315
245
 
316
- function makeSection(pathValue, summary, details) {
317
- return {
318
- pointer: {
319
- path: pathValue,
320
- confidence: details.confidence ?? "detected",
321
- lastScannedAt: details.scannedAt,
322
- stale: false,
323
- summary,
324
- },
325
- markdown: details.markdown,
326
- };
327
- }
328
-
329
- function buildSections(root, files, stacks, commands, scannedAt) {
330
- const sections = {};
331
- const topFiles = files.map((file) => file.relative);
246
+ const docs = paths.filter((file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING|AGENTS|CLAUDE)\.md$/i.test(file) || file.startsWith("docs/")).slice(0, 20);
247
+ if (docs.length > 0) areas.docs = docs;
332
248
 
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);
335
- sections.docs = makeSection(".oris-flow/maps/docs.md", "Documentation and task context detected.", {
336
- scannedAt,
337
- markdown: [
338
- "# Docs",
339
- "",
340
- "Detected documentation paths:",
341
- ...docs.map((file) => `- ${file}`),
342
- "",
343
- ].join("\n"),
344
- });
345
- }
346
-
347
- const guidancePaths = topFiles.filter((file) =>
249
+ const agentGuidance = paths.filter((file) =>
348
250
  file.startsWith(".cursor/skills/") || file.startsWith(".cursor/rules/")
349
251
  || file.startsWith(".claude/skills/") || /^AGENTS\.md$/i.test(file) || /^CLAUDE\.md$/i.test(file)
350
252
  || /^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).", {
353
- scannedAt,
354
- markdown: [
355
- "# Agent Guidance",
356
- "",
357
- "Detected agent-facing guidance:",
358
- ...guidancePaths.map((file) => `- ${file}`),
359
- "",
360
- ].join("\n"),
361
- });
362
- }
253
+ if (agentGuidance.length > 0) areas.agentGuidance = agentGuidance;
363
254
 
364
- if (stacks.length > 0) {
365
- sections.stack = makeSection(".oris-flow/maps/stack.md", `Detected ${stacks.length} stack signal(s).`, {
366
- scannedAt,
367
- markdown: [
368
- "# Stack",
369
- "",
370
- ...stacks.flatMap((stack) => [
371
- `## ${stack.label}`,
372
- "",
373
- ...stack.evidence.map((file) => `- ${file}`),
374
- "",
375
- ]),
376
- ].join("\n"),
377
- });
378
- }
255
+ const code = [...new Set(paths
256
+ .filter((file) => /(^|\/)(src|app|apps|packages)\//.test(file))
257
+ .map((file) => file.split("/").slice(0, 2).join("/")))]
258
+ .slice(0, 20);
259
+ if (code.length > 0) areas.code = code;
379
260
 
380
- if (commands.length > 0) {
381
- sections.commands = makeSection(".oris-flow/maps/commands.md", `Detected ${commands.length} command(s).`, {
382
- scannedAt,
383
- markdown: [
384
- "# Commands",
385
- "",
386
- ...commands.map((command) => `- \`${command.command}\` (${command.evidence || command.confidence}${command.confidence === "inferred" ? ", inferred — confirm before relying on it" : ""})`),
387
- "",
388
- ].join("\n"),
389
- });
390
- }
391
-
392
- if (hasAny(files, (file) => /(^|\/)(src|app|apps|packages)\//.test(file.relative))) {
393
- const codeRoots = [...new Set(topFiles
394
- .filter((file) => /(^|\/)(src|app|apps|packages)\//.test(file))
395
- .map((file) => file.split("/").slice(0, 2).join("/")))]
396
- .slice(0, 20);
397
- sections.code = makeSection(".oris-flow/maps/code.md", "Source code areas detected.", {
398
- scannedAt,
399
- markdown: [
400
- "# Code",
401
- "",
402
- "Detected source areas:",
403
- ...codeRoots.map((file) => `- ${file}`),
404
- "",
405
- ].join("\n"),
406
- });
407
- }
408
-
409
- if (hasAny(files, (file) => /(^|\/)(test|tests|spec|specs|e2e)(\/|$)|\.(test|spec)\.[jt]sx?$/i.test(file.relative))) {
410
- const tests = topFiles.filter((file) => /(^|\/)(test|tests|spec|specs|e2e)(\/|$)|\.(test|spec)\.[jt]sx?$/i.test(file)).slice(0, 20);
411
- sections.tests = makeSection(".oris-flow/maps/tests.md", "Test assets detected.", {
412
- scannedAt,
413
- markdown: [
414
- "# Tests",
415
- "",
416
- "Detected test paths:",
417
- ...tests.map((file) => `- ${file}`),
418
- "",
419
- ].join("\n"),
420
- });
421
- }
261
+ const tests = paths.filter((file) => /(^|\/)(test|tests|spec|specs|e2e)(\/|$)|\.(test|spec)\.[jt]sx?$/i.test(file)).slice(0, 20);
262
+ if (tests.length > 0) areas.tests = tests;
422
263
 
423
- return sections;
424
- }
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
- * confirmed commands, and confirmed sections (whose map files are also left
437
- * untouched). A confirmed section the scan no longer sees is marked stale,
438
- * 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.projectType?.confidence === "confirmed") {
451
- merged.manifest.projectType = existingManifest.projectType;
452
- }
453
-
454
- const confirmedCommands = (existingManifest.commands ?? []).filter((command) => command?.confidence === "confirmed");
455
- if (confirmedCommands.length > 0) {
456
- const scanned = merged.manifest.commands.filter((command) =>
457
- !confirmedCommands.some((confirmed) => confirmed.name === command.name && confirmed.cwd === command.cwd));
458
- merged.manifest.commands = [...confirmedCommands, ...scanned];
459
- merged.commands = merged.manifest.commands;
460
- }
461
-
462
- merged.preservedSections = new Set();
463
- for (const [key, pointer] of Object.entries(existingManifest.sections ?? {})) {
464
- if (pointer?.confidence !== "confirmed") continue;
465
- const rescanned = merged.manifest.sections[key];
466
- merged.manifest.sections[key] = {
467
- ...pointer,
468
- stale: rescanned ? pointer.stale === true : true,
469
- };
470
- merged.preservedSections.add(key);
471
- if (merged.sections[key]) delete merged.sections[key];
472
- }
473
- return merged;
264
+ return areas;
474
265
  }
475
266
 
476
267
  export function scanRepository(repositoryRoot, options = {}) {
477
268
  const root = path.resolve(repositoryRoot);
478
- const scannedAt = new Date().toISOString();
479
- const headSha = gitHeadSha(root);
480
- const existingManifest = readJson(path.join(root, ...ORIS_FLOW_MANIFEST.split("/")));
481
269
  const { files, truncated } = walk(root, options);
482
270
  const stacks = detectStacks(files);
483
- const projectType = detectProjectType(files, stacks);
484
- const commands = [...findPackageCommands(files), ...deriveStackCommands(stacks, files)];
485
- const sections = buildSections(root, files, stacks, commands, scannedAt);
486
- const sectionPointers = Object.fromEntries(
487
- Object.entries(sections).map(([key, value]) => [key, value.pointer]),
488
- );
489
- const manifest = {
490
- $schema: ORIS_FLOW_MAP_SCHEMA,
491
- schemaVersion: ORIS_FLOW_MAP_SCHEMA_VERSION,
492
- repository: path.basename(root),
493
- generatedAt: scannedAt,
494
- updatedAt: scannedAt,
495
- headSha,
496
- projectType,
497
- truncated,
498
- setup: {
499
- mode: options.mode ?? (existingManifest ? "refresh" : "create"),
500
- scanDepth: options.scanDepth ?? "balanced",
501
- tasksRoot: options.tasksRoot ?? resolveTasksRoot(root, existingManifest),
502
- confirmedDecisions: [],
503
- versionControl: {
504
- orisFlowMap: options.versionControl ?? "commit",
505
- },
506
- },
507
- stacks,
508
- commands,
509
- sections: sectionPointers,
510
- };
511
- const scan = {
271
+ return {
512
272
  repositoryRoot: root,
513
- scannedAt,
273
+ scannedAt: new Date().toISOString(),
514
274
  fileCount: files.length,
515
275
  truncated,
516
- projectType,
276
+ projectType: detectProjectType(files, stacks),
517
277
  stacks,
518
- commands,
519
- sections,
520
- manifest,
521
- preservedSections: new Set(),
522
- };
523
- return mergeWithExisting(scan, existingManifest);
524
- }
525
-
526
- export function writeScanResult(scan, options = {}) {
527
- const root = scan.repositoryRoot;
528
- const area = options.area ?? "";
529
- const manifestPath = resolveInsideRepository(root, ORIS_FLOW_MANIFEST, "Oris Flow manifest");
530
- fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
531
- fs.writeFileSync(manifestPath, `${JSON.stringify(scan.manifest, null, 2)}\n`, "utf8");
532
-
533
- const written = [];
534
- for (const [key, section] of Object.entries(scan.sections)) {
535
- if (area && key !== area) continue;
536
- const sectionPath = resolveInsideRepository(root, section.pointer.path, "Oris Flow section");
537
- fs.mkdirSync(path.dirname(sectionPath), { recursive: true });
538
- fs.writeFileSync(sectionPath, section.markdown, "utf8");
539
- written.push(section.pointer.path);
540
- }
541
-
542
- const readmePath = resolveInsideRepository(root, `${ORIS_FLOW_MAPS_ROOT}/README.md`, "Oris Flow map README");
543
- if (!fs.existsSync(readmePath)) {
544
- fs.mkdirSync(path.dirname(readmePath), { recursive: true });
545
- fs.writeFileSync(readmePath, "# Oris Flow Maps\n\nGenerated by `oris-flow-scan.mjs`. Sections are present only when repository evidence supports them.\n", "utf8");
546
- }
547
- return {
548
- manifestPath: relativeToRepository(root, manifestPath),
549
- sectionPaths: written,
550
- preservedSections: [...(scan.preservedSections ?? [])],
278
+ commands: [...findPackageCommands(files), ...deriveStackCommands(stacks, files)],
279
+ areas: detectAreas(files),
551
280
  };
552
281
  }
553
282
 
554
283
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
555
284
  const options = parseArgs(process.argv.slice(2));
556
- if (options.check) {
557
- console.log(JSON.stringify(checkFreshness(options.repositoryRoot), null, 2));
558
- process.exit(0);
559
- }
560
285
  const scan = scanRepository(options.repositoryRoot, options);
561
- const written = options.write ? writeScanResult(scan, { area: options.area }) : null;
562
- const payload = {
563
- repositoryRoot: toPosixPath(scan.repositoryRoot),
564
- fileCount: scan.fileCount,
565
- truncated: scan.truncated,
566
- projectType: scan.projectType,
567
- tasksRoot: scan.manifest.setup.tasksRoot,
568
- stacks: scan.stacks,
569
- commands: scan.commands,
570
- sections: scan.manifest.sections,
571
- written,
572
- };
573
- if (options.json || options.write) {
286
+ const payload = { ...scan, repositoryRoot: toPosixPath(scan.repositoryRoot) };
287
+ if (options.json) {
574
288
  console.log(JSON.stringify(payload, null, 2));
575
289
  } else {
576
290
  console.log(`Oris Flow scan: ${payload.repositoryRoot}`);
577
291
  console.log(`Files scanned: ${payload.fileCount}${payload.truncated ? " (truncated)" : ""}`);
578
292
  console.log(`Project type: ${payload.projectType.label}`);
579
293
  console.log(`Stacks: ${payload.stacks.map((stack) => stack.label).join(", ") || "none"}`);
580
- console.log(`Sections: ${Object.keys(payload.sections).join(", ") || "none"}`);
294
+ console.log(`Commands: ${payload.commands.map((command) => command.command).join(", ") || "none"}`);
295
+ console.log(`Areas: ${Object.keys(payload.areas).join(", ") || "none"}`);
581
296
  }
582
297
  }
@@ -2,13 +2,9 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  export const ORIS_FLOW_GITIGNORE_ENTRIES = [
5
- ".oris-flow/manifest.json",
6
- ".oris-flow/maps/",
5
+ ".oris-flow/",
7
6
  ];
8
7
 
9
- export const ORIS_FLOW_RUNTIME_GITIGNORE_ENTRY = ".oris-flow/runtime/";
10
- export const ORIS_LOOP_GITIGNORE_ENTRY = ORIS_FLOW_RUNTIME_GITIGNORE_ENTRY;
11
-
12
8
  function normalizeGitignoreLines(text) {
13
9
  return text.replace(/\r\n/g, "\n").split("\n");
14
10
  }
@@ -22,6 +22,8 @@ import os from "node:os";
22
22
  import path from "node:path";
23
23
  import process from "node:process";
24
24
  import { fileURLToPath } from "node:url";
25
+ import { buildMcpServer, mcpServerName, resolveDevops } from "../flow/oris-flow-devops.mjs";
26
+ import { MCP_AGENTS, seedMcpServer } from "./oris-mcp.mjs";
25
27
 
26
28
  const markerName = ".oris-skills-managed.json";
27
29
  const installManifestName = ".oris-skills-install.json";
@@ -61,6 +63,7 @@ function parseArgs(argv) {
61
63
  const options = {
62
64
  dryRun: false,
63
65
  force: false,
66
+ noMcp: false,
64
67
  agents: [],
65
68
  bundleRoot: paths.bundleRoot,
66
69
  agentRoots: paths.agentRoots,
@@ -76,6 +79,7 @@ function parseArgs(argv) {
76
79
  };
77
80
  if (key === "--dry-run") options.dryRun = true;
78
81
  else if (key === "--force") options.force = true;
82
+ else if (key === "--no-mcp") options.noMcp = true;
79
83
  else if (key === "--agents" || key.startsWith("--agents=")) {
80
84
  options.agents = value().split(",").map((name) => name.trim().toLowerCase()).filter(Boolean);
81
85
  } else if (key === "--bundle-root" || key.startsWith("--bundle-root=")) options.bundleRoot = value();
@@ -246,7 +250,7 @@ Tell any agent that can read files:
246
250
  > Read \`${bundledSkill}\` and follow it exactly.
247
251
 
248
252
  That file routes every Oris flow (setup, discover, criteria, plan, implement, fix,
249
- verify, change, loop, architecture, docs, help) in the same chat.
253
+ verify, change, architecture, docs, help) in the same chat.
250
254
  `, "utf8");
251
255
  }
252
256
 
@@ -256,6 +260,10 @@ function main() {
256
260
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
257
261
  const manifest = readJson(path.join(repoRoot, ".cursor-plugin", "plugin.json"));
258
262
  const skills = manifestSkills(manifest, repoRoot);
263
+ const devops = resolveDevops(null).devops;
264
+ const mcpServer = buildMcpServer(devops);
265
+ const mcpName = mcpServerName(devops);
266
+ const mcpAgents = options.noMcp ? [] : (options.agents.length > 0 ? options.agents : detectAgents(options.agentRoots)).filter((agent) => MCP_AGENTS.includes(agent));
259
267
  const agents = options.agents.length > 0 ? options.agents : detectAgents(options.agentRoots);
260
268
  if (agents.length === 0) {
261
269
  fail("No agent detected (~/.cursor, ~/.claude, ~/.codex). Pass --agents cursor,claude,codex explicitly.");
@@ -273,6 +281,7 @@ function main() {
273
281
  else log(`Would write ${agent} skills: ${path.join(options.agentRoots[agent], "skills")}`);
274
282
  }
275
283
  log(`Would write universal pointer: ${path.join(path.dirname(options.bundleRoot), "oris-flow.md")}`);
284
+ if (mcpAgents.length > 0) log(`Would seed MCP server '${mcpName}' for: ${mcpAgents.join(", ")}`);
276
285
  return;
277
286
  }
278
287
 
@@ -282,7 +291,7 @@ function main() {
282
291
 
283
292
  fs.rmSync(options.bundleRoot, { recursive: true, force: true });
284
293
  fs.mkdirSync(options.bundleRoot, { recursive: true });
285
- for (const item of [".cursor-plugin", "skills", "references", "agents", "scripts", "docs", "package.json", "README.md", "LICENSE"]) {
294
+ for (const item of [".cursor-plugin", "skills", "references", "scripts", "docs", "package.json", "README.md", "LICENSE"]) {
286
295
  const source = path.join(repoRoot, item);
287
296
  if (fs.existsSync(source)) copy(source, path.join(options.bundleRoot, item));
288
297
  }
@@ -301,6 +310,11 @@ function main() {
301
310
  }
302
311
  installUniversalPointer(skills, options.bundleRoot);
303
312
 
313
+ for (const agent of mcpAgents) {
314
+ const result = seedMcpServer(agent, options.agentRoots, mcpName, mcpServer, { force: options.force });
315
+ log(`MCP ${mcpName} for ${agent}: ${result.status}`);
316
+ }
317
+
304
318
  writeJson(path.join(options.bundleRoot, installManifestName), {
305
319
  package: manifest.name,
306
320
  version: manifest.version,
@@ -309,7 +323,7 @@ function main() {
309
323
  installedAt: new Date().toISOString(),
310
324
  });
311
325
  log(`Done for agents: ${agents.join(", ")}. Reload your agent environment.`);
312
- log("Next: type /oris-flow in your agent chat, or try the loop demo: npx oris-skills loop demo");
326
+ log("Next: type /oris-flow in your agent chat.");
313
327
  }
314
328
 
315
329
  try {