@piagent/platform 1.0.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 (159) hide show
  1. package/.gitattributes +7 -0
  2. package/AGENTS.md +31 -0
  3. package/CHANGELOG.md +296 -0
  4. package/CODE_OF_CONDUCT.md +36 -0
  5. package/CONTRIBUTING.md +66 -0
  6. package/LICENSE +21 -0
  7. package/README.md +588 -0
  8. package/SECURITY.md +38 -0
  9. package/adapters/backend-api/profile.json +88 -0
  10. package/adapters/be-readonly-fe/profile.json +120 -0
  11. package/adapters/data/profile.json +88 -0
  12. package/adapters/devops/profile.json +92 -0
  13. package/adapters/docs/profile.json +74 -0
  14. package/adapters/fullstack/profile.json +94 -0
  15. package/adapters/generic/profile.json +95 -0
  16. package/adapters/mobile/profile.json +87 -0
  17. package/adapters/node-typescript/profile.json +76 -0
  18. package/adapters/python/profile.json +75 -0
  19. package/adapters/web-frontend/profile.json +101 -0
  20. package/catalog/capabilities.json +206 -0
  21. package/docs/architecture.md +64 -0
  22. package/docs/auto-delegation-policy.md +192 -0
  23. package/docs/capability-packs.md +234 -0
  24. package/docs/command-reference-vietnamese.md +484 -0
  25. package/docs/context-window-policy.md +85 -0
  26. package/docs/distribution-standard.md +229 -0
  27. package/docs/herdr-workflow.md +58 -0
  28. package/docs/mcp-and-tools.md +316 -0
  29. package/docs/memory-policy.md +164 -0
  30. package/docs/model-options.md +165 -0
  31. package/docs/oauth-providers.md +53 -0
  32. package/docs/operator-manual-vietnamese.md +1080 -0
  33. package/docs/package-architecture-notes.md +101 -0
  34. package/docs/project-adapters.md +174 -0
  35. package/docs/project-onboarding.md +135 -0
  36. package/docs/publishing-for-teams.md +64 -0
  37. package/docs/quality-benchmark.md +74 -0
  38. package/docs/quickstart-vietnamese.md +350 -0
  39. package/docs/release-install-policy.md +223 -0
  40. package/docs/runtime-harness-standard.md +118 -0
  41. package/docs/runtime-policy-design.md +132 -0
  42. package/docs/runtime-quality-baseline.md +257 -0
  43. package/docs/security-redaction-benchmark.md +56 -0
  44. package/docs/security-threat-model.md +66 -0
  45. package/docs/subagent-orchestration-capabilities.md +104 -0
  46. package/docs/subagents-and-multiagent.md +346 -0
  47. package/docs/task-implementation-contract.md +200 -0
  48. package/docs/team-onboarding.md +333 -0
  49. package/docs/usage-observability.md +130 -0
  50. package/docs/vercel-docs-site.md +115 -0
  51. package/docs/workflow-recipes.md +125 -0
  52. package/docs-site/assets/piagent-logo.svg +35 -0
  53. package/docs-site/favicon.svg +18 -0
  54. package/docs-site/index.html +1613 -0
  55. package/docs-site/vercel.json +24 -0
  56. package/evals/fixtures/action-proposal.invalid.json +34 -0
  57. package/evals/fixtures/action-proposal.valid.json +34 -0
  58. package/evals/fixtures/capability-pack.invalid.json +123 -0
  59. package/evals/fixtures/capability-pack.valid.json +123 -0
  60. package/evals/fixtures/capability-recipe.invalid.json +81 -0
  61. package/evals/fixtures/capability-recipe.valid.json +82 -0
  62. package/evals/fixtures/context-index.invalid.json +25 -0
  63. package/evals/fixtures/context-index.valid.json +25 -0
  64. package/evals/fixtures/eval-scenario.invalid.json +40 -0
  65. package/evals/fixtures/eval-scenario.valid.json +45 -0
  66. package/evals/fixtures/project-profile.invalid.json +88 -0
  67. package/evals/fixtures/project-profile.valid.json +95 -0
  68. package/evals/fixtures/task-contract.invalid.json +73 -0
  69. package/evals/fixtures/task-contract.valid.json +74 -0
  70. package/evals/golden/enforcement-decisions.json +329 -0
  71. package/evals/scenarios/capability-resolution.json +45 -0
  72. package/package.json +132 -0
  73. package/packages/piagent-core/README.md +147 -0
  74. package/packages/piagent-core/capabilities/capability-core.js +1023 -0
  75. package/packages/piagent-core/capabilities/capability-sources.js +265 -0
  76. package/packages/piagent-core/extensions/guard-io.js +45 -0
  77. package/packages/piagent-core/extensions/guard-shell-analysis.ts +558 -0
  78. package/packages/piagent-core/extensions/guard-types.ts +406 -0
  79. package/packages/piagent-core/extensions/piagent-guard.ts +5760 -0
  80. package/packages/piagent-core/extensions/policy-core.js +1070 -0
  81. package/packages/piagent-core/extensions/redaction-core.js +4 -0
  82. package/packages/piagent-core/extensions/runtime-evidence.js +215 -0
  83. package/packages/piagent-core/package.json +46 -0
  84. package/packages/piagent-core/policies/base-policy.json +249 -0
  85. package/packages/piagent-core/prompts/be-to-fe.md +82 -0
  86. package/packages/piagent-core/prompts/commit.md +27 -0
  87. package/packages/piagent-core/prompts/discuss.md +35 -0
  88. package/packages/piagent-core/prompts/memory-policy.md +52 -0
  89. package/packages/piagent-core/prompts/model-options.md +68 -0
  90. package/packages/piagent-core/prompts/onboard-project.md +147 -0
  91. package/packages/piagent-core/prompts/piagent-commands.md +68 -0
  92. package/packages/piagent-core/prompts/plan.md +35 -0
  93. package/packages/piagent-core/prompts/platform-improve.md +69 -0
  94. package/packages/piagent-core/prompts/pr.md +26 -0
  95. package/packages/piagent-core/prompts/review.md +38 -0
  96. package/packages/piagent-core/prompts/scout.md +50 -0
  97. package/packages/piagent-core/prompts/task.md +69 -0
  98. package/packages/piagent-core/security/sensitive-data.js +187 -0
  99. package/packages/piagent-core/skills/piagent-ops/SKILL.md +44 -0
  100. package/packages/piagent-core/skills/piagent-source-cache/SKILL.md +59 -0
  101. package/packages/piagent-core/skills/piagent-source-cache/checkout-source-repo.sh +144 -0
  102. package/packages/piagent-core/subagents/piagent-oracle.md +29 -0
  103. package/packages/piagent-core/subagents/piagent-planner.md +46 -0
  104. package/packages/piagent-core/subagents/piagent-reviewer.md +48 -0
  105. package/packages/piagent-core/subagents/piagent-scout.md +37 -0
  106. package/packages/piagent-core/subagents/piagent-worker.md +44 -0
  107. package/packs/engineering-base/pack.json +123 -0
  108. package/packs/engineering-base/recipes/bounded-change.json +82 -0
  109. package/packs/web-delivery/pack.json +72 -0
  110. package/packs/web-delivery/recipes/verified-web-change.json +57 -0
  111. package/schemas/action-proposal.schema.json +90 -0
  112. package/schemas/capability-pack.schema.json +190 -0
  113. package/schemas/capability-recipe.schema.json +72 -0
  114. package/schemas/context-index.schema.json +258 -0
  115. package/schemas/eval-scenario.schema.json +67 -0
  116. package/schemas/project-profile.schema.json +555 -0
  117. package/schemas/task-contract.schema.json +314 -0
  118. package/scripts/audit-runtime-host.sh +78 -0
  119. package/scripts/bootstrap-local.sh +25 -0
  120. package/scripts/capability-catalog.mjs +290 -0
  121. package/scripts/check-runtime-advisories.mjs +114 -0
  122. package/scripts/configure-mcp.sh +301 -0
  123. package/scripts/configure-model-scope.sh +143 -0
  124. package/scripts/configure-subagents.sh +241 -0
  125. package/scripts/import-agent-instructions.mjs +199 -0
  126. package/scripts/init-project.sh +369 -0
  127. package/scripts/install-global.sh +532 -0
  128. package/scripts/link-project.sh +25 -0
  129. package/scripts/migrate-project-state.mjs +176 -0
  130. package/scripts/pi-auto.sh +79 -0
  131. package/scripts/pi-model-catalog.sh +154 -0
  132. package/scripts/pi-session-stats.sh +135 -0
  133. package/scripts/piagent-cli.mjs +53 -0
  134. package/scripts/profile-doctor.sh +238 -0
  135. package/scripts/quality-benchmark.sh +185 -0
  136. package/scripts/redaction-benchmark.mjs +227 -0
  137. package/scripts/runtime-policy-smoke.sh +48 -0
  138. package/scripts/setup.sh +425 -0
  139. package/scripts/team-doctor.sh +461 -0
  140. package/scripts/verify-local.sh +540 -0
  141. package/scripts/verify-release-identity.mjs +85 -0
  142. package/scripts/verify-vercel-link.mjs +64 -0
  143. package/templates/global/mcp.json +9 -0
  144. package/templates/global/settings.json +37 -0
  145. package/templates/project/.mcp.json +9 -0
  146. package/templates/project/.pi/context-index.json +25 -0
  147. package/templates/project/.pi/gitignore.template +22 -0
  148. package/templates/project/.pi/mcp.json +9 -0
  149. package/templates/project/.pi/memory/MEMORY.md +11 -0
  150. package/templates/project/.pi/memory/memory_summary.md +9 -0
  151. package/templates/project/.pi/piagent-profile.json +107 -0
  152. package/templates/project/.pi/project-context.md +30 -0
  153. package/templates/project/.pi/settings.json +17 -0
  154. package/templates/project/.pi/task-contract.template.json +74 -0
  155. package/templates/project/.pi/tech-context/README.md +7 -0
  156. package/templates/project/.pi/tech-stack.json +10 -0
  157. package/templates/project/AGENTS.md +34 -0
  158. package/templates/project/REVIEW_GUIDELINES.md +23 -0
  159. package/types/pi-runtime-shims.d.ts +13 -0
@@ -0,0 +1,265 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { CapabilityValidationError, sha256, validateCapabilityPackageSource } from "./capability-core.js";
6
+
7
+ // A capability source lets a project use packs it does not own without forking
8
+ // the platform. There are two levels, and they end in the same place: a
9
+ // directory on disk that is scanned by exactly the same code that scans the
10
+ // platform's own packs.
11
+ //
12
+ // level 1 — `path`, a directory inside the project (a team's private packs)
13
+ // level 2 — `source`, an npm or git release vendored into the project
14
+ //
15
+ // Fetching is deliberately not part of resolution. A maintainer runs the vendor
16
+ // command, the result is written under the project and committed, and every
17
+ // later resolution reads only what is already on disk. The guard therefore
18
+ // never reaches the network, and a pack cannot change under it between the
19
+ // review and the run.
20
+ //
21
+ // None of this widens the trust model. An external pack still resolves only
22
+ // when the profile already names its owner and lifecycle, so an unknown owner
23
+ // is refused rather than warned about.
24
+
25
+ const VENDOR_DIRECTORY = path.join(".pi", "capability-vendor");
26
+ const SOURCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
27
+ const MAX_SOURCES = 16;
28
+ const MAX_VENDOR_ENTRIES = 4096;
29
+ const NETWORK_TIMEOUT_MS = 120_000;
30
+
31
+ function fail(message, details) {
32
+ throw new CapabilityValidationError(message, details);
33
+ }
34
+
35
+ /**
36
+ * Validate the `capabilitySources` block of a project profile.
37
+ *
38
+ * @param {unknown} value
39
+ * @param {string} source
40
+ * @returns {Array<{name: string, path?: string, source?: string}>}
41
+ */
42
+ export function validateCapabilitySources(value, source = "project profile") {
43
+ if (value === undefined) return [];
44
+ if (!Array.isArray(value)) fail(`${source}.capabilitySources must be an array`);
45
+ if (value.length > MAX_SOURCES) fail(`${source}.capabilitySources must declare at most ${MAX_SOURCES} sources`);
46
+
47
+ const names = new Set();
48
+ return value.map((entry, index) => {
49
+ const location = `${source}.capabilitySources[${index}]`;
50
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) fail(`${location} must be an object`);
51
+ const allowed = new Set(["name", "path", "source"]);
52
+ for (const key of Object.keys(entry)) {
53
+ if (!allowed.has(key)) fail(`${location}.${key} is not a supported key`);
54
+ }
55
+ if (typeof entry.name !== "string" || !SOURCE_NAME_PATTERN.test(entry.name)) {
56
+ fail(`${location}.name must be a lowercase identifier`);
57
+ }
58
+ if (names.has(entry.name)) fail(`${location}.name must be unique`);
59
+ names.add(entry.name);
60
+
61
+ // One level per entry. Accepting both would leave it ambiguous which one
62
+ // the lock digest actually covers.
63
+ const hasPath = entry.path !== undefined;
64
+ const hasSource = entry.source !== undefined;
65
+ if (hasPath === hasSource) fail(`${location} must declare exactly one of path or source`);
66
+
67
+ if (hasPath) {
68
+ if (typeof entry.path !== "string" || entry.path.length === 0 || entry.path.length > 512) {
69
+ fail(`${location}.path must be a short relative path`);
70
+ }
71
+ if (path.isAbsolute(entry.path) || entry.path.includes("\\") || entry.path.includes("\0")) {
72
+ fail(`${location}.path must be a project-relative path`);
73
+ }
74
+ if (entry.path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
75
+ fail(`${location}.path must not contain traversal segments`);
76
+ }
77
+ return { name: entry.name, path: entry.path };
78
+ }
79
+
80
+ const remote = validateCapabilityPackageSource(entry.source);
81
+ if (!/^(?:npm|git):/.test(remote)) fail(`${location}.source must be an npm or git source`);
82
+ return { name: entry.name, source: remote };
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Where a remote source is vendored. Deterministic so the lock, the vendor
88
+ * command, and the guard all agree without passing paths around.
89
+ *
90
+ * @param {string} projectRoot
91
+ * @param {string} name
92
+ */
93
+ export function vendorDirectoryFor(projectRoot, name) {
94
+ if (!SOURCE_NAME_PATTERN.test(name)) fail(`${name} is not a valid capability source name`);
95
+ return path.join(projectRoot, VENDOR_DIRECTORY, name);
96
+ }
97
+
98
+ function assertContainedDirectory(projectRoot, target, label) {
99
+ const rootReal = fs.realpathSync(projectRoot);
100
+ if (!fs.existsSync(target)) fail(`${label} does not exist; run the vendor command or correct the path`);
101
+ const stat = fs.lstatSync(target);
102
+ if (stat.isSymbolicLink()) fail(`${label} must not be a symbolic link`);
103
+ if (!stat.isDirectory()) fail(`${label} must be a directory`);
104
+
105
+ // realpath after the symlink check: the check above rejects the final
106
+ // component, and this rejects a parent that leads outside the project.
107
+ const targetReal = fs.realpathSync(target);
108
+ const relative = path.relative(rootReal, targetReal);
109
+ if (relative.startsWith("..") || path.isAbsolute(relative)) fail(`${label} resolves outside the project`);
110
+ return targetReal;
111
+ }
112
+
113
+ function assertNoSymbolicLinks(root, label, budget = { remaining: MAX_VENDOR_ENTRIES }) {
114
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
115
+ if (budget.remaining-- <= 0) fail(`${label} contains more than ${MAX_VENDOR_ENTRIES} entries`);
116
+ const child = path.join(root, entry.name);
117
+ if (entry.isSymbolicLink()) fail(`${label} must not contain symbolic links (${entry.name})`);
118
+ if (entry.isDirectory()) assertNoSymbolicLinks(child, label, budget);
119
+ else if (!entry.isFile()) fail(`${label} must contain only regular files (${entry.name})`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Turn declared sources into roots the pack scanner can read. Every root is
125
+ * verified to sit inside the project and to be free of symbolic links, because
126
+ * this is the boundary where someone else's tree becomes readable.
127
+ *
128
+ * @param {string} projectRoot
129
+ * @param {Array<{name: string, path?: string, source?: string}>} sources
130
+ */
131
+ export function resolveCapabilitySourceRoots(projectRoot, sources) {
132
+ // Resolved roots come back from realpath, so the project root has to be
133
+ // resolved the same way before the two are compared. On a system where the
134
+ // project sits under a symlinked parent, mixing the two forms produces a
135
+ // relative path that climbs out of the project and looks like an escape.
136
+ const rootReal = fs.realpathSync(projectRoot);
137
+ return validateCapabilitySources(sources).map((entry) => {
138
+ const target = entry.path
139
+ ? path.resolve(rootReal, entry.path)
140
+ : vendorDirectoryFor(rootReal, entry.name);
141
+ const label = `capability source ${entry.name}`;
142
+ const root = assertContainedDirectory(rootReal, target, label);
143
+ assertNoSymbolicLinks(root, label);
144
+ if (!fs.existsSync(path.join(root, "packs"))) fail(`${label} must contain a packs directory`);
145
+ return {
146
+ origin: entry.name,
147
+ root,
148
+ source: entry.source ?? `./${path.relative(rootReal, root).split(path.sep).join("/")}`
149
+ };
150
+ });
151
+ }
152
+
153
+ function runCommand(command, args, options) {
154
+ try {
155
+ return execFileSync(command, args, {
156
+ encoding: "utf8",
157
+ timeout: NETWORK_TIMEOUT_MS,
158
+ stdio: ["ignore", "pipe", "pipe"],
159
+ ...options
160
+ });
161
+ } catch (error) {
162
+ const detail = error && typeof error === "object" && "stderr" in error ? String(error.stderr ?? "").trim() : "";
163
+ fail(`${command} ${args[0] ?? ""} failed`, detail ? [detail] : undefined);
164
+ }
165
+ }
166
+
167
+ function fetchNpmSource(reference, destination) {
168
+ // `npm pack` resolves the exact version already validated by the source
169
+ // grammar and writes one tarball; nothing is executed from the package.
170
+ const staging = fs.mkdtempSync(path.join(os.tmpdir(), "piagent-vendor-"));
171
+ try {
172
+ const output = runCommand("npm", ["pack", reference, "--pack-destination", staging, "--ignore-scripts"], { cwd: staging });
173
+ const tarball = output.trim().split("\n").pop()?.trim();
174
+ if (!tarball) fail(`npm pack produced no tarball for ${reference}`);
175
+ const tarballPath = path.join(staging, tarball);
176
+ if (!fs.existsSync(tarballPath)) fail(`npm pack reported ${tarball} but wrote no such file`);
177
+ const extracted = path.join(staging, "extracted");
178
+ fs.mkdirSync(extracted);
179
+ runCommand("tar", ["-xzf", tarballPath, "-C", extracted, "--strip-components=1"]);
180
+ fs.cpSync(extracted, destination, { recursive: true, verbatimSymlinks: true });
181
+ } finally {
182
+ fs.rmSync(staging, { recursive: true, force: true });
183
+ }
184
+ }
185
+
186
+ function fetchGitSource(reference, destination) {
187
+ const separator = reference.lastIndexOf("@");
188
+ const location = reference.slice("git:".length, separator);
189
+ const revision = reference.slice(separator + 1);
190
+ const staging = fs.mkdtempSync(path.join(os.tmpdir(), "piagent-vendor-"));
191
+ try {
192
+ const url = `https://${location}.git`;
193
+ // No --depth: a tag or commit is fetched by name, and a shallow clone
194
+ // cannot always reach an arbitrary commit.
195
+ runCommand("git", ["clone", "--quiet", "--no-checkout", url, staging]);
196
+ runCommand("git", ["-C", staging, "checkout", "--quiet", "--detach", revision]);
197
+ fs.rmSync(path.join(staging, ".git"), { recursive: true, force: true });
198
+ fs.cpSync(staging, destination, { recursive: true, verbatimSymlinks: true });
199
+ } finally {
200
+ fs.rmSync(staging, { recursive: true, force: true });
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Fetch a remote source into the project's vendor directory. Only the vendor
206
+ * command calls this; resolution and enforcement never do.
207
+ *
208
+ * @param {string} projectRoot
209
+ * @param {{name: string, source?: string}} entry
210
+ */
211
+ export function vendorRemoteSource(projectRoot, entry) {
212
+ const [validated] = validateCapabilitySources([entry]);
213
+ if (!validated.source) fail(`capability source ${validated.name} is a local path and does not need vendoring`);
214
+
215
+ const destination = vendorDirectoryFor(projectRoot, validated.name);
216
+ const parent = path.dirname(destination);
217
+ fs.mkdirSync(parent, { recursive: true, mode: 0o755 });
218
+ if (fs.lstatSync(parent).isSymbolicLink()) fail(`${parent} must not be a symbolic link`);
219
+ fs.rmSync(destination, { recursive: true, force: true });
220
+ fs.mkdirSync(destination, { mode: 0o755 });
221
+
222
+ try {
223
+ if (validated.source.startsWith("npm:")) fetchNpmSource(validated.source.slice("npm:".length), destination);
224
+ else fetchGitSource(validated.source, destination);
225
+
226
+ // The fetched tree is untrusted until this passes. Checking here means a
227
+ // hostile archive is rejected before anything reads a manifest out of it.
228
+ assertNoSymbolicLinks(destination, `capability source ${validated.name}`);
229
+ if (!fs.existsSync(path.join(destination, "packs"))) {
230
+ fail(`capability source ${validated.name} provides no packs directory`);
231
+ }
232
+ } catch (error) {
233
+ // A half-written vendor directory would resolve on the next run and hide
234
+ // the failure, so remove it.
235
+ fs.rmSync(destination, { recursive: true, force: true });
236
+ throw error;
237
+ }
238
+
239
+ return { name: validated.name, source: validated.source, directory: destination, digest: digestDirectory(destination) };
240
+ }
241
+
242
+ /**
243
+ * Deterministic digest of a vendored tree: path and content of every regular
244
+ * file, in code-point order. Recorded so a later run can tell that the tree it
245
+ * reads is the tree that was reviewed.
246
+ *
247
+ * @param {string} root
248
+ */
249
+ export function digestDirectory(root) {
250
+ const files = [];
251
+ function walk(directory, prefix) {
252
+ const entries = fs.readdirSync(directory, { withFileTypes: true })
253
+ .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
254
+ for (const entry of entries) {
255
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
256
+ const child = path.join(directory, entry.name);
257
+ if (entry.isSymbolicLink()) fail(`${root} must not contain symbolic links (${relative})`);
258
+ if (entry.isDirectory()) walk(child, relative);
259
+ else if (entry.isFile()) files.push(`${relative}:${sha256(fs.readFileSync(child))}`);
260
+ else fail(`${root} must contain only regular files (${relative})`);
261
+ }
262
+ }
263
+ walk(root, "");
264
+ return sha256(files.join("\n"));
265
+ }
@@ -0,0 +1,45 @@
1
+ // Filesystem helpers shared by the guard and its domain modules.
2
+ // Deliberately dependency-free apart from node builtins so any domain module
3
+ // can import it without creating a cycle back through the guard.
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ /**
8
+ * @template T
9
+ * @param {string} filePath
10
+ * @returns {T | undefined}
11
+ */
12
+ export function readJsonFile(filePath) {
13
+ try {
14
+ if (!fs.existsSync(filePath)) return undefined;
15
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ // The package root carries the runtime policy; the platform root carries the
22
+ // adapter catalogue. They are usually the same directory but not always, so
23
+ // each is located by the marker that actually matters to the caller.
24
+ /** @param {string} startDir @returns {string} */
25
+ export function findPackageRoot(startDir) {
26
+ return findRootWithMarker(startDir, "policies");
27
+ }
28
+
29
+ /** @param {string} startDir @returns {string} */
30
+ export function findPlatformRoot(startDir) {
31
+ return findRootWithMarker(startDir, "adapters");
32
+ }
33
+
34
+ /** @param {string} startDir @param {string} marker @returns {string} */
35
+ function findRootWithMarker(startDir, marker) {
36
+ let current = startDir;
37
+ while (true) {
38
+ if (fs.existsSync(path.join(current, "package.json")) && fs.existsSync(path.join(current, marker))) {
39
+ return current;
40
+ }
41
+ const parent = path.dirname(current);
42
+ if (parent === current) return startDir;
43
+ current = parent;
44
+ }
45
+ }