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.
- package/.cursor-plugin/plugin.json +2 -3
- package/CHANGELOG.md +24 -6
- package/README.md +23 -59
- package/docs/architecture.md +15 -11
- package/docs/distribution.md +3 -3
- package/docs/maintainer-guide.md +4 -4
- package/docs/user-guide.md +12 -20
- package/package.json +3 -6
- package/references/conventions.md +23 -13
- package/references/devops.md +58 -0
- package/references/devops.schema.json +54 -0
- package/references/doc-policy.md +5 -5
- package/references/research.md +45 -0
- package/references/settings.md +11 -5
- package/references/settings.schema.json +10 -0
- package/scripts/flow/oris-flow-devops.mjs +77 -0
- package/scripts/flow/oris-flow-layout.mjs +0 -16
- package/scripts/flow/oris-flow-scan.mjs +32 -317
- package/scripts/flow/oris-gitignore.mjs +1 -5
- package/scripts/install/install-user-skills.mjs +17 -3
- package/scripts/install/oris-mcp.mjs +68 -0
- package/scripts/install/uninstall-user-skills.mjs +2 -29
- package/scripts/oris-skills.mjs +0 -47
- package/scripts/tests/run-all-tests.mjs +3 -9
- package/scripts/tests/test-cleanliness.mjs +54 -0
- package/scripts/tests/test-install-mcp.mjs +66 -0
- package/scripts/tests/test-oris-flow-devops.mjs +64 -0
- package/scripts/tests/test-oris-flow-scan.mjs +21 -91
- package/scripts/tests/test-routing-lifecycle.mjs +22 -53
- package/scripts/tests/test-schemas.mjs +17 -31
- package/scripts/tests/test-skill-style.mjs +5 -2
- package/skills/oris-flow/SKILL.md +10 -6
- package/skills/oris-flow/references/architecture.md +1 -1
- package/skills/oris-flow/references/commit.md +46 -0
- package/skills/oris-flow/references/criteria.md +2 -2
- package/skills/oris-flow/references/discover.md +2 -2
- package/skills/oris-flow/references/fix.md +7 -3
- package/skills/oris-flow/references/help.md +1 -6
- package/skills/oris-flow/references/implement.md +5 -3
- package/skills/oris-flow/references/new.md +6 -6
- package/skills/oris-flow/references/plan.md +7 -2
- package/skills/oris-flow/references/pr.md +44 -0
- package/skills/oris-flow/references/setup.md +28 -35
- package/skills/oris-flow/references/verify.md +4 -5
- package/agents/oris-loop-debriefer.md +0 -30
- package/agents/oris-loop-doctor.md +0 -32
- package/agents/oris-loop-executor.md +0 -35
- package/agents/oris-loop-verifier.md +0 -35
- package/references/loop-adapter.schema.json +0 -93
- package/references/loop-contract.md +0 -126
- package/references/loop.schema.json +0 -156
- package/references/repo-map.md +0 -51
- package/references/repo-map.schema.json +0 -213
- package/scripts/flow/oris-flow-clean-runtime.mjs +0 -182
- package/scripts/install/generate-agent-adapters.mjs +0 -81
- package/scripts/loop/oris-loop-bootstrap.mjs +0 -383
- package/scripts/loop/oris-loop-bundle.mjs +0 -22
- package/scripts/loop/oris-loop-chat.mjs +0 -396
- package/scripts/loop/oris-loop-demo.mjs +0 -171
- package/scripts/loop/oris-loop-document.mjs +0 -196
- package/scripts/loop/oris-loop-dry-run.mjs +0 -114
- package/scripts/loop/oris-loop-fixtures.mjs +0 -149
- package/scripts/loop/oris-loop-list.mjs +0 -81
- package/scripts/loop/oris-loop-paths.mjs +0 -232
- package/scripts/loop/oris-loop-run.mjs +0 -301
- package/scripts/loop/oris-loop-stop.mjs +0 -358
- package/scripts/loop/oris-loop-templates.mjs +0 -80
- package/scripts/loop/oris-loop-verify.mjs +0 -205
- package/scripts/tests/test-agent-adapters.mjs +0 -70
- package/scripts/tests/test-oris-1-0-cleanliness.mjs +0 -58
- package/scripts/tests/test-oris-flow-clean-runtime.mjs +0 -75
- package/scripts/tests/test-oris-loop-bootstrap.mjs +0 -94
- package/scripts/tests/test-oris-loop-document.mjs +0 -148
- package/scripts/tests/test-oris-loop-list.mjs +0 -74
- package/scripts/tests/test-oris-loop-run.mjs +0 -71
- package/scripts/tests/test-oris-loop-smoke.mjs +0 -120
- package/scripts/tests/test-oris-loop-stop.mjs +0 -432
- package/skills/oris-flow/references/loop-craft.md +0 -59
- package/skills/oris-flow/references/loop-improve.md +0 -32
- package/skills/oris-flow/references/loop-run.md +0 -47
- package/skills/oris-flow/references/loop.md +0 -56
- package/skills/oris-flow/templates/debriefer.md +0 -19
- package/skills/oris-flow/templates/doctor.md +0 -22
- package/skills/oris-flow/templates/executor.md +0 -25
- package/skills/oris-flow/templates/orchestrator.md +0 -36
- package/skills/oris-flow/templates/verifier.md +0 -24
package/references/settings.md
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
# Oris Settings
|
|
2
2
|
|
|
3
|
-
Per-project preferences. Path: `.oris-flow/settings.json`. Small and non-secret
|
|
4
|
-
|
|
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": {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
562
|
-
|
|
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(`
|
|
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/
|
|
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,
|
|
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", "
|
|
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
|
|
326
|
+
log("Next: type /oris-flow in your agent chat.");
|
|
313
327
|
}
|
|
314
328
|
|
|
315
329
|
try {
|