@treeseed/sdk 0.10.5 → 0.10.7

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 (41) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +58 -0
  3. package/dist/operations/repository-operations.d.ts +129 -0
  4. package/dist/operations/repository-operations.js +634 -0
  5. package/dist/operations/services/config-runtime.d.ts +7 -6
  6. package/dist/operations/services/config-runtime.js +45 -25
  7. package/dist/operations/services/deploy.d.ts +42 -0
  8. package/dist/operations/services/deploy.js +1 -1
  9. package/dist/operations/services/project-platform.d.ts +41 -1
  10. package/dist/operations/services/project-platform.js +13 -0
  11. package/dist/operations/services/railway-api.d.ts +35 -1
  12. package/dist/operations/services/railway-api.js +240 -35
  13. package/dist/operations/services/railway-deploy.d.ts +16 -234
  14. package/dist/operations/services/railway-deploy.js +177 -62
  15. package/dist/operations/services/release-candidate.js +1 -2
  16. package/dist/operations/services/runtime-tools.d.ts +14 -0
  17. package/dist/operations/services/runtime-tools.js +15 -1
  18. package/dist/operations/services/workspace-save.d.ts +24 -0
  19. package/dist/operations/services/workspace-save.js +143 -3
  20. package/dist/operations/services/workspace-tools.js +1 -1
  21. package/dist/platform/env.yaml +163 -2
  22. package/dist/platform/environment.d.ts +1 -0
  23. package/dist/platform/environment.js +9 -0
  24. package/dist/platform-operation-store.d.ts +90 -0
  25. package/dist/platform-operation-store.js +505 -0
  26. package/dist/platform-operations.d.ts +265 -0
  27. package/dist/platform-operations.js +421 -0
  28. package/dist/reconcile/bootstrap-systems.js +3 -3
  29. package/dist/reconcile/builtin-adapters.js +225 -29
  30. package/dist/reconcile/contracts.d.ts +1 -1
  31. package/dist/reconcile/desired-state.d.ts +14 -0
  32. package/dist/reconcile/desired-state.js +4 -0
  33. package/dist/reconcile/engine.d.ts +28 -0
  34. package/dist/reconcile/state.js +3 -0
  35. package/dist/reconcile/units.js +2 -0
  36. package/dist/workflow/operations.d.ts +13 -5
  37. package/dist/workflow/operations.js +69 -12
  38. package/dist/workflow-state.d.ts +2 -0
  39. package/dist/workflow-state.js +7 -2
  40. package/dist/workflow.d.ts +2 -0
  41. package/package.json +15 -2
@@ -0,0 +1,634 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, relative, resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { serializeFrontmatterDocument, parseFrontmatterDocument } from "../frontmatter.js";
7
+ const execFileAsync = promisify(execFile);
8
+ const PLATFORM_CONTENT_COLLECTIONS = ["objectives", "questions", "notes", "proposals", "decisions", "agents"];
9
+ const PLATFORM_WORK_CONTENT_COLLECTIONS = ["objectives", "questions", "notes", "proposals", "decisions"];
10
+ const CONTENT_COLLECTION_SET = new Set(PLATFORM_CONTENT_COLLECTIONS);
11
+ const WORK_CONTENT_COLLECTION_SET = new Set(PLATFORM_WORK_CONTENT_COLLECTIONS);
12
+ const DECISION_TYPE_VALUES = ["approved", "rejected", "deferred", "request_changes", "superseded"];
13
+ const PROPOSAL_VERDICT_DECISION_TYPES = /* @__PURE__ */ new Set(["approved", "rejected", "deferred", "request_changes"]);
14
+ const CONTENT_DEFAULTS = {
15
+ objectives: {
16
+ idPrefix: "objective",
17
+ extension: "mdx",
18
+ fields: { timeHorizon: "near-term", motivation: "", primaryContributor: "market-steward", relatedQuestions: [], relatedBooks: [] },
19
+ body: "Describe the objective, expected outcome, and the evidence that should update it over time."
20
+ },
21
+ questions: {
22
+ idPrefix: "question",
23
+ extension: "mdx",
24
+ fields: { questionType: "strategy", motivation: "", primaryContributor: "market-steward", relatedObjectives: [], relatedBooks: [] },
25
+ body: "Describe what needs to be learned and what evidence would make the answer useful."
26
+ },
27
+ notes: {
28
+ idPrefix: "note",
29
+ extension: "mdx",
30
+ fields: { author: "market-steward", relatedObjectives: [], relatedQuestions: [], relatedProposals: [], relatedBooks: [] },
31
+ body: "Capture the useful context, evidence, and follow-up links for this note."
32
+ },
33
+ proposals: {
34
+ idPrefix: "proposal",
35
+ extension: "mdx",
36
+ fields: { proposalType: "implementation", motivation: "", primaryContributor: "market-steward", relatedObjectives: [], relatedQuestions: [], relatedNotes: [], relatedBooks: [], decision: "", supersedes: [] },
37
+ body: "Describe the proposed change, why it matters, what it affects, and how a reviewer should evaluate it."
38
+ },
39
+ decisions: {
40
+ idPrefix: "decision",
41
+ extension: "mdx",
42
+ fields: { decisionType: "approved", rationale: "", authority: "TreeSeed Market Team", primaryContributor: "market-steward", relatedObjectives: [], relatedQuestions: [], relatedNotes: [], relatedProposals: [], relatedBooks: [], supersedes: [], implements: [] },
43
+ body: "Record what was decided, why it was decided, and which proposals or evidence it closes."
44
+ },
45
+ agents: {
46
+ idPrefix: "agent",
47
+ extension: "mdx",
48
+ fields: {
49
+ name: "",
50
+ handler: "planner",
51
+ enabled: true,
52
+ operator: "TreeSeed platform",
53
+ runtimeStatus: "active",
54
+ capabilities: [],
55
+ tags: ["agent"],
56
+ systemPrompt: "Use the core objective as the first context message. Keep work observable, governed, and grounded in project content.",
57
+ persona: "Helpful, careful, and accountable.",
58
+ triggers: [{ type: "message", messageTypes: [] }],
59
+ permissions: [],
60
+ execution: { provider: "codex", model: "gpt-5.5", approvalPolicy: "never", sandboxMode: "read_only", reasoningEffort: "medium" },
61
+ outputs: {}
62
+ },
63
+ body: "Describe this agent role, operating boundaries, and expected outputs."
64
+ }
65
+ };
66
+ const CONTENT_RELATION_POLICIES = {
67
+ objectives: {
68
+ questions: { sourceField: "relatedQuestions", targetField: "relatedObjectives" }
69
+ },
70
+ questions: {
71
+ objectives: { sourceField: "relatedObjectives", targetField: "relatedQuestions" }
72
+ },
73
+ notes: {
74
+ objectives: { sourceField: "relatedObjectives" },
75
+ questions: { sourceField: "relatedQuestions" },
76
+ proposals: { sourceField: "relatedProposals", targetField: "relatedNotes" }
77
+ },
78
+ proposals: {
79
+ objectives: { sourceField: "relatedObjectives" },
80
+ questions: { sourceField: "relatedQuestions" },
81
+ notes: { sourceField: "relatedNotes", targetField: "relatedProposals" },
82
+ decisions: { sourceField: "decision", targetField: "relatedProposals", sourceSingle: true }
83
+ },
84
+ decisions: {
85
+ objectives: { sourceField: "relatedObjectives" },
86
+ questions: { sourceField: "relatedQuestions" },
87
+ notes: { sourceField: "relatedNotes" },
88
+ proposals: { sourceField: "relatedProposals", targetField: "decision", targetSingle: true }
89
+ }
90
+ };
91
+ class PlatformRepositoryVerificationError extends Error {
92
+ verification;
93
+ constructor(message, verification) {
94
+ super(message);
95
+ this.name = "PlatformRepositoryVerificationError";
96
+ this.verification = verification;
97
+ }
98
+ }
99
+ function optionalTrimmedString(value) {
100
+ return typeof value === "string" && value.trim() ? value.trim() : null;
101
+ }
102
+ function slugifyPlatformContent(value) {
103
+ return String(value ?? "").toLowerCase().trim().replace(/['"]/gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 96);
104
+ }
105
+ function enumValue(value, allowed, fallback = null) {
106
+ const candidate = typeof value === "string" ? value.trim() : "";
107
+ return allowed.includes(candidate) ? candidate : fallback;
108
+ }
109
+ function normalizePlatformRelationArray(value) {
110
+ if (Array.isArray(value)) return value.map((entry) => String(entry).trim()).filter(Boolean);
111
+ if (typeof value === "string") return value.split(/[\n,]/u).map((entry) => entry.trim()).filter(Boolean);
112
+ return [];
113
+ }
114
+ function uniqueRelationArray(value) {
115
+ return [...new Set(normalizePlatformRelationArray(value))];
116
+ }
117
+ function addRelationValue(frontmatter, field, value, single = false) {
118
+ const ref = String(value ?? "").trim();
119
+ if (!field || !ref) return;
120
+ if (single) {
121
+ frontmatter[field] = ref;
122
+ return;
123
+ }
124
+ frontmatter[field] = uniqueRelationArray([...normalizePlatformRelationArray(frontmatter[field]), ref]);
125
+ }
126
+ function platformContentRelationPolicy(parentCollection, targetCollection) {
127
+ return CONTENT_RELATION_POLICIES[parentCollection]?.[targetCollection] ?? null;
128
+ }
129
+ function normalizePlatformContentInput(collection, body) {
130
+ const defaults = CONTENT_DEFAULTS[collection];
131
+ if (!defaults) return { error: "Unsupported content collection." };
132
+ const title = optionalTrimmedString(body.title);
133
+ if (!title) return { error: "title is required." };
134
+ const slug = slugifyPlatformContent(body.slug || title);
135
+ if (!slug) return { error: "A safe slug is required." };
136
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
137
+ const summary = optionalTrimmedString(body.summary) ?? optionalTrimmedString(body.description) ?? title;
138
+ const description = optionalTrimmedString(body.description) ?? summary;
139
+ const frontmatter = {
140
+ id: optionalTrimmedString(body.id) ?? `${defaults.idPrefix}:${slug}`,
141
+ title,
142
+ description,
143
+ date: optionalTrimmedString(body.date) ?? today,
144
+ summary,
145
+ status: enumValue(body.status, ["recorded", "live", "in progress", "exploratory", "planned", "speculative"], "planned"),
146
+ ...defaults.fields
147
+ };
148
+ if (collection === "agents") {
149
+ frontmatter.name = optionalTrimmedString(body.name) ?? title;
150
+ frontmatter.slug = slug;
151
+ frontmatter.description = description;
152
+ frontmatter.summary = summary;
153
+ frontmatter.handler = optionalTrimmedString(body.handler) ?? frontmatter.handler;
154
+ frontmatter.systemPrompt = optionalTrimmedString(body.systemPrompt) ?? frontmatter.systemPrompt;
155
+ frontmatter.runtimeStatus = enumValue(body.runtimeStatus, ["active", "experimental", "dormant"], String(frontmatter.runtimeStatus));
156
+ delete frontmatter.date;
157
+ delete frontmatter.status;
158
+ } else if (collection === "notes") {
159
+ frontmatter.author = optionalTrimmedString(body.author) ?? frontmatter.author;
160
+ frontmatter.relatedObjectives = normalizePlatformRelationArray(body.relatedObjectives);
161
+ frontmatter.relatedQuestions = normalizePlatformRelationArray(body.relatedQuestions);
162
+ frontmatter.relatedProposals = normalizePlatformRelationArray(body.relatedProposals);
163
+ } else if (collection === "objectives") {
164
+ frontmatter.primaryContributor = optionalTrimmedString(body.primaryContributor) ?? frontmatter.primaryContributor;
165
+ frontmatter.timeHorizon = enumValue(body.timeHorizon, ["near-term", "mid-term", "long-term"], String(frontmatter.timeHorizon));
166
+ frontmatter.motivation = optionalTrimmedString(body.motivation) ?? description;
167
+ frontmatter.relatedQuestions = normalizePlatformRelationArray(body.relatedQuestions);
168
+ } else if (collection === "questions") {
169
+ frontmatter.primaryContributor = optionalTrimmedString(body.primaryContributor) ?? frontmatter.primaryContributor;
170
+ frontmatter.questionType = enumValue(body.questionType, ["research", "implementation", "strategy", "evaluation"], String(frontmatter.questionType));
171
+ frontmatter.motivation = optionalTrimmedString(body.motivation) ?? description;
172
+ frontmatter.relatedObjectives = normalizePlatformRelationArray(body.relatedObjectives);
173
+ } else if (collection === "proposals") {
174
+ frontmatter.primaryContributor = optionalTrimmedString(body.primaryContributor) ?? frontmatter.primaryContributor;
175
+ frontmatter.proposalType = enumValue(body.proposalType, ["strategy", "policy", "implementation", "research"], String(frontmatter.proposalType));
176
+ frontmatter.motivation = optionalTrimmedString(body.motivation) ?? description;
177
+ frontmatter.relatedObjectives = normalizePlatformRelationArray(body.relatedObjectives);
178
+ frontmatter.relatedQuestions = normalizePlatformRelationArray(body.relatedQuestions);
179
+ frontmatter.relatedNotes = normalizePlatformRelationArray(body.relatedNotes);
180
+ frontmatter.decision = optionalTrimmedString(body.decision) ?? void 0;
181
+ } else if (collection === "decisions") {
182
+ frontmatter.primaryContributor = optionalTrimmedString(body.primaryContributor) ?? frontmatter.primaryContributor;
183
+ frontmatter.decisionType = enumValue(body.decisionType, DECISION_TYPE_VALUES, String(frontmatter.decisionType));
184
+ frontmatter.rationale = optionalTrimmedString(body.rationale) ?? description;
185
+ frontmatter.authority = optionalTrimmedString(body.authority) ?? frontmatter.authority;
186
+ frontmatter.relatedObjectives = normalizePlatformRelationArray(body.relatedObjectives);
187
+ frontmatter.relatedQuestions = normalizePlatformRelationArray(body.relatedQuestions);
188
+ frontmatter.relatedNotes = normalizePlatformRelationArray(body.relatedNotes);
189
+ frontmatter.relatedProposals = normalizePlatformRelationArray(body.relatedProposals);
190
+ }
191
+ return {
192
+ slug,
193
+ extension: defaults.extension,
194
+ frontmatter: Object.fromEntries(Object.entries(frontmatter).filter(([, value]) => value !== void 0)),
195
+ body: optionalTrimmedString(body.body) ?? defaults.body
196
+ };
197
+ }
198
+ function derivePlatformRepositoryKey(repository) {
199
+ return [repository.provider ?? "git", repository.owner ?? "local", repository.name].join("-").toLowerCase().replace(/[^a-z0-9.-]+/gu, "-").replace(/^-+|-+$/gu, "") || "repository";
200
+ }
201
+ function resolvePlatformRepositoryWorkspacePath(workspaceRoot, repository) {
202
+ return resolve(workspaceRoot, "repositories", derivePlatformRepositoryKey(repository), "repo");
203
+ }
204
+ function createPlatformRepositoryClaim(input) {
205
+ const now = (/* @__PURE__ */ new Date()).toISOString();
206
+ const leaseSeconds = Math.max(30, Math.min(Number(input.leaseSeconds ?? 300), 3600));
207
+ const repositoryKey = derivePlatformRepositoryKey(input.repository);
208
+ return {
209
+ id: `${repositoryKey}:${input.runnerId}`,
210
+ repositoryKey,
211
+ runnerId: input.runnerId,
212
+ workspacePath: resolvePlatformRepositoryWorkspacePath(input.workspaceRoot, input.repository),
213
+ branch: input.branch ?? input.repository.defaultBranch ?? null,
214
+ commitSha: input.commitSha ?? null,
215
+ claimState: "active",
216
+ leaseExpiresAt: new Date(Date.now() + leaseSeconds * 1e3).toISOString(),
217
+ metadata: input.metadata ?? {},
218
+ createdAt: now,
219
+ updatedAt: now
220
+ };
221
+ }
222
+ async function runGit(args, cwd) {
223
+ const result = await execFileAsync("git", args, {
224
+ cwd,
225
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
226
+ maxBuffer: 1024 * 1024 * 8
227
+ });
228
+ return `${result.stdout}${result.stderr}`.trim();
229
+ }
230
+ async function syncRepository(repository, workspaceRoot) {
231
+ const repoPath = resolvePlatformRepositoryWorkspacePath(workspaceRoot, repository);
232
+ const branch = repository.defaultBranch || "staging";
233
+ await mkdir(dirname(repoPath), { recursive: true });
234
+ if (!existsSync(resolve(repoPath, ".git"))) {
235
+ try {
236
+ await runGit(["clone", "--branch", branch, "--single-branch", repository.cloneUrl, repoPath], workspaceRoot);
237
+ } catch {
238
+ try {
239
+ await runGit(["clone", repository.cloneUrl, repoPath], workspaceRoot);
240
+ await runGit(["checkout", branch], repoPath).catch(() => "");
241
+ } catch (error) {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ if (repository.writeMode === "workspace" && message.includes("ENOENT")) {
244
+ await mkdir(repoPath, { recursive: true });
245
+ } else {
246
+ throw error;
247
+ }
248
+ }
249
+ }
250
+ } else {
251
+ await runGit(["fetch", "origin", branch, "--prune"], repoPath).catch(() => "");
252
+ await runGit(["checkout", branch], repoPath).catch(() => "");
253
+ }
254
+ return { repoPath, branch };
255
+ }
256
+ function contentRoot(repoPath, collection) {
257
+ if (!CONTENT_COLLECTION_SET.has(collection)) throw new Error("Unsupported content collection.");
258
+ return resolve(repoPath, "src", "content", collection);
259
+ }
260
+ function safeContentPath(repoPath, collection, slug, extension = null) {
261
+ const safeSlug = slugifyPlatformContent(slug);
262
+ if (!safeSlug || safeSlug !== String(slug ?? "").trim()) throw new Error("Unsafe content slug.");
263
+ const root = contentRoot(repoPath, collection);
264
+ const candidates = extension ? [resolve(root, `${safeSlug}.${extension}`)] : ["mdx", "md"].map((ext) => resolve(root, `${safeSlug}.${ext}`));
265
+ const target = candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
266
+ const relativeTarget = relative(root, target);
267
+ if (relativeTarget.startsWith("..") || relativeTarget.includes("..") || relativeTarget.startsWith("/")) {
268
+ throw new Error("Unsafe content path.");
269
+ }
270
+ return target;
271
+ }
272
+ function assertAllowedPath(repoPath, targetPath) {
273
+ const relativePath = relative(repoPath, targetPath);
274
+ if (relativePath.startsWith("..") || relativePath.includes("..") || relativePath.startsWith("/")) {
275
+ throw new Error("Repository operation attempted to write outside the repository workspace.");
276
+ }
277
+ if (!relativePath.startsWith("src/content/")) {
278
+ throw new Error(`Repository operation path is outside src/content: ${relativePath}`);
279
+ }
280
+ return relativePath;
281
+ }
282
+ async function readContentRecord(repoPath, collection, slug) {
283
+ if (!WORK_CONTENT_COLLECTION_SET.has(collection)) throw new Error("Unsupported content collection.");
284
+ const target = safeContentPath(repoPath, collection, slug);
285
+ if (!existsSync(target)) throw new Error("Parent content record was not found.");
286
+ const parsed = parseFrontmatterDocument(await readFile(target, "utf8"));
287
+ if (!parsed.frontmatter || typeof parsed.frontmatter !== "object" || Array.isArray(parsed.frontmatter)) {
288
+ throw new Error("Content frontmatter could not be parsed.");
289
+ }
290
+ return {
291
+ path: target,
292
+ slug,
293
+ extension: target.endsWith(".md") ? "md" : "mdx",
294
+ frontmatter: parsed.frontmatter,
295
+ body: parsed.body
296
+ };
297
+ }
298
+ async function writeParsedRecord(repoPath, record) {
299
+ const relativePath = assertAllowedPath(repoPath, record.path);
300
+ await mkdir(dirname(record.path), { recursive: true });
301
+ await writeFile(record.path, serializeFrontmatterDocument(record.frontmatter, `
302
+ ${String(record.body ?? "").trim()}
303
+ `), "utf8");
304
+ return relativePath;
305
+ }
306
+ async function writeContentRecord(repoPath, collection, input, normalizedInput) {
307
+ const normalized = normalizedInput ?? normalizePlatformContentInput(collection, input);
308
+ if ("error" in normalized) throw new Error(normalized.error);
309
+ const root = contentRoot(repoPath, collection);
310
+ const existingTarget = input.overwrite === true ? [`${normalized.slug}.mdx`, `${normalized.slug}.md`].map((file) => resolve(root, file)).find((candidate) => existsSync(candidate)) : null;
311
+ const target = existingTarget ?? safeContentPath(repoPath, collection, normalized.slug, normalized.extension);
312
+ if (existsSync(target) && input.overwrite !== true) throw new Error("A content record with that slug already exists.");
313
+ const relativePath = await writeParsedRecord(repoPath, {
314
+ path: target,
315
+ frontmatter: normalized.frontmatter,
316
+ body: normalized.body
317
+ });
318
+ return {
319
+ collection,
320
+ slug: normalized.slug,
321
+ id: normalized.frontmatter.id,
322
+ path: relativePath,
323
+ href: collection === "agents" ? `/app/projects/${encodeURIComponent(String(input.projectId ?? ""))}/agents/${encodeURIComponent(normalized.slug)}` : `/app/work/${collection}/${encodeURIComponent(normalized.slug)}`
324
+ };
325
+ }
326
+ async function createRelatedContent(repoPath, input) {
327
+ const parentCollection = String(input.parentCollection ?? "");
328
+ const targetCollection = String(input.targetCollection ?? input.collection ?? "");
329
+ if (!WORK_CONTENT_COLLECTION_SET.has(parentCollection) || !WORK_CONTENT_COLLECTION_SET.has(targetCollection)) {
330
+ throw new Error("Unsupported content relation collection.");
331
+ }
332
+ const policy = platformContentRelationPolicy(parentCollection, targetCollection);
333
+ if (!policy) throw new Error(`Cannot create related ${targetCollection} from ${parentCollection}.`);
334
+ const parentSlug = optionalTrimmedString(input.parentSlug);
335
+ if (!parentSlug) throw new Error("parentSlug is required.");
336
+ const parent = await readContentRecord(repoPath, parentCollection, parentSlug);
337
+ const normalized = input.normalized ?? normalizePlatformContentInput(targetCollection, input.payload ?? {});
338
+ if ("error" in normalized) throw new Error(normalized.error);
339
+ const childTarget = safeContentPath(repoPath, targetCollection, normalized.slug, normalized.extension);
340
+ if (existsSync(childTarget)) throw new Error("A content record with that slug already exists.");
341
+ addRelationValue(parent.frontmatter, policy.sourceField, normalized.slug, policy.sourceSingle);
342
+ addRelationValue(normalized.frontmatter, policy.targetField, parent.slug, policy.targetSingle);
343
+ await mkdir(contentRoot(repoPath, targetCollection), { recursive: true });
344
+ const child = {
345
+ path: childTarget,
346
+ frontmatter: normalized.frontmatter,
347
+ body: normalized.body
348
+ };
349
+ const originalParent = { ...parent, frontmatter: { ...parent.frontmatter }, body: parent.body };
350
+ const changedPaths2 = [];
351
+ try {
352
+ changedPaths2.push(await writeParsedRecord(repoPath, child));
353
+ changedPaths2.push(await writeParsedRecord(repoPath, parent));
354
+ } catch (error) {
355
+ await rm(childTarget, { force: true }).catch(() => {
356
+ });
357
+ await writeParsedRecord(repoPath, originalParent).catch(() => {
358
+ });
359
+ throw error;
360
+ }
361
+ return {
362
+ parent: {
363
+ collection: parentCollection,
364
+ slug: parent.slug,
365
+ path: relative(repoPath, parent.path),
366
+ href: `/app/work/${parentCollection}/${encodeURIComponent(parent.slug)}`
367
+ },
368
+ child: {
369
+ collection: targetCollection,
370
+ slug: normalized.slug,
371
+ id: normalized.frontmatter.id,
372
+ path: relative(repoPath, childTarget),
373
+ href: `/app/work/${targetCollection}/${encodeURIComponent(normalized.slug)}`
374
+ },
375
+ relation: {
376
+ parentField: policy.sourceField,
377
+ childField: policy.targetField
378
+ },
379
+ changedPaths: changedPaths2
380
+ };
381
+ }
382
+ async function createDecisionFromProposals(repoPath, input) {
383
+ const proposalSlugs = [...new Set(normalizePlatformRelationArray(input.proposalSlugs ?? input.payload?.proposalSlugs))];
384
+ if (proposalSlugs.length === 0) throw new Error("Select at least one proposal.");
385
+ for (const slug of proposalSlugs) {
386
+ if (!slug || slugifyPlatformContent(slug) !== slug) throw new Error("Unsafe proposal slug.");
387
+ }
388
+ const decisionType = enumValue(input.decisionType ?? input.payload?.decisionType, [...PROPOSAL_VERDICT_DECISION_TYPES], null);
389
+ if (!decisionType) throw new Error("Unsupported proposal verdict.");
390
+ const reason = optionalTrimmedString(input.reason) ?? optionalTrimmedString(input.payload?.reason) ?? optionalTrimmedString(input.payload?.rationale);
391
+ if (!reason) throw new Error("A decision reason is required.");
392
+ const title = optionalTrimmedString(input.title) ?? optionalTrimmedString(input.payload?.title) ?? `Decision for ${proposalSlugs.length === 1 ? proposalSlugs[0] : `${proposalSlugs.length} proposals`}`;
393
+ const decisionSlug = slugifyPlatformContent(input.slug || input.payload?.slug || title);
394
+ if (!decisionSlug) throw new Error("A safe decision slug is required.");
395
+ const decisionTarget = safeContentPath(repoPath, "decisions", decisionSlug, "mdx");
396
+ if (existsSync(decisionTarget)) throw new Error("A decision with that slug already exists.");
397
+ const proposals = [];
398
+ for (const slug of proposalSlugs) {
399
+ try {
400
+ proposals.push(await readContentRecord(repoPath, "proposals", slug));
401
+ } catch {
402
+ throw new Error(`Proposal ${slug} was not found.`);
403
+ }
404
+ }
405
+ const proposalTitles = proposals.map((proposal) => proposal.frontmatter.title ?? proposal.slug);
406
+ const body = optionalTrimmedString(input.payload?.body) ?? [
407
+ "## Verdict",
408
+ decisionType.replace(/_/gu, " "),
409
+ "",
410
+ "## Reason",
411
+ reason,
412
+ "",
413
+ "## Proposals",
414
+ ...proposalTitles.map((proposalTitle, index) => `- ${proposalTitle} (${proposalSlugs[index]})`)
415
+ ].join("\n");
416
+ const decision = await writeContentRecord(repoPath, "decisions", {
417
+ ...input.payload ?? {},
418
+ projectId: input.projectId,
419
+ slug: decisionSlug,
420
+ title,
421
+ status: "live",
422
+ decisionType,
423
+ description: optionalTrimmedString(input.payload?.description) ?? reason,
424
+ summary: optionalTrimmedString(input.payload?.summary) ?? reason,
425
+ rationale: reason,
426
+ relatedProposals: proposalSlugs,
427
+ body
428
+ });
429
+ const writtenProposals = [];
430
+ const originalProposals = proposals.map((proposal) => ({
431
+ ...proposal,
432
+ frontmatter: { ...proposal.frontmatter },
433
+ body: proposal.body
434
+ }));
435
+ const changedPaths2 = [decision.path];
436
+ try {
437
+ for (const proposal of proposals) {
438
+ proposal.frontmatter.decision = decisionSlug;
439
+ changedPaths2.push(await writeParsedRecord(repoPath, proposal));
440
+ writtenProposals.push(proposal);
441
+ }
442
+ } catch (error) {
443
+ await rm(decisionTarget, { force: true }).catch(() => {
444
+ });
445
+ for (const original of originalProposals.slice(0, writtenProposals.length)) {
446
+ await writeParsedRecord(repoPath, original).catch(() => {
447
+ });
448
+ }
449
+ throw error;
450
+ }
451
+ return {
452
+ decision,
453
+ proposals: proposalSlugs.map((slug) => ({ collection: "proposals", slug, href: `/app/work/proposals/${encodeURIComponent(slug)}` })),
454
+ href: decision.href,
455
+ changedPaths: changedPaths2
456
+ };
457
+ }
458
+ async function changedPaths(repoPath) {
459
+ const output = await runGit(["status", "--porcelain", "--untracked-files=all"], repoPath).catch(() => "");
460
+ return output.split("\n").map((line) => line.slice(3).trim()).filter(Boolean);
461
+ }
462
+ function changedPathsFromOutput(output) {
463
+ const paths = [];
464
+ if (typeof output.record === "object" && output.record && !Array.isArray(output.record) && typeof output.record.path === "string") {
465
+ paths.push(String(output.record.path));
466
+ }
467
+ if (Array.isArray(output.changedPaths)) {
468
+ paths.push(...output.changedPaths.map((entry) => String(entry)));
469
+ }
470
+ return [...new Set(paths.filter(Boolean))];
471
+ }
472
+ async function commitIfRequested(repoPath, repository, input, changed) {
473
+ if (repository.writeMode !== "branch") return { branch: null, commitSha: null };
474
+ const branchName = repository.branchName || `treeseed/platform-${Date.now()}`;
475
+ if (!/^[-/._a-zA-Z0-9]{1,120}$/u.test(branchName) || branchName.includes("..") || branchName.startsWith("/") || branchName.endsWith("/")) {
476
+ throw new Error("Repository branch name is outside the allowed platform operation policy.");
477
+ }
478
+ await runGit(["checkout", "-B", branchName], repoPath);
479
+ if (changed.length === 0) return { branch: branchName, commitSha: null };
480
+ await runGit(["add", "--", ...changed], repoPath);
481
+ await runGit([
482
+ "-c",
483
+ "user.name=TreeSeed Platform Runner",
484
+ "-c",
485
+ "user.email=platform-runner@treeseed.local",
486
+ "commit",
487
+ "-m",
488
+ input.commitMessage || `TreeSeed platform operation: ${input.projectId ?? "repository"}`
489
+ ], repoPath).catch((error) => {
490
+ const message = error instanceof Error ? error.message : String(error);
491
+ if (!message.includes("nothing to commit")) throw error;
492
+ });
493
+ const commitSha = (await runGit(["rev-parse", "HEAD"], repoPath)).trim();
494
+ if (repository.push === true) {
495
+ await runGit(["push", "origin", branchName], repoPath);
496
+ }
497
+ return { branch: branchName, commitSha };
498
+ }
499
+ function commandOutput(value) {
500
+ return String(value ?? "").slice(0, 12e3);
501
+ }
502
+ async function runVerificationCommands(repoPath, repository) {
503
+ const commands = Array.isArray(repository.verificationCommands) ? repository.verificationCommands.filter((command) => command && typeof command.command === "string" && command.command.trim()) : [];
504
+ if (commands.length === 0) return null;
505
+ const results = [];
506
+ for (const command of commands) {
507
+ const args = Array.isArray(command.args) ? command.args.map(String) : [];
508
+ const cwd = resolve(repoPath, command.workingDirectory ?? ".");
509
+ const relativeCwd = relative(repoPath, cwd);
510
+ if (relativeCwd.startsWith("..") || relativeCwd.includes("..") || relativeCwd.startsWith("/")) {
511
+ throw new Error("Repository verification command attempted to run outside the repository workspace.");
512
+ }
513
+ try {
514
+ const result = await execFileAsync(command.command, args, {
515
+ cwd,
516
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
517
+ timeout: Math.max(1e3, Math.min(Number(command.timeoutMs ?? 12e4), 6e5)),
518
+ maxBuffer: 1024 * 1024 * 8
519
+ });
520
+ results.push({
521
+ command: command.command,
522
+ args,
523
+ cwd: relative(repoPath, cwd) || ".",
524
+ exitCode: 0,
525
+ stdout: commandOutput(result.stdout),
526
+ stderr: commandOutput(result.stderr)
527
+ });
528
+ } catch (error) {
529
+ const failure = error;
530
+ results.push({
531
+ command: command.command,
532
+ args,
533
+ cwd: relative(repoPath, cwd) || ".",
534
+ exitCode: Number(failure.code ?? 1) || 1,
535
+ stdout: commandOutput(failure.stdout),
536
+ stderr: commandOutput(failure.stderr ?? failure.message)
537
+ });
538
+ const verification = { status: "failed", commands: results };
539
+ throw new PlatformRepositoryVerificationError(`Repository verification failed for "${command.command}".`, verification);
540
+ }
541
+ }
542
+ return { status: "passed", commands: results };
543
+ }
544
+ function assertRepositoryWriteMode(input, options) {
545
+ const mode = input.repository.writeMode ?? "workspace";
546
+ if (mode === "direct" || mode === "pull_request") {
547
+ throw new Error(`Repository write mode "${mode}" is not enabled for platform runner operations.`);
548
+ }
549
+ if (!["workspace", "branch"].includes(mode)) {
550
+ throw new Error(`Unsupported repository write mode "${mode}".`);
551
+ }
552
+ const environment = String(options.environment ?? "").toLowerCase();
553
+ const approvalGated = input.approvalRequired === true && Boolean(input.approvalId || input.payload?.approvalId);
554
+ if (input.repository.push === true && !approvalGated) {
555
+ throw new Error("Repository push requires an approval-gated platform operation.");
556
+ }
557
+ if ((environment === "prod" || environment === "production") && input.repository.push === true) {
558
+ throw new Error("Production repository push is disabled for this platform runner slice.");
559
+ }
560
+ }
561
+ function outputHref(output) {
562
+ if (typeof output.href === "string" && output.href.trim()) return output.href.trim();
563
+ for (const key of ["record", "child", "decision"]) {
564
+ const value = output[key];
565
+ if (value && typeof value === "object" && !Array.isArray(value) && typeof value.href === "string") {
566
+ return String(value.href).trim();
567
+ }
568
+ }
569
+ return null;
570
+ }
571
+ async function executePlatformRepositoryOperation(operation, input, options) {
572
+ if (!input.repository?.cloneUrl || !input.repository.name) {
573
+ throw new Error("Repository operation requires a repository descriptor with name and cloneUrl.");
574
+ }
575
+ assertRepositoryWriteMode(input, options);
576
+ const { repoPath, branch: baseBranch } = await syncRepository(input.repository, options.workspaceRoot);
577
+ let output;
578
+ if (operation === "write_content_record") {
579
+ const collection = String(input.collection ?? "");
580
+ const record = await writeContentRecord(repoPath, collection, {
581
+ ...input.payload ?? {},
582
+ projectId: input.projectId,
583
+ teamId: input.teamId,
584
+ createdBy: input.createdBy
585
+ }, input.normalized);
586
+ output = { record };
587
+ } else if (operation === "create_related_content") {
588
+ output = await createRelatedContent(repoPath, input);
589
+ } else if (operation === "create_decision_from_proposals") {
590
+ output = await createDecisionFromProposals(repoPath, input);
591
+ } else {
592
+ throw new Error(`Unsupported repository operation "${operation}".`);
593
+ }
594
+ const gitChanged = await changedPaths(repoPath);
595
+ const changed = gitChanged.length > 0 ? gitChanged : changedPathsFromOutput(output);
596
+ const verification = await runVerificationCommands(repoPath, input.repository);
597
+ const commit = await commitIfRequested(repoPath, input.repository, input, changed);
598
+ return {
599
+ ok: true,
600
+ operation,
601
+ repository: {
602
+ key: derivePlatformRepositoryKey(input.repository),
603
+ provider: input.repository.provider ?? "git",
604
+ owner: input.repository.owner ?? null,
605
+ name: input.repository.name,
606
+ cloneUrl: input.repository.cloneUrl
607
+ },
608
+ baseBranch,
609
+ repositoryPath: repoPath,
610
+ workspacePath: options.workspaceRoot,
611
+ href: outputHref(output),
612
+ branch: commit.branch ?? baseBranch,
613
+ operationBranch: commit.branch,
614
+ commitSha: commit.commitSha,
615
+ changedPaths: changed,
616
+ verification,
617
+ pullRequest: null,
618
+ workflowRun: null,
619
+ output
620
+ };
621
+ }
622
+ export {
623
+ PLATFORM_CONTENT_COLLECTIONS,
624
+ PLATFORM_WORK_CONTENT_COLLECTIONS,
625
+ PlatformRepositoryVerificationError,
626
+ createPlatformRepositoryClaim,
627
+ derivePlatformRepositoryKey,
628
+ executePlatformRepositoryOperation,
629
+ normalizePlatformContentInput,
630
+ normalizePlatformRelationArray,
631
+ platformContentRelationPolicy,
632
+ resolvePlatformRepositoryWorkspacePath,
633
+ slugifyPlatformContent
634
+ };
@@ -356,18 +356,19 @@ export declare function syncTreeseedRailwayEnvironment({ tenantRoot, scope, dryR
356
356
  dryRun?: boolean | undefined;
357
357
  }): {
358
358
  scope: string;
359
- services: ({
360
- service: string;
359
+ services: {
360
+ service: any;
361
+ instanceKey: any;
361
362
  projectName: any;
362
363
  serviceName: any;
363
364
  serviceId: any;
364
- rootDir: string;
365
+ rootDir: any;
365
366
  baseUrl: any;
366
367
  environmentName: string;
367
- secrets: string[];
368
- variables: string[];
368
+ secrets: any;
369
+ variables: any;
369
370
  dryRun: boolean;
370
- } | null)[];
371
+ }[];
371
372
  };
372
373
  export declare function initializeTreeseedPersistentEnvironment({ tenantRoot, scope, dryRun }?: {
373
374
  scope?: string | undefined;