primitive-admin 1.1.0-alpha.85 → 1.1.0-alpha.86

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 (73) hide show
  1. package/README.md +34 -0
  2. package/assets/skill/skills/primitive-platform/SKILL.md +8 -0
  3. package/dist/src/commands/analytics.js +13 -1
  4. package/dist/src/commands/analytics.js.map +1 -1
  5. package/dist/src/commands/collections.js +648 -1
  6. package/dist/src/commands/collections.js.map +1 -1
  7. package/dist/src/commands/database-type-configs.js +1 -1
  8. package/dist/src/commands/database-type-configs.js.map +1 -1
  9. package/dist/src/commands/databases.js +11 -2
  10. package/dist/src/commands/databases.js.map +1 -1
  11. package/dist/src/commands/documents.d.ts +20 -0
  12. package/dist/src/commands/documents.js +213 -17
  13. package/dist/src/commands/documents.js.map +1 -1
  14. package/dist/src/commands/functions.js +56 -5
  15. package/dist/src/commands/functions.js.map +1 -1
  16. package/dist/src/commands/integrations.js +4 -0
  17. package/dist/src/commands/integrations.js.map +1 -1
  18. package/dist/src/commands/sync.d.ts +73 -41
  19. package/dist/src/commands/sync.js +527 -80
  20. package/dist/src/commands/sync.js.map +1 -1
  21. package/dist/src/commands/users.js +3 -1
  22. package/dist/src/commands/users.js.map +1 -1
  23. package/dist/src/lib/api-client.d.ts +93 -7
  24. package/dist/src/lib/api-client.js +72 -19
  25. package/dist/src/lib/api-client.js.map +1 -1
  26. package/dist/src/lib/collection-export.d.ts +184 -0
  27. package/dist/src/lib/collection-export.js +252 -0
  28. package/dist/src/lib/collection-export.js.map +1 -0
  29. package/dist/src/lib/config-object-descriptor.js +17 -3
  30. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  31. package/dist/src/lib/config-payload.d.ts +8 -1
  32. package/dist/src/lib/config-payload.js +29 -9
  33. package/dist/src/lib/config-payload.js.map +1 -1
  34. package/dist/src/lib/config-surface.js +11 -7
  35. package/dist/src/lib/config-surface.js.map +1 -1
  36. package/dist/src/lib/deprecation.d.ts +22 -0
  37. package/dist/src/lib/deprecation.js +43 -0
  38. package/dist/src/lib/deprecation.js.map +1 -0
  39. package/dist/src/lib/function-db-types.d.ts +14 -3
  40. package/dist/src/lib/function-db-types.js +55 -11
  41. package/dist/src/lib/function-db-types.js.map +1 -1
  42. package/dist/src/lib/function-trigger-listing.d.ts +27 -0
  43. package/dist/src/lib/function-trigger-listing.js +70 -0
  44. package/dist/src/lib/function-trigger-listing.js.map +1 -0
  45. package/dist/src/lib/generated-config-surfaces.d.ts +540 -10
  46. package/dist/src/lib/generated-config-surfaces.js +1914 -155
  47. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  48. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  49. package/dist/src/lib/generated-sdk-types.js +1 -1
  50. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  51. package/dist/src/lib/log-inspection.d.ts +18 -1
  52. package/dist/src/lib/log-inspection.js +5 -0
  53. package/dist/src/lib/log-inspection.js.map +1 -1
  54. package/dist/src/lib/prompt-cost-format.d.ts +11 -0
  55. package/dist/src/lib/prompt-cost-format.js +41 -0
  56. package/dist/src/lib/prompt-cost-format.js.map +1 -0
  57. package/dist/src/lib/prompt-schema-codegen.d.ts +53 -0
  58. package/dist/src/lib/prompt-schema-codegen.js +253 -3
  59. package/dist/src/lib/prompt-schema-codegen.js.map +1 -1
  60. package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
  61. package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
  62. package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
  63. package/dist/src/lib/swift-codegen/banners.d.ts +6 -0
  64. package/dist/src/lib/swift-codegen/banners.js +6 -0
  65. package/dist/src/lib/swift-codegen/banners.js.map +1 -1
  66. package/dist/src/lib/swift-codegen/dbGenerator.js +11 -3
  67. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  68. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +7 -0
  69. package/dist/src/lib/swift-codegen/functionGenerator.js +41 -3
  70. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
  71. package/dist/src/lib/swift-codegen/generator.js +8 -2
  72. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  73. package/package.json +2 -2
@@ -34,10 +34,18 @@ import { WORKFLOW_MODEL_DEFAULTS } from "../lib/workflow-defaults.js";
34
34
  // #2644 — the one definition per configuration object, vendored from the server.
35
35
  import { BLOB_BUCKET_TABLE, COLLECTION_TYPE_CONFIG_TABLE, CRON_TRIGGER_TABLE, DATABASE_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, GROUP_TYPE_CONFIG_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, WEBHOOK_TABLE, WORKFLOW_TABLE, configDeclaredTypeErrors, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownGroupedTomlKeys, findUnknownServerKeys, findUnknownTomlKeys, misshapenTableMessage, retiredConfigKey, spellsDecimalNumber, unrecognizedPullKeysMessage, unrecognizedPushKeysMessage, unrecognizedPushTablesMessage, warnUnrecognizedServerKeys, } from "../lib/config-surface.js";
36
36
  import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, tomlOwned, } from "../lib/config-payload.js";
37
- import { CONFIG_SURFACES, isEmptyForEmit, } from "../lib/generated-config-surfaces.js";
37
+ import { CONFIG_SURFACES, isEmptyForEmit, tomlGroupsOf, } from "../lib/generated-config-surfaces.js";
38
38
  // #3626 — the prompt KIND rule, vendored with the definitions so the CLI's
39
39
  // preflight refuses exactly what the admin routes refuse.
40
40
  import { hoistDeprecatedChatKeys, promptKindOf, resolvePromptKind, } from "../lib/generated-config-surfaces.js";
41
+ // #3798 — the `[prompt.agent]` declaration rule, vendored the same way.
42
+ import { agentDeclarationFunctionKeys, agentTestCaseRefusal, resolveAgentDeclaration, } from "../lib/generated-config-surfaces.js";
43
+ // #3799 — the meaning-preserving schema translation, vendored the same way, so
44
+ // the preflight refuses exactly the schemas the admin routes refuse.
45
+ import { agentSchemaTranslationRefusals } from "../lib/generated-config-surfaces.js";
46
+ // #3801 — the strict-output rule, vendored the same way, so the preflight
47
+ // refuses exactly the strict configs the admin routes refuse.
48
+ import { resolvePromptStrictOutput } from "../lib/generated-config-surfaces.js";
41
49
  // #3179 — the function's two halves: the header payloads and the envelope
42
50
  // `config push` sends, and the bytes `config pull` writes back.
43
51
  import { applyFunctionPull, buildFunctionConfigPush, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlIgnoredKeyWarnings, functionTomlPreflightErrors, planDocumentSchemaPull, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
@@ -1668,22 +1676,20 @@ export function buildBlobBucketUpdatePayload(bucket) {
1668
1676
  */
1669
1677
  const PROMPT_CONFIG_DEFAULT_STATUS = fieldServerDefault(PROMPT_CONFIG_TABLE, "status");
1670
1678
  /**
1671
- * The status a freshly CREATED prompt config still owes the file, or null
1672
- * (#3201).
1679
+ * #3201's post-create status patch is RETIRED — #3799 (DSO-3799-005).
1673
1680
  *
1674
- * `status` is update-only — `POST /prompts` and `POST /prompts/:id/configs`
1675
- * both assign `active` whatever the body says — so an authored
1676
- * `status = "archived"` was silently dropped on every create path: a pulled
1677
- * tree pushed from a clean clone into a fresh environment brought every
1678
- * retired config back into service. Read through the push payload builder, so
1679
- * a file that omits the key asks for the default and needs no request.
1681
+ * `status` was update-only, so `POST /prompts` and `POST /prompts/:id/configs`
1682
+ * both assigned `active` whatever the body said, and an authored
1683
+ * `status = "archived"` needed a follow-up PATCH ordered after the activation of
1684
+ * the entry the file marks live. Both create paths honor the key now
1685
+ * (`writableOn: BOTH`, validated by one rule on all three paths), so the entry
1686
+ * lands retired on its own create: the same end state, two fewer calls per
1687
+ * retired config, and no window in which the server holds a status the file
1688
+ * never asked for. That window is what made a pulled tree unpushable into a
1689
+ * fresh environment once an agent's schemas are checked against each
1690
+ * non-archived config's provider — the check fired on the transient active
1691
+ * state.
1680
1692
  */
1681
- export function createdConfigStatusPatch(tomlConfig) {
1682
- const { status } = buildPromptConfigPayload(tomlConfig, { mode: "update" });
1683
- return typeof status === "string" && status !== PROMPT_CONFIG_DEFAULT_STATUS
1684
- ? { status }
1685
- : null;
1686
- }
1687
1693
  /**
1688
1694
  * Which `[[configs]]` entry a prompt file marks as live (issue #2645).
1689
1695
  *
@@ -1791,6 +1797,11 @@ export function serializePrompt(prompt, logger) {
1791
1797
  ...projectRecordToToml(PROMPT_TABLE, prompt, {
1792
1798
  inputSchema: jsonField(promptSubject),
1793
1799
  outputSchema: jsonField(promptSubject),
1800
+ // #3798 — the agent declaration goes out as `[prompt.agent]` tables
1801
+ // (JSON-string fallback for a `null` TOML cannot spell), exactly as
1802
+ // stored: the server keeps it as authored, so what pull writes is
1803
+ // what the author pushed.
1804
+ agent: jsonField(promptSubject),
1794
1805
  }),
1795
1806
  },
1796
1807
  configs: (prompt.configs || []).map((config) => ({
@@ -1819,7 +1830,10 @@ export function serializePrompt(prompt, logger) {
1819
1830
  // `config pull` and `config diff` down with it. The JSON-string
1820
1831
  // fallback keeps the declaration whole either way.
1821
1832
  questions: jsonField(`Prompt '${prompt?.promptKey}' config '${config?.configName}'`),
1822
- }),
1833
+ },
1834
+ // #3798 — the model settings chat and agent share are written under the
1835
+ // block the prompt's kind names: `[configs.agent]` for an agent.
1836
+ { tomlGroup: promptKindOf(prompt) }),
1823
1837
  // #2645 — the live config has to be identifiable in the repo. Emitted
1824
1838
  // only on the active entry (`undefined` elsewhere so the key is omitted),
1825
1839
  // which keeps the marker unambiguous by construction.
@@ -1891,6 +1905,16 @@ export function buildPromptPushPayload(key, tomlData, { mode = "update" } = {})
1891
1905
  // dropped and leaving a stale rule live. On create it is omitted so
1892
1906
  // the push-adopt path (#1006) can still reach the server's 409.
1893
1907
  accessRule: (value) => (create ? value : (value ?? null)),
1908
+ // #3798 — the agent declaration, as authored: the server stores it
1909
+ // unchanged, so the value sent is the value pull writes back. On
1910
+ // update a deleted block CLEARS it — for an agent only: a chat or
1911
+ // decisions file carries no block to delete, and omitting the key
1912
+ // keeps its body what it was before this field existed.
1913
+ agent: (value) => create || value !== undefined
1914
+ ? value
1915
+ : promptKindOf(prompt) === "agent"
1916
+ ? null
1917
+ : undefined,
1894
1918
  // #3626, CR3626-004 — the kind NORMALIZED, exactly as the resolver
1895
1919
  // that validates it reads it. `kind = " chat "` and `kind = ""` both
1896
1920
  // mean `chat` to `resolvePromptKind`, so the server stores `chat` and
@@ -1914,6 +1938,10 @@ export function buildPromptPushPayload(key, tomlData, { mode = "update" } = {})
1914
1938
  };
1915
1939
  if (create)
1916
1940
  payload.promptKey = key;
1941
+ // #3798 — a prompt that declares no agent block sends no `agent` key at all,
1942
+ // so a chat or decisions body is byte for byte what it was.
1943
+ if (payload.agent === undefined)
1944
+ delete payload.agent;
1917
1945
  return payload;
1918
1946
  }
1919
1947
  /**
@@ -1947,9 +1975,15 @@ export function buildPromptCreatePayload(key, tomlData) {
1947
1975
  // #3626, SO3626-001 — the placeholder is a CHAT fallback. A decisions prompt
1948
1976
  // has no template at all (its input is a `state` value plus its questions),
1949
1977
  // and injecting one would store a chat key the block rule then refuses on
1950
- // every later update.
1951
- if (promptKindOf(prompt) === "decisions") {
1952
- return { ...prompt, ...seed };
1978
+ // every later update. #3798 — nor has an agent: the model sees each message
1979
+ // as sent.
1980
+ if (promptKindOf(prompt) !== "chat") {
1981
+ const body = { ...prompt, ...seed };
1982
+ for (const field of Object.keys(body)) {
1983
+ if (body[field] === undefined)
1984
+ delete body[field];
1985
+ }
1986
+ return body;
1953
1987
  }
1954
1988
  return {
1955
1989
  ...prompt,
@@ -1987,12 +2021,11 @@ const SEEDED_CONFIG_DEFERRED = ["outputSchema", "description"];
1987
2021
  * response rather than assumed, so a handler that one day honors the name
1988
2022
  * produces no needless PATCH.
1989
2023
  *
1990
- * #3201 — the entry's `status` is deferred the OTHER way and is deliberately
1991
- * not here. The create points the prompt's `activeConfigId` at this seeded
1992
- * config, so retiring it in this early patch would archive the config the
1993
- * prompt is serving from and leave it there until the marked entry is created
1994
- * and activated — a window a failed create or activation freezes. It is
1995
- * applied after activation instead (`createdConfigStatusPatch`).
2024
+ * #3201 — the entry's `status` was deferred the OTHER way, to a patch after the
2025
+ * activation, because the create pointed the prompt's `activeConfigId` at a
2026
+ * config it had forced live. #3799 (DSO-3799-005) — the create carries the
2027
+ * entry's `status` itself now, so there is no deferral either way and this patch
2028
+ * is the two fields `POST /prompts` cannot express, nothing more.
1996
2029
  */
1997
2030
  export function seededOnlyPromptConfigFields(firstConfig, seededConfigName = "default") {
1998
2031
  const full = buildPromptConfigPayload(firstConfig || {}, { mode: "update" });
@@ -2049,6 +2082,12 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
2049
2082
  mode,
2050
2083
  values: {
2051
2084
  provider: (value) => (create ? value || "openrouter" : value),
2085
+ // #3799 (DSO-3799-005) — carried on CREATE when the entry names one, and
2086
+ // omitted otherwise, so the create body for a file that says nothing
2087
+ // about status is what the server has always received (principle 5). On
2088
+ // update it still sends the declared default for an absent key, which is
2089
+ // what lets a deleted line converge (#2880 DSO-002).
2090
+ status: (value) => create ? value : (value ?? PROMPT_CONFIG_DEFAULT_STATUS),
2052
2091
  model: (value) => create ? value || "google/gemini-2.0-flash-001" : value,
2053
2092
  description: tomlOwned,
2054
2093
  systemPrompt: tomlOwned,
@@ -2065,6 +2104,9 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
2065
2104
  // the merged config refused as "both set".
2066
2105
  reasoningEffort: tomlOwned,
2067
2106
  reasoningBudget: tomlOwned,
2107
+ // #3801 — deleting `strictOutput = true` has to turn strict output off
2108
+ // server-side, or the file stops describing the request that is sent.
2109
+ strictOutput: tomlOwned,
2068
2110
  // #3626, SO3626-002 — the decisions declaration is TOML-owned on the
2069
2111
  // same terms: deleting `[configs.decisions]` has to clear `questions`
2070
2112
  // server-side, or the file stops describing what the model is asked.
@@ -2182,13 +2224,14 @@ export function configUnknownKeyError(filePath, tomlData, table) {
2182
2224
  const misplaced = trulyUnknown
2183
2225
  .map((key) => ({
2184
2226
  key,
2185
- group: table.fields.find((f) => f.tomlKey === key && f.tomlGroup)
2186
- ?.tomlGroup,
2227
+ groups: tomlGroupsOf(table.fields.find((f) => f.tomlKey === key && f.tomlGroup) ??
2228
+ {}),
2187
2229
  }))
2188
- .filter((entry) => !!entry.group);
2189
- for (const { key, group } of misplaced) {
2230
+ .filter((entry) => entry.groups.length > 0);
2231
+ for (const { key, groups } of misplaced) {
2232
+ // #3798 — a key two blocks share names both; the prompt's kind picks one.
2190
2233
  messages.push(`${filePath}: ${prefix}[0].${key} is not a [[${prefix}]] key; ` +
2191
- `write it under [${prefix}.${group}]`);
2234
+ `write it under ${groups.map((g) => `[${prefix}.${g}]`).join(" or ")}`);
2192
2235
  }
2193
2236
  const misplacedKeys = new Set(misplaced.map((m) => m.key));
2194
2237
  const stillUnknown = trulyUnknown.filter((k) => !misplacedKeys.has(k));
@@ -2636,6 +2679,9 @@ export async function runConfigPushPreflight(input) {
2636
2679
  // single pull-then-push clears the notice. Printed even under `--dry-run`,
2637
2680
  // which never reaches the apply — the notice is about the FILE.
2638
2681
  const preflightPromptsDir = join(configDir, "prompts");
2682
+ // #3798 — the functions an agent's references are checked against, read
2683
+ // once and only when a selected prompt is an agent that names one.
2684
+ let agentFunctions = null;
2639
2685
  if (existsSync(preflightPromptsDir)) {
2640
2686
  for (const file of selectFiles("prompt", preflightPromptsDir, readdirSync(preflightPromptsDir).filter((f) => f.endsWith(".toml")))) {
2641
2687
  const filePath = join(preflightPromptsDir, file);
@@ -2650,8 +2696,36 @@ export async function runConfigPushPreflight(input) {
2650
2696
  const notice = promptFlatChatKeyWarning(filePath, tomlData);
2651
2697
  if (notice)
2652
2698
  warn(` ${notice}`);
2699
+ // #3798 — an agent's tool, turn-context and history functions must
2700
+ // exist, and a tool's must declare both schemas, before anything is
2701
+ // applied. Bounded by `--only` like the rest of this loop.
2702
+ if (promptKindOf(tomlData?.prompt) === "agent" &&
2703
+ agentDeclarationFunctionKeys(tomlData?.prompt?.agent).length > 0) {
2704
+ if (!agentFunctions) {
2705
+ let live = null;
2706
+ try {
2707
+ live = (await client.listFunctions(resolvedAppId)).items;
2708
+ }
2709
+ catch {
2710
+ live = null;
2711
+ }
2712
+ agentFunctions = agentFunctionFactsForPush({
2713
+ configDir,
2714
+ pushSelection,
2715
+ live,
2716
+ });
2717
+ }
2718
+ for (const message of promptAgentFunctionErrors(filePath, tomlData, agentFunctions)) {
2719
+ preflightValidationErrors.push(` ${message}`);
2720
+ }
2721
+ }
2653
2722
  }
2654
2723
  }
2724
+ // #3798 — a test case on an agent is refused by the admin routes; say so
2725
+ // here, before any other file is applied.
2726
+ for (const message of promptAgentSidecarErrors(configDir)) {
2727
+ preflightValidationErrors.push(` ${message}`);
2728
+ }
2655
2729
  // The `<key>.tests/*.toml` sidecars, which sit beside the block they
2656
2730
  // test rather than in a type directory of their own. Same posture,
2657
2731
  // reached through the registry's `test-case` surface — and through the
@@ -2691,6 +2765,41 @@ export async function runConfigPushPreflight(input) {
2691
2765
  * whose spelling is not the declared type. A file that does not parse is left
2692
2766
  * to the per-type loop that reads it, which names the parse error itself.
2693
2767
  */
2768
+ /**
2769
+ * The blocks one `[[configs]]` entry carries that its prompt's kind does not
2770
+ * name — #3798. An agent's entry may carry only `[configs.agent]`; a chat or
2771
+ * decisions entry may not carry it. `movedFlat` are the deprecated flat chat
2772
+ * keys the hoist moved under `chat`, named as the author wrote them.
2773
+ *
2774
+ * Only the blocks the other arms of `resolvePromptKind` cannot see are checked
2775
+ * here: a decisions block on a chat entry, and chat keys on a decisions entry,
2776
+ * stay the resolver's refusals with their #3626 wording.
2777
+ */
2778
+ function promptEntryBlockErrors(kind, entry, movedFlat) {
2779
+ const has = (block) => entry != null && typeof entry === "object" && entry[block] !== undefined;
2780
+ const errors = [];
2781
+ if (kind === "agent") {
2782
+ if (movedFlat.length > 0) {
2783
+ errors.push(`sets ${movedFlat.join(", ")} flat — the deprecated [configs.chat] ` +
2784
+ 'spelling — but prompt kind is "agent", so it may carry only a ' +
2785
+ "[configs.agent] block; write them under [configs.agent]");
2786
+ }
2787
+ for (const block of ["chat", "decisions"]) {
2788
+ if (!has(block))
2789
+ continue;
2790
+ errors.push(`carries a [configs.${block}] block, but prompt kind is "agent", so ` +
2791
+ "it may carry only a [configs.agent] block (systemPrompt, " +
2792
+ "temperature, topP, maxTokens, reasoningEffort, reasoningBudget)");
2793
+ }
2794
+ return errors;
2795
+ }
2796
+ if (has("agent")) {
2797
+ errors.push(`carries a [configs.agent] block, but prompt kind is "${kind}", so it ` +
2798
+ `may carry only a [configs.${kind}] block. An agent is its own ` +
2799
+ 'prompt: create it under a new key with kind = "agent"');
2800
+ }
2801
+ return errors;
2802
+ }
2694
2803
  /**
2695
2804
  * A prompt file's KIND errors — #3626, decidable from the file alone.
2696
2805
  *
@@ -2702,15 +2811,94 @@ export async function runConfigPushPreflight(input) {
2702
2811
  * the server's cannot disagree — and this one lands in the preflight, before
2703
2812
  * the first mutating call.
2704
2813
  */
2814
+ /**
2815
+ * The `[[configs]]` entries of a prompt file as the agent rules read them —
2816
+ * #3799: which provider each named configuration runs on, and whether the file
2817
+ * retires it.
2818
+ *
2819
+ * An archived entry is skipped by the translation rule (a config the server does
2820
+ * not run cannot make the declaration untranslatable), which is what lets a
2821
+ * pulled tree keep a retired config on a provider the current schemas could not
2822
+ * reach. `status` is read through the same default the surface declares, so an
2823
+ * entry that omits the key is live.
2824
+ */
2825
+ function promptFileConfigFacts(entries) {
2826
+ return (entries || []).map((entry, index) => ({
2827
+ configName: String(entry?.name ?? `configs[${index}]`),
2828
+ provider: String(entry?.provider ?? ""),
2829
+ status: String(entry?.status ?? PROMPT_CONFIG_DEFAULT_STATUS),
2830
+ }));
2831
+ }
2705
2832
  export function promptKindFileErrors(filePath, tomlData) {
2706
2833
  const entries = Array.isArray(tomlData?.configs) ? tomlData.configs : [];
2707
2834
  const errors = [];
2835
+ const fileKind = promptKindOf(tomlData?.prompt);
2836
+ // #3798 — the `[prompt.agent]` declaration's file-decidable half: its shape,
2837
+ // and `[prompt].outputSchema` on an agent. The function half needs app state
2838
+ // and runs in the push preflight (`promptAgentFunctionErrors`).
2839
+ const declaration = resolveAgentDeclaration({
2840
+ kind: tomlData?.prompt?.kind,
2841
+ agent: tomlData?.prompt?.agent,
2842
+ outputSchema: tomlData?.prompt?.outputSchema,
2843
+ });
2844
+ if (declaration.ok === false) {
2845
+ for (const refusal of declaration.refusals) {
2846
+ errors.push(`${filePath}: ${refusal.message}`);
2847
+ }
2848
+ }
2849
+ else if (fileKind === "agent") {
2850
+ // #3799 — the schemas a CLIENT tool and an event declare are in the file,
2851
+ // so whether each provider the file configures can carry them without
2852
+ // losing meaning is file-decidable too: `config diff` reports the same row
2853
+ // as `config push`. A server tool's schemas are its FUNCTION's and need app
2854
+ // state, so they are checked in the push preflight
2855
+ // (`promptAgentFunctionErrors`). Only after the shape refusals above, so a
2856
+ // malformed declaration is not also reported as untranslatable.
2857
+ for (const refusal of agentSchemaTranslationRefusals({
2858
+ declaration: tomlData?.prompt?.agent,
2859
+ configs: promptFileConfigFacts(entries),
2860
+ })) {
2861
+ errors.push(`${filePath}: ${refusal.message}`);
2862
+ }
2863
+ }
2708
2864
  for (const [index, entry] of entries.entries()) {
2709
2865
  const hoisted = hoistDeprecatedChatKeys(entry);
2710
2866
  for (const key of hoisted.conflicts) {
2711
2867
  errors.push(`${filePath}: configs[${index}].${key} is set both flat and under ` +
2712
2868
  `[configs.chat]; keep the [configs.chat] spelling`);
2713
2869
  }
2870
+ // #3798 — the block rule for three kinds, stated per entry: an entry may
2871
+ // carry only the block its prompt's kind names. An agent's `[configs.agent]`
2872
+ // holds keys chat also holds, so a stray block would otherwise be read (or
2873
+ // ignored) in silence by the payload builder.
2874
+ const blockErrors = promptEntryBlockErrors(fileKind, entry, hoisted.moved).map((message) => `${filePath}: configs[${index}] ${message}`);
2875
+ if (blockErrors.length > 0) {
2876
+ errors.push(...blockErrors);
2877
+ continue;
2878
+ }
2879
+ if (fileKind === "agent") {
2880
+ const agent = hoisted.entry.agent ?? {};
2881
+ const resolution = resolvePromptKind({
2882
+ kind: "agent",
2883
+ provider: hoisted.entry.provider,
2884
+ model: hoisted.entry.model,
2885
+ config: {
2886
+ systemPrompt: agent.systemPrompt,
2887
+ temperature: agent.temperature,
2888
+ topP: agent.topP,
2889
+ maxTokens: agent.maxTokens,
2890
+ reasoningEffort: agent.reasoningEffort,
2891
+ reasoningBudget: agent.reasoningBudget,
2892
+ // #3801 — a chat key: named, so the refusal says which key an agent
2893
+ // does not take (§D1: an agent has no output schema).
2894
+ strictOutput: agent.strictOutput,
2895
+ },
2896
+ });
2897
+ if (resolution.ok === false) {
2898
+ errors.push(`${filePath}: configs[${index}] — ${resolution.error}`);
2899
+ }
2900
+ continue;
2901
+ }
2714
2902
  const chat = hoisted.entry.chat ?? {};
2715
2903
  const decisions = hoisted.entry.decisions ?? {};
2716
2904
  // #3626 — the FIRST entry of a chat prompt carries the create seed's
@@ -2726,7 +2914,7 @@ export function promptKindFileErrors(filePath, tomlData) {
2726
2914
  // `userPromptTemplate`, so reading absence more narrowly than the builder
2727
2915
  // does refused the file this CLI had just written, for a value the create
2728
2916
  // would have replaced with the placeholder anyway.
2729
- const seedsTemplate = index === 0 && promptKindOf(tomlData?.prompt) !== "decisions";
2917
+ const seedsTemplate = index === 0 && fileKind === "chat";
2730
2918
  const userPromptTemplate = seedsTemplate
2731
2919
  ? chat.userPromptTemplate || "{{ input }}"
2732
2920
  : chat.userPromptTemplate;
@@ -2744,15 +2932,203 @@ export function promptKindFileErrors(filePath, tomlData) {
2744
2932
  outputSchema: chat.outputSchema,
2745
2933
  reasoningEffort: chat.reasoningEffort,
2746
2934
  reasoningBudget: chat.reasoningBudget,
2935
+ strictOutput: chat.strictOutput,
2747
2936
  questions: decisions.questions,
2748
2937
  },
2749
2938
  });
2750
2939
  if (resolution.ok === false) {
2751
2940
  errors.push(`${filePath}: configs[${index}] — ${resolution.error}`);
2941
+ continue;
2942
+ }
2943
+ // #3801 — strict output, file-decidable: the entry's own keys, the file's
2944
+ // `[prompt].outputSchema` and the entry's status (an archived entry is not
2945
+ // held to the prompt schema, DSO-3801-002). The route runs the same rule
2946
+ // against what the server holds.
2947
+ if (resolution.kind === "chat") {
2948
+ const strict = resolvePromptStrictOutput({
2949
+ provider: hoisted.entry.provider,
2950
+ strictOutput: chat.strictOutput,
2951
+ outputFormat: chat.outputFormat,
2952
+ configOutputSchema: chat.outputSchema,
2953
+ promptOutputSchema: tomlData?.prompt?.outputSchema,
2954
+ status: hoisted.entry.status ?? PROMPT_CONFIG_DEFAULT_STATUS,
2955
+ });
2956
+ if (strict.ok === false) {
2957
+ errors.push(`${filePath}: configs[${index}] — ${strict.error} (${strict.code})`);
2958
+ }
2752
2959
  }
2753
2960
  }
2754
2961
  return errors;
2755
2962
  }
2963
+ /** A schema slot counts as declared when it holds anything but nothing. */
2964
+ function declaresSchema(value) {
2965
+ if (value === undefined || value === null)
2966
+ return false;
2967
+ return !(typeof value === "string" && value.trim() === "");
2968
+ }
2969
+ /**
2970
+ * The functions an agent's references are checked against at push — #3798,
2971
+ * DSO-3798-002.
2972
+ *
2973
+ * `config push` applies prompts, then (for agents) the server functions leg,
2974
+ * so a reference may be satisfied by a function that is live already or by one
2975
+ * this push creates. What the push BELIEVES is therefore the live listing
2976
+ * overlaid by the function files this push SELECTS (`--only`, every file when
2977
+ * unscoped): a selected file's schemas are what the server will hold after the
2978
+ * push, so it wins over the live row. An UNSELECTED file is not read at all —
2979
+ * its edits will not be applied, and a function only it declares will not
2980
+ * exist afterwards.
2981
+ *
2982
+ * `live` is `null` when the listing could not be fetched: the selected files
2983
+ * still count, and `liveListed` lets the refusal say why every other reference
2984
+ * could not be confirmed. A live row that is archived is a tombstone, not a
2985
+ * function; an inactive one is accepted (the turn engine re-checks
2986
+ * availability on every call).
2987
+ */
2988
+ export function agentFunctionFactsForPush(input) {
2989
+ const facts = {};
2990
+ for (const row of input.live ?? []) {
2991
+ const key = String(row?.functionKey ?? "");
2992
+ if (!key || row?.status === "archived")
2993
+ continue;
2994
+ facts[key] = {
2995
+ hasInputSchema: declaresSchema(row.inputSchema),
2996
+ hasOutputSchema: declaresSchema(row.outputSchema),
2997
+ // #3799 — the schemas themselves, as stored (a JSON string from the API,
2998
+ // an object from a tree file), so a server tool's two schemas can be
2999
+ // translated for each of the agent's providers.
3000
+ inputSchema: row.inputSchema,
3001
+ outputSchema: row.outputSchema,
3002
+ };
3003
+ }
3004
+ const functionsDir = join(input.configDir, "functions");
3005
+ if (existsSync(functionsDir)) {
3006
+ for (const file of filterFilesBySelection(input.pushSelection, "function", functionsDir, readdirSync(functionsDir).filter(isFunctionDeclarationFile))) {
3007
+ let table;
3008
+ try {
3009
+ table = parseTomlFile(join(functionsDir, file))?.function ?? {};
3010
+ }
3011
+ catch {
3012
+ // Reported by the function preflight with its own message.
3013
+ continue;
3014
+ }
3015
+ const key = table.key || basename(file, ".toml");
3016
+ facts[key] = {
3017
+ hasInputSchema: declaresSchema(table.inputSchema),
3018
+ hasOutputSchema: declaresSchema(table.outputSchema),
3019
+ // #3799 — a SELECTED file's schemas are what the server will hold after
3020
+ // this push, so they are what the translation is checked against.
3021
+ inputSchema: table.inputSchema,
3022
+ outputSchema: table.outputSchema,
3023
+ };
3024
+ }
3025
+ }
3026
+ return { facts, liveListed: input.live !== null };
3027
+ }
3028
+ /**
3029
+ * The function half of an agent's declaration — #3798: every tool, turn
3030
+ * context and history function that the functions this push believes
3031
+ * (`agentFunctionFactsForPush`) do not have, and every tool function lacking an
3032
+ * input or output schema. The same `resolveAgentDeclaration` the admin routes
3033
+ * run; only its function refusals are reported here, because the collector
3034
+ * (`promptKindFileErrors`) already reported the file-decidable ones.
3035
+ */
3036
+ export function promptAgentFunctionErrors(filePath, tomlData, known) {
3037
+ if (promptKindOf(tomlData?.prompt) !== "agent")
3038
+ return [];
3039
+ const result = resolveAgentDeclaration({
3040
+ kind: "agent",
3041
+ agent: tomlData?.prompt?.agent,
3042
+ functions: known.facts,
3043
+ });
3044
+ const errors = result.ok === false
3045
+ ? result.refusals
3046
+ .filter((refusal) => refusal.code === "AGENT_TOOL_FUNCTION_MISSING" ||
3047
+ refusal.code === "AGENT_TOOL_FUNCTION_SCHEMA_MISSING")
3048
+ .map((refusal) => refusal.code === "AGENT_TOOL_FUNCTION_MISSING" && !known.liveListed
3049
+ ? `${filePath}: ${refusal.message} (the app's live functions could ` +
3050
+ "not be listed, so only the function files this push applies were " +
3051
+ "checked)"
3052
+ : `${filePath}: ${refusal.message}`)
3053
+ : [];
3054
+ // #3799 — a SERVER tool's schemas are its function's, so translating them
3055
+ // needs the facts this push believes (the live listing overlaid by the
3056
+ // function files it selects, DSO-3798-002). The client-tool and event half is
3057
+ // file-decidable and reported by `promptKindFileErrors`, which `config diff`
3058
+ // runs too; passing the facts here means only the server-tool refusals are
3059
+ // new, because that collector already saw the rest.
3060
+ const entries = Array.isArray(tomlData?.configs) ? tomlData.configs : [];
3061
+ const configs = promptFileConfigFacts(entries);
3062
+ const fileDecidable = new Set(agentSchemaTranslationRefusals({
3063
+ declaration: tomlData?.prompt?.agent,
3064
+ configs,
3065
+ }).map((refusal) => refusal.message));
3066
+ for (const refusal of agentSchemaTranslationRefusals({
3067
+ declaration: tomlData?.prompt?.agent,
3068
+ functions: known.facts,
3069
+ configs,
3070
+ })) {
3071
+ if (fileDecidable.has(refusal.message))
3072
+ continue;
3073
+ errors.push(`${filePath}: ${refusal.message}`);
3074
+ }
3075
+ return errors;
3076
+ }
3077
+ /**
3078
+ * The order `config push` applies prompt files in — #3798. An agent names
3079
+ * server functions that must exist when it is created, and the push applies
3080
+ * prompts before functions, so agent files go AFTER the server-functions leg;
3081
+ * every other file keeps its place. A file that does not parse stays first, so
3082
+ * the leg reports it where it always has.
3083
+ */
3084
+ export function partitionPromptFilesForPush(promptsDir, files) {
3085
+ const beforeFunctions = [];
3086
+ const afterFunctions = [];
3087
+ for (const file of files) {
3088
+ let kind = "chat";
3089
+ try {
3090
+ kind = promptKindOf(parseTomlFile(join(promptsDir, file))?.prompt);
3091
+ }
3092
+ catch {
3093
+ beforeFunctions.push(file);
3094
+ continue;
3095
+ }
3096
+ (kind === "agent" ? afterFunctions : beforeFunctions).push(file);
3097
+ }
3098
+ return { beforeFunctions, afterFunctions };
3099
+ }
3100
+ /**
3101
+ * Every `prompts/<key>.tests/*.toml` sidecar whose prompt is an agent — #3798
3102
+ * (§D1: prompt test cases are refused for the kind in v1). The admin test-case
3103
+ * routes refuse the same case with `AGENT_TEST_CASE_NOT_SUPPORTED`; this says
3104
+ * so before anything is applied.
3105
+ */
3106
+ export function promptAgentSidecarErrors(configDir) {
3107
+ const promptsDir = join(configDir, "prompts");
3108
+ const errors = [];
3109
+ for (const filePath of testCaseTomlFiles(configDir)) {
3110
+ const parent = basename(join(filePath, ".."));
3111
+ if (!filePath.startsWith(promptsDir) || !parent.endsWith(".tests"))
3112
+ continue;
3113
+ const key = parent.slice(0, -".tests".length);
3114
+ const owner = join(promptsDir, `${key}.toml`);
3115
+ if (!existsSync(owner))
3116
+ continue;
3117
+ let tomlData;
3118
+ try {
3119
+ tomlData = parseTomlFile(owner);
3120
+ }
3121
+ catch {
3122
+ continue;
3123
+ }
3124
+ if (promptKindOf(tomlData?.prompt) !== "agent")
3125
+ continue;
3126
+ const promptKey = tomlData?.prompt?.key || key;
3127
+ errors.push(`${filePath.slice(configDir.length + 1)}: ` +
3128
+ agentTestCaseRefusal(promptKey, "subject").message);
3129
+ }
3130
+ return errors;
3131
+ }
2756
3132
  /**
2757
3133
  * The immutable-`kind` refusal for a prompt update the comparison gate did not
2758
3134
  * decide — #3626, CR3626-001.
@@ -10772,8 +11148,14 @@ What a push guarantees (two stages):
10772
11148
  }
10773
11149
  // Process prompts
10774
11150
  const promptsDir = join(configDir, "prompts");
10775
- if (existsSync(promptsDir)) {
10776
- const files = selectFiles("prompt", promptsDir, readdirSync(promptsDir).filter((f) => f.endsWith(".toml")));
11151
+ // #3798 — the prompt leg as one function, called twice. `config push`
11152
+ // applies prompts BEFORE server functions, and an agent's tools name
11153
+ // functions the server must hold when the agent is created, so a tree
11154
+ // that pushes an agent together with its tool functions would be
11155
+ // refused on its first push. Chat and decisions files keep their place;
11156
+ // agent files are applied after the server-functions leg below
11157
+ // (`deferredAgentPromptFiles`). Nothing inside the leg changed.
11158
+ const pushPromptFiles = async (files) => {
10777
11159
  // #2869 — prompts join the live-state reconcile (#2731 B3). #2748
10778
11160
  // gave the type diff's comparator and left push on the manifest byte
10779
11161
  // hash, so the two commands answered "changed?" from different
@@ -10976,6 +11358,79 @@ What a push guarantees (two stages):
10976
11358
  // (mirrors `applyWorkflowUpdate`). The adopt branch seeds the
10977
11359
  // sync-state entity record before calling this so the stamp lands.
10978
11360
  const applyPromptUpdate = async (promptId, expectedModifiedAt) => {
11361
+ // #3799 (CSO-3799-005) — an agent's declaration is checked against
11362
+ // every config the server holds non-archived, so a file that
11363
+ // retires a config AND declares a schema that config's provider
11364
+ // cannot carry must retire it first: sent the other way round,
11365
+ // the prompt PATCH is refused for a config the file has already
11366
+ // archived, and no re-run changes the order. Only the entries
11367
+ // the file archives and the server does not yet — every other
11368
+ // config update still follows the prompt, and decisions prompts
11369
+ // keep their sequence. A config PATCH does not move the
11370
+ // prompt's `modifiedAt`, so the concurrency guard below still
11371
+ // holds.
11372
+ //
11373
+ // #3801 (DSO-3801-001) — the same staging, with a second admission
11374
+ // rule. The admin API refuses clearing a CHAT prompt's
11375
+ // `[prompt].outputSchema` while a config it holds non-archived is
11376
+ // strict, so a file that clears the schema AND turns strict output
11377
+ // off (or retires the config) would be refused on every push if the
11378
+ // prompt went first. Those config updates go ahead of the prompt
11379
+ // PATCH, with the payload they would have been sent anyway. The
11380
+ // read this needs is the one the name→id mapping below makes,
11381
+ // moved ahead of the PATCH, so the request count does not grow.
11382
+ const stagedFirst = new Set();
11383
+ const fileKind = promptKindOf(tomlData?.prompt);
11384
+ const retiring = configs.filter((c) => c?.name &&
11385
+ String(c?.status ?? "").trim().toLowerCase() === "archived");
11386
+ const stagesRetiringAgent = fileKind === "agent" && retiring.length > 0;
11387
+ const clearsPromptSchema = fileKind === "chat" &&
11388
+ !declaresSchema(tomlData?.prompt?.outputSchema);
11389
+ const current = stagesRetiringAgent || clearsPromptSchema
11390
+ ? await client.getPrompt(resolvedAppId, promptId)
11391
+ : null;
11392
+ const liveServerConfig = (name) => {
11393
+ const found = (current?.configs ?? []).find((c) => c.configName === name);
11394
+ return found && String(found.status ?? "active") !== "archived"
11395
+ ? found
11396
+ : null;
11397
+ };
11398
+ const staged = [];
11399
+ if (stagesRetiringAgent) {
11400
+ for (const tomlConfig of retiring) {
11401
+ const serverConfig = liveServerConfig(tomlConfig.name);
11402
+ if (serverConfig)
11403
+ staged.push({ tomlConfig, serverConfig });
11404
+ }
11405
+ }
11406
+ if (clearsPromptSchema && declaresSchema(current?.outputSchema)) {
11407
+ for (const tomlConfig of configs) {
11408
+ if (!tomlConfig?.name)
11409
+ continue;
11410
+ const serverConfig = liveServerConfig(tomlConfig.name);
11411
+ if (!serverConfig || serverConfig.strictOutput !== true)
11412
+ continue;
11413
+ const chat = hoistDeprecatedChatKeys(tomlConfig).entry.chat ?? {};
11414
+ const retired = String(tomlConfig?.status ?? "").trim().toLowerCase() ===
11415
+ "archived";
11416
+ // An entry that stays strict AND live is refused in preflight
11417
+ // (no schema to send); it is never staged.
11418
+ if (chat.strictOutput === true && !retired)
11419
+ continue;
11420
+ staged.push({ tomlConfig, serverConfig });
11421
+ }
11422
+ }
11423
+ for (const { tomlConfig, serverConfig } of staged) {
11424
+ if (stagedFirst.has(tomlConfig.name))
11425
+ continue;
11426
+ try {
11427
+ await client.updatePromptConfig(resolvedAppId, promptId, serverConfig.configId, buildPromptConfigPayload(tomlConfig, { mode: "update" }));
11428
+ }
11429
+ catch (err) {
11430
+ throw wrapPromptConfigError(err, "update", key, tomlConfig.name);
11431
+ }
11432
+ stagedFirst.add(tomlConfig.name);
11433
+ }
10979
11434
  const updated = await client.updatePrompt(resolvedAppId, promptId, buildPromptPushPayload(key, tomlData, { mode: "update" }), expectedModifiedAt);
10980
11435
  info(` Updated prompt: ${key}`);
10981
11436
  // Record the `modifiedAt` this PATCH just caused, so a later
@@ -10998,7 +11453,10 @@ What a push guarantees (two stages):
10998
11453
  try {
10999
11454
  // Fetch full prompt to get config name→ID mappings
11000
11455
  // (updatePrompt response doesn't include configs)
11001
- const fullPrompt = await client.getPrompt(resolvedAppId, promptId);
11456
+ // #3801 — the read made ahead of the prompt PATCH when the
11457
+ // staging needed it (neither the PATCH nor a staged config
11458
+ // update moves a config's id or the activation).
11459
+ const fullPrompt = current ?? (await client.getPrompt(resolvedAppId, promptId));
11002
11460
  if (fullPrompt?.configs) {
11003
11461
  for (const config of fullPrompt.configs) {
11004
11462
  promptConfigNameToId.set(`${key}#${config.configName}`, config.configId);
@@ -11012,6 +11470,10 @@ What a push guarantees (two stages):
11012
11470
  continue;
11013
11471
  const serverConfig = fullPrompt.configs.find((c) => c.configName === configName);
11014
11472
  if (serverConfig) {
11473
+ // Staged ahead of the prompt PATCH, with this same
11474
+ // payload (#3799 CSO-3799-005, #3801 DSO-3801-001).
11475
+ if (stagedFirst.has(configName))
11476
+ continue;
11015
11477
  // Update existing config
11016
11478
  try {
11017
11479
  await client.updatePromptConfig(resolvedAppId, promptId, serverConfig.configId, buildPromptConfigPayload(tomlConfig, { mode: "update" }));
@@ -11032,18 +11494,9 @@ What a push guarantees (two stages):
11032
11494
  }
11033
11495
  if (newConfig?.configId) {
11034
11496
  promptConfigNameToId.set(`${key}#${configName}`, newConfig.configId);
11035
- // #3201 — the create assigned `active`; a file that
11036
- // retired this entry says so in the only mode the
11037
- // server accepts the key in.
11038
- const statusPatch = createdConfigStatusPatch(tomlConfig);
11039
- if (statusPatch) {
11040
- try {
11041
- await client.updatePromptConfig(resolvedAppId, promptId, newConfig.configId, statusPatch);
11042
- }
11043
- catch (err) {
11044
- throw wrapPromptConfigError(err, "update", key, configName);
11045
- }
11046
- }
11497
+ // #3799 — the create body carried the entry's own
11498
+ // `status`, so there is no follow-up patch (#3201's is
11499
+ // retired).
11047
11500
  }
11048
11501
  }
11049
11502
  }
@@ -11377,21 +11830,11 @@ What a push guarantees (two stages):
11377
11830
  throw wrapPromptConfigError(err, "create", key, extraName);
11378
11831
  }
11379
11832
  if (extraCreated?.configId) {
11380
- const configName = extraName;
11381
- promptConfigNameToId.set(`${key}#${configName}`, extraCreated.configId);
11382
- // #3201 — as on the update path: the create ignored
11383
- // the entry's `status`, so a retired one is applied
11384
- // here or the fresh environment runs a config the
11385
- // file retired.
11386
- const statusPatch = createdConfigStatusPatch(extraConfig);
11387
- if (statusPatch) {
11388
- try {
11389
- await client.updatePromptConfig(resolvedAppId, created.promptId, extraCreated.configId, statusPatch);
11390
- }
11391
- catch (err) {
11392
- throw wrapPromptConfigError(err, "update", key, configName);
11393
- }
11394
- }
11833
+ promptConfigNameToId.set(`${key}#${extraName}`, extraCreated.configId);
11834
+ // #3799 — as on the update path: the create carried the
11835
+ // entry's `status`, so a retired entry is retired when
11836
+ // it is created and the fresh environment never runs a
11837
+ // config the file retired.
11395
11838
  }
11396
11839
  }
11397
11840
  info(` Created ${configs.length - 1} additional config(s) for prompt: ${key}`);
@@ -11423,25 +11866,17 @@ What a push guarantees (two stages):
11423
11866
  activated = true;
11424
11867
  }
11425
11868
  }
11426
- // #3201 — the seeded config's own `status`, applied LAST for
11427
- // the same reason activation runs after the extra creates:
11428
- // the create points `activeConfigId` at this config, so a
11429
- // file that retires it is asking for a state that only makes
11430
- // sense once the entry it marks active exists and is live.
11431
- // Retiring it earlier would leave the prompt serving from an
11432
- // archived config for the length of the remaining calls, and
11433
- // permanently if one of them failed.
11434
- const seededStatusPatch = seededConfigId
11435
- ? createdConfigStatusPatch(firstConfig || {})
11436
- : null;
11437
- if (created?.promptId && seededConfigId && seededStatusPatch) {
11438
- try {
11439
- await client.updatePromptConfig(resolvedAppId, created.promptId, seededConfigId, seededStatusPatch);
11440
- }
11441
- catch (err) {
11442
- throw wrapPromptConfigError(err, "update", key, firstConfigPatch?.configName || seededConfig?.configName);
11443
- }
11444
- }
11869
+ // #3799 (DSO-3799-005) — the seeded config's `status` is in
11870
+ // the create body (`POST /prompts` honors it now), so #3201's
11871
+ // trailing patch is gone. Activation still runs after every
11872
+ // create (#2645), so the marked entry is what the prompt ends
11873
+ // up serving; when the FIRST entry is the retired one, the
11874
+ // seeded config the prompt points at is created archived and
11875
+ // the activation moves the pointer off it. A push that dies
11876
+ // between the two leaves a prompt pointing at an archived
11877
+ // config — recovered by re-running the push, and the price of
11878
+ // never creating a config in a state the file did not ask for
11879
+ // (which is what an agent's schema check fires on).
11445
11880
  // Stamp sync state ONLY after all create work (base prompt +
11446
11881
  // every extra config) succeeded. If an extra-config create
11447
11882
  // above threw, we never reach here, so the file hash is not
@@ -11477,6 +11912,13 @@ What a push guarantees (two stages):
11477
11912
  }
11478
11913
  }
11479
11914
  }
11915
+ };
11916
+ let deferredAgentPromptFiles = [];
11917
+ if (existsSync(promptsDir)) {
11918
+ const files = selectFiles("prompt", promptsDir, readdirSync(promptsDir).filter((f) => f.endsWith(".toml")));
11919
+ const ordered = partitionPromptFilesForPush(promptsDir, files);
11920
+ deferredAgentPromptFiles = ordered.afterFunctions;
11921
+ await pushPromptFiles(ordered.beforeFunctions);
11480
11922
  }
11481
11923
  // Process transforms (Rhai scripts) — issue #892, slice 7 of #783.
11482
11924
  //
@@ -13949,6 +14391,11 @@ What a push guarantees (two stages):
13949
14391
  }
13950
14392
  }
13951
14393
  }
14394
+ // #3798 — the agent prompts, now that every function they name that
14395
+ // this push applies is on the server (see `pushPromptFiles`).
14396
+ if (deferredAgentPromptFiles.length > 0) {
14397
+ await pushPromptFiles(deferredAgentPromptFiles);
14398
+ }
13952
14399
  // Process group type configs
13953
14400
  const groupTypeConfigsDir = join(configDir, "group-type-configs");
13954
14401
  if (existsSync(groupTypeConfigsDir)) {