@orchestraworks/cli 0.5.2 → 0.5.3

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 (103) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/api/compute-provider-check.d.ts +29 -0
  3. package/dist/cli/api/compute-provider-check.js +207 -0
  4. package/dist/cli/api/tail.d.ts +66 -0
  5. package/dist/cli/api/tail.js +564 -0
  6. package/dist/cli/main.d.ts +5 -1
  7. package/dist/cli/main.js +22 -0
  8. package/dist/constructs/agent.d.ts +10 -0
  9. package/dist/constructs/agent.js +39 -4
  10. package/dist/constructs/environment.d.ts +29 -1
  11. package/dist/constructs/environment.js +28 -1
  12. package/dist/constructs/skill.d.ts +6 -1
  13. package/dist/constructs/skill.js +21 -2
  14. package/dist/engine/reconcile.js +15 -1
  15. package/dist/generated/cli-commands.json +842 -0
  16. package/dist/generated/managed-skills.d.ts +2 -0
  17. package/dist/generated/managed-skills.js +13 -0
  18. package/dist/index.d.ts +1 -1
  19. package/docs/REFERENCE.md +21 -1
  20. package/node_modules/@orchestraworks/client/dist/esm/models/ComputeProvider.d.ts +3 -3
  21. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +13 -0
  22. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +5 -0
  23. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +13 -0
  24. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +5 -0
  25. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHook.d.ts +38 -0
  26. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHook.js +45 -0
  27. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHooks.d.ts +39 -0
  28. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHooks.js +44 -0
  29. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHooksRequest.d.ts +39 -0
  30. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHooksRequest.js +44 -0
  31. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +2 -1
  32. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +2 -1
  33. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +19 -0
  34. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.js +7 -0
  35. package/node_modules/@orchestraworks/client/dist/esm/models/Machine.d.ts +7 -1
  36. package/node_modules/@orchestraworks/client/dist/esm/models/Machine.js +2 -0
  37. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +13 -0
  38. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +5 -0
  39. package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +3 -0
  40. package/node_modules/@orchestraworks/client/dist/esm/models/index.js +3 -0
  41. package/node_modules/@orchestraworks/client/dist/models/ComputeProvider.d.ts +3 -3
  42. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +13 -0
  43. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +5 -0
  44. package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +13 -0
  45. package/node_modules/@orchestraworks/client/dist/models/Environment.js +5 -0
  46. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHook.d.ts +38 -0
  47. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHook.js +52 -0
  48. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHooks.d.ts +39 -0
  49. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHooks.js +51 -0
  50. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHooksRequest.d.ts +39 -0
  51. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHooksRequest.js +51 -0
  52. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +2 -1
  53. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +2 -1
  54. package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +19 -0
  55. package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.js +7 -0
  56. package/node_modules/@orchestraworks/client/dist/models/Machine.d.ts +7 -1
  57. package/node_modules/@orchestraworks/client/dist/models/Machine.js +2 -0
  58. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +13 -0
  59. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +5 -0
  60. package/node_modules/@orchestraworks/client/dist/models/index.d.ts +3 -0
  61. package/node_modules/@orchestraworks/client/dist/models/index.js +3 -0
  62. package/node_modules/@orchestraworks/client/package.json +1 -1
  63. package/package.json +4 -4
  64. package/skills/orchestra/SKILL.md +167 -42
  65. package/skills/orchestra-fdk/SKILL.md +112 -545
  66. package/skills/orchestra-fdk/references/authoring.md +29 -2
  67. package/skills/orchestra-fdk/references/environment.md +295 -41
  68. package/skills/orchestra-fdk/references/integrations/analytics.md +18 -0
  69. package/skills/orchestra-fdk/references/integrations/ci-cd.md +20 -0
  70. package/skills/orchestra-fdk/references/integrations/cloud.md +19 -0
  71. package/skills/orchestra-fdk/references/integrations/code-repo.md +76 -0
  72. package/skills/orchestra-fdk/references/integrations/databases.md +22 -0
  73. package/skills/orchestra-fdk/references/integrations/dependencies.md +29 -0
  74. package/skills/orchestra-fdk/references/integrations/design.md +18 -0
  75. package/skills/orchestra-fdk/references/integrations/feature-flags.md +19 -0
  76. package/skills/orchestra-fdk/references/integrations/incident.md +29 -0
  77. package/skills/orchestra-fdk/references/integrations/issue-tracker.md +65 -0
  78. package/skills/orchestra-fdk/references/integrations/knowledge-base.md +43 -0
  79. package/skills/orchestra-fdk/references/integrations/messaging.md +38 -0
  80. package/skills/orchestra-fdk/references/integrations/observability.md +36 -0
  81. package/skills/orchestra-fdk/references/integrations/secrets-managers.md +25 -0
  82. package/skills/orchestra-fdk/references/integrations/support-desk.md +31 -0
  83. package/skills/orchestra-fdk/references/phases/0-start.md +62 -0
  84. package/skills/orchestra-fdk/references/phases/1-discover.md +111 -0
  85. package/skills/orchestra-fdk/references/phases/2-plan.md +157 -0
  86. package/skills/orchestra-fdk/references/phases/3-connect.md +99 -0
  87. package/skills/orchestra-fdk/references/phases/4-build.md +159 -0
  88. package/skills/orchestra-fdk/references/phases/5-prove.md +107 -0
  89. package/skills/orchestra-fdk/references/phases/6-handover.md +42 -0
  90. package/skills/orchestra-fdk/references/troubleshooting.md +128 -0
  91. package/templates/bug-fix/agents/bugfix.md +1 -1
  92. package/templates/bug-fix/main.ts +1 -1
  93. package/templates/engineering-team/README.md +45 -181
  94. package/templates/engineering-team/agents/engineer-agent.md +10 -7
  95. package/templates/engineering-team/agents/manager-agent.md +23 -120
  96. package/templates/engineering-team/inputs.json +1 -3
  97. package/templates/engineering-team/lib/environment.ts +10 -90
  98. package/templates/engineering-team/lib/inputs.ts +5 -97
  99. package/templates/engineering-team/main.ts +22 -69
  100. package/templates/todo-factory/agents/bugfix.md +1 -1
  101. package/templates/todo-factory/main.ts +1 -1
  102. package/templates/engineering-team/agents/engineer-bare.md +0 -22
  103. package/templates/engineering-team/onboarding/create-score-metrics.sh +0 -33
@@ -23,6 +23,13 @@ export interface AgentProps {
23
23
  harness: HarnessFamily;
24
24
  instructions?: string;
25
25
  skills?: ISkill[];
26
+ /**
27
+ * Whether the platform's managed skills are added after `skills`, which they are unless this is `false` or
28
+ * the harness is `script` or `custom`. Each managed id is added once, and an id `skills` already names stays
29
+ * where it is named there. A managed skill is left out when an authored `Skill` in `skills` declares the
30
+ * same frontmatter `name`, so the agent never installs two skills under one name.
31
+ */
32
+ includeManagedSkills?: boolean;
26
33
  memoryFiles?: MemoryFile[];
27
34
  labels?: Record<string, string>;
28
35
  modelSettings?: AgentModelSettings;
@@ -47,11 +54,14 @@ export declare class Agent extends Construct implements IAgent {
47
54
  private readonly explicitSlug;
48
55
  private readonly displayName;
49
56
  private readonly harness;
57
+ private readonly managedSkillIds;
50
58
  constructor(scope: Construct, id: string, props: AgentProps);
51
59
  get slug(): string;
52
60
  /** @internal */
53
61
  _resourceId(): string;
54
62
  _toPlanResource(): PlanResource;
63
+ private receivesManagedSkills;
64
+ private skillIds;
55
65
  static fromSlug(scope: Construct, slug: string): IAgent;
56
66
  /**
57
67
  * Apply resolves the reference through the shape-opaque `findByIdOrSlug` lookup, so a slug passed here —
@@ -2,9 +2,13 @@ import { CreateAgentRequestSyncModeEnum, StandbyCapacityWindowDaysOfWeekEnum, }
2
2
  import { Construct } from "../core/construct.js";
3
3
  import { validateDeclaredLabels } from "../core/labels.js";
4
4
  import { refToken } from "../core/tokens.js";
5
+ import { MANAGED_SKILL_IDS } from "../generated/managed-skills.js";
5
6
  import { registerImport, resolveManagedSlug, validateExplicitSlug } from "./references.js";
7
+ import { Skill } from "./skill.js";
6
8
  const HARNESS_FAMILIES = ["claude", "codex", "pi", "script", "custom"];
7
9
  const SYNC_MODES = Object.values(CreateAgentRequestSyncModeEnum);
10
+ /** The platform serves each managed skill under `skill_<key>`, and `<key>` is that skill's frontmatter `name`. */
11
+ const MANAGED_ID_PREFIX = "skill_";
8
12
  /**
9
13
  * Checked at runtime as well as in the type: orchestra apps are spawned under `npx tsx`, which transpiles without
10
14
  * typechecking, so an app that omits the prop reaches synth with `harness === undefined` and the TypeScript
@@ -75,6 +79,20 @@ function validateStandbyCapacity(label, capacity) {
75
79
  }
76
80
  });
77
81
  }
82
+ /** Checked at runtime for the same reason as the harness: a transpiled app reaches synth untypechecked. */
83
+ function validateIncludeManagedSkills(label, include) {
84
+ if (include !== undefined && typeof include !== "boolean") {
85
+ throw new Error(`${label}: includeManagedSkills is ${JSON.stringify(include)}. Set it to true or false, or remove it ` +
86
+ `to add the managed skills.`);
87
+ }
88
+ }
89
+ function managedIdsWithoutAuthoredNames(skills) {
90
+ const authoredNames = new Set(skills.flatMap((skill) => {
91
+ const name = skill instanceof Skill ? skill._declaredName() : undefined;
92
+ return name === undefined ? [] : [name];
93
+ }));
94
+ return MANAGED_SKILL_IDS.filter((managed) => !authoredNames.has(managed.slice(MANAGED_ID_PREFIX.length)));
95
+ }
78
96
  export class Agent extends Construct {
79
97
  /** Accepts a platform id or a slug interchangeably — one rule, stated on `IResource`. */
80
98
  agentId;
@@ -82,17 +100,20 @@ export class Agent extends Construct {
82
100
  explicitSlug;
83
101
  displayName;
84
102
  harness;
103
+ managedSkillIds;
85
104
  constructor(scope, id, props) {
86
105
  super(scope, id);
87
106
  validateDeclaredLabels(props.labels, `agent '${this.node.path.join("/")}' labels`);
88
107
  validateExplicitSlug(this, props.slug, "agent");
89
108
  this.harness = normalizeHarness(`agent '${this.node.path.join("/")}'`, props.harness);
90
109
  validateSyncMode(`agent '${this.node.path.join("/")}'`, props.syncMode);
110
+ validateIncludeManagedSkills(`agent '${this.node.path.join("/")}'`, props.includeManagedSkills);
91
111
  // Snapshot at construction so a later mutation of the caller's literal cannot change what synth emits —
92
112
  // notably a `labels` key added after the `validateDeclaredLabels` gate above already passed. `skills`
93
- // holds references (only each `skillId` is read), so copying the array is enough. Those references are
94
- // also why the deep `structuredClone` `connection.ts` uses is unavailable here: an `ISkill` carries a
95
- // `_resourceId()` method, which structured clone refuses — and copying an identity would be wrong anyway.
113
+ // holds references (only each `skillId` and a `Skill`'s declared name are read), so copying the array is
114
+ // enough. Those references are also why the deep `structuredClone` `connection.ts` uses is unavailable here:
115
+ // an `ISkill` carries a `_resourceId()` method, which structured clone refuses — and copying an identity
116
+ // would be wrong anyway.
96
117
  this.props = {
97
118
  ...props,
98
119
  ...(props.skills !== undefined ? { skills: [...props.skills] } : {}),
@@ -102,6 +123,11 @@ export class Agent extends Construct {
102
123
  ...(props.standbyCapacity !== undefined ? { standbyCapacity: structuredClone(props.standbyCapacity) } : {}),
103
124
  };
104
125
  validateStandbyCapacity(`agent '${this.node.path.join("/")}'`, this.props.standbyCapacity);
126
+ this.managedSkillIds = this.receivesManagedSkills() ? managedIdsWithoutAuthoredNames(this.props.skills ?? []) : [];
127
+ // Imports, as `Skill.fromId` registers one, so the pre-flight reads that the platform serves each id.
128
+ for (const managed of this.managedSkillIds) {
129
+ registerImport(this, { kind: "skill", ref: managed }, managed);
130
+ }
105
131
  this.explicitSlug = props.slug;
106
132
  this.displayName = props.name ?? id;
107
133
  this.agentId = refToken("agent", this.node.path);
@@ -127,7 +153,7 @@ export class Agent extends Construct {
127
153
  environmentId: this.props.environment.environmentId,
128
154
  instructions: this.props.instructions ?? "",
129
155
  harness: this.harness,
130
- skillIds: (this.props.skills ?? []).map((skill) => skill.skillId),
156
+ skillIds: this.skillIds(),
131
157
  memoryFiles: (this.props.memoryFiles ?? []).map((file) => ({
132
158
  path: file.path,
133
159
  content: file.content,
@@ -148,6 +174,15 @@ export class Agent extends Construct {
148
174
  };
149
175
  return { kind: "agent", path: this.node.path.join("/"), slug, managed: true, payload };
150
176
  }
177
+ // A script agent installs no skill, and the platform refuses `skillIds` on one; a custom agent starts no harness
178
+ // that reads a skill. Neither gets the managed set.
179
+ receivesManagedSkills() {
180
+ return this.harness !== "script" && this.harness !== "custom" && this.props.includeManagedSkills !== false;
181
+ }
182
+ skillIds() {
183
+ const authored = (this.props.skills ?? []).map((skill) => skill.skillId);
184
+ return [...authored, ...this.managedSkillIds.filter((managed) => !authored.includes(managed))];
185
+ }
151
186
  static fromSlug(scope, slug) {
152
187
  registerImport(scope, { kind: "agent", ref: slug }, slug);
153
188
  return { agentId: slug, _resourceId: () => slug };
@@ -245,6 +245,24 @@ export interface ResourceSpec {
245
245
  * exactly `2 × cpu`. */
246
246
  memory?: string;
247
247
  }
248
+ /** One hook: a script, at most 64 KiB, and how long it may run. Omitted, the hook's own default bound applies. */
249
+ export interface EnvironmentHook {
250
+ script: string;
251
+ timeoutSeconds?: number;
252
+ }
253
+ export interface EnvironmentHooks {
254
+ /**
255
+ * Run once as root on each new machine before it registers. Its bound is 1 to 1,800 seconds; omitted, the
256
+ * deployment's, which is 600 unless the deployment sets another. A machine keeps the hook it was created with.
257
+ */
258
+ onStartMachine?: EnvironmentHook;
259
+ /**
260
+ * Run once for each agent placed on a machine, as the agent's account in `workingDirectory`, before that
261
+ * agent's first conversation there. Its bound is 1 to 240 seconds, 120 when omitted. A failure fails the
262
+ * conversation that waited on it, and the next one runs the hook again.
263
+ */
264
+ onStartAgent?: EnvironmentHook;
265
+ }
248
266
  export interface EnvironmentProps {
249
267
  /** Omitted, each launch uses the compute provider's default image for the runtime. */
250
268
  image?: IImage | string;
@@ -253,8 +271,18 @@ export interface EnvironmentProps {
253
271
  * runs the worker; a command of your own must still start the worker, or the machine never dials in.
254
272
  */
255
273
  command?: string[];
256
- /** A script run once as root on each new machine before it registers, at most 64 KiB. */
274
+ /**
275
+ * A script run once as root on each new machine before it registers, at most 64 KiB.
276
+ * @deprecated Declare `hooks.onStartMachine`. Both together must name the same script.
277
+ */
257
278
  setup?: string;
279
+ /**
280
+ * The folder every conversation's agent starts in: an absolute path for the environment's OS, at most 1,024
281
+ * characters. The worker creates it for the agent's account when it is missing. Omitted, the agent starts in
282
+ * the worker's own folder for its harness.
283
+ */
284
+ workingDirectory?: string;
285
+ hooks?: EnvironmentHooks;
258
286
  egress: EgressPolicy;
259
287
  platform?: PlatformSpec;
260
288
  resources?: ResourceSpec;
@@ -499,6 +499,12 @@ const LADDER_MEMORY_BY_CPU = {
499
499
  /** The Standard capacity — the emitted default when nothing is declared. */
500
500
  const STANDARD_CPU = "2";
501
501
  const STANDARD_MEMORY = "4G";
502
+ /** A timeout left undeclared is left out, so the platform's default applies and a read of it is no drift. */
503
+ function hookPayload(hook) {
504
+ if (hook === undefined)
505
+ return null;
506
+ return { script: hook.script, ...(hook.timeoutSeconds !== undefined ? { timeoutSeconds: hook.timeoutSeconds } : {}) };
507
+ }
502
508
  export class Environment extends Construct {
503
509
  /** Accepts a platform id or a slug interchangeably — one rule, stated on `IResource`. */
504
510
  environmentId;
@@ -508,6 +514,11 @@ export class Environment extends Construct {
508
514
  constructor(scope, id, props) {
509
515
  super(scope, id);
510
516
  validateExplicitSlug(this, props.slug, "environment");
517
+ const declaredOnStartMachine = props.hooks?.onStartMachine?.script;
518
+ if (props.setup !== undefined && declaredOnStartMachine !== undefined && props.setup !== declaredOnStartMachine) {
519
+ throw new Error(`Environment '${id}' declares both setup and hooks.onStartMachine with different scripts. ` +
520
+ "Declare hooks.onStartMachine alone: setup is its older name.");
521
+ }
511
522
  // Snapshot at construction so a later mutation of the caller's literal cannot change what synth emits.
512
523
  // Each nested plain-data structure is copied one level deeper than the spread; a mount's `volume` stays
513
524
  // the same IVolume, since a reference is shared by identity and only its `volumeId` is ever read. Those
@@ -524,6 +535,14 @@ export class Environment extends Construct {
524
535
  : {}),
525
536
  ...(props.disks !== undefined ? { disks: props.disks.map((disk) => ({ ...disk })) } : {}),
526
537
  ...(props.command !== undefined ? { command: [...props.command] } : {}),
538
+ ...(props.hooks !== undefined
539
+ ? {
540
+ hooks: {
541
+ ...(props.hooks.onStartMachine !== undefined ? { onStartMachine: { ...props.hooks.onStartMachine } } : {}),
542
+ ...(props.hooks.onStartAgent !== undefined ? { onStartAgent: { ...props.hooks.onStartAgent } } : {}),
543
+ },
544
+ }
545
+ : {}),
527
546
  // Copied so a key added after the construct accepted the literal cannot ride a later synth into a
528
547
  // machine's env.
529
548
  ...(props.env !== undefined ? { env: { ...props.env } } : {}),
@@ -555,6 +574,7 @@ export class Environment extends Construct {
555
574
  platform.runtime = platformProps.runtime;
556
575
  const resources = this.props.resources ?? {};
557
576
  const cpu = resources.cpu ?? STANDARD_CPU;
577
+ const onStartMachine = this.props.hooks?.onStartMachine ?? (this.props.setup ? { script: this.props.setup } : undefined);
558
578
  // The egress rules, `ingressPolicy.endpoints`, `volumeMounts` and `env` are ALWAYS emitted — empty when
559
579
  // unset — so clearing any of them reconciles through the platform's whole-object PATCH replace instead of
560
580
  // being masked as omitted. `env` inherits the known map residual: emptying a declared map as the SOLE
@@ -573,6 +593,12 @@ export class Environment extends Construct {
573
593
  access: endpoint.access,
574
594
  })),
575
595
  },
596
+ // Sent empty when undeclared, and each hook as null, so removing one clears what an earlier apply set.
597
+ workingDirectory: this.props.workingDirectory ?? "",
598
+ hooks: {
599
+ onStartMachine: hookPayload(onStartMachine),
600
+ onStartAgent: hookPayload(this.props.hooks?.onStartAgent),
601
+ },
576
602
  // The launch facts are declared here and nowhere else: the platform ignores one spelled at the top
577
603
  // level of the environment body.
578
604
  provisionSpec: {
@@ -580,7 +606,8 @@ export class Environment extends Construct {
580
606
  // leaves the stored value, and only an empty one clears it.
581
607
  image,
582
608
  command: [...(this.props.command ?? [])],
583
- setup: this.props.setup ?? "",
609
+ // The platform refuses a setup that names another script than hooks.onStartMachine in the same body.
610
+ setup: onStartMachine?.script ?? "",
584
611
  resources: {
585
612
  cpu,
586
613
  // A cpu that names no rung has no `2 x cpu` to look up, so it falls back to the Standard memory: the
@@ -2,7 +2,7 @@ import { Construct } from "../core/construct.js";
2
2
  import type { PlanResource } from "../core/plan.js";
3
3
  import type { ISkill } from "./interfaces.js";
4
4
  export interface SkillProps {
5
- /** Read at synth; text-only. */
5
+ /** Read whole at synth, and its `SKILL.md` name when an agent is constructed; text-only. */
6
6
  source: string;
7
7
  /** When omitted a factory-namespaced slug is derived. */
8
8
  slug?: string;
@@ -25,6 +25,11 @@ export declare class Skill extends Construct implements ISkill {
25
25
  _toPlanResource(): PlanResource;
26
26
  /** Sorted by path for a deterministic plan. The missing-root-SKILL.md check only pre-empts a server 400. */
27
27
  private readSourceFiles;
28
+ /**
29
+ * @internal The `name` the root `SKILL.md` frontmatter declares, or `undefined` when the file or the field
30
+ * cannot be read; synth reports an unreadable source itself.
31
+ */
32
+ _declaredName(): string | undefined;
28
33
  static fromSlug(scope: Construct, slug: string): ISkill;
29
34
  static fromId(scope: Construct, id: string): ISkill;
30
35
  }
@@ -1,7 +1,7 @@
1
1
  // The payload mirrors the platform's create request EXCEPT the wire's `files: [{path, fileId}]`: the plan
2
2
  // carries the ergonomic authoring shape `{path, content}` and the apply engine does the file choreography.
3
- // The root `SKILL.md` frontmatter is parsed and validated SERVER-side, so the construct only checks the file
4
- // set is text, in-cap, and carries a root `SKILL.md`.
3
+ // The root `SKILL.md` frontmatter is validated SERVER-side, so the construct checks only that the file set
4
+ // is text, in-cap, and carries a root `SKILL.md`; it reads the frontmatter's `name` alone, for `Agent`.
5
5
  import { readdirSync, readFileSync } from "node:fs";
6
6
  import { relative, resolve, sep } from "node:path";
7
7
  import { Construct } from "../core/construct.js";
@@ -73,6 +73,25 @@ export class Skill extends Construct {
73
73
  files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
74
74
  return files;
75
75
  }
76
+ /**
77
+ * @internal The `name` the root `SKILL.md` frontmatter declares, or `undefined` when the file or the field
78
+ * cannot be read; synth reports an unreadable source itself.
79
+ */
80
+ _declaredName() {
81
+ let text;
82
+ try {
83
+ text = readFileSync(resolve(this.props.source, ROOT_SKILL_PATH), "utf8");
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
89
+ const name = frontmatter === null ? null : /^name[ \t]*:[ \t]*(.*?)[ \t]*$/m.exec(frontmatter[1]);
90
+ if (name === null)
91
+ return undefined;
92
+ const quoted = /^(["'])(.*)\1$/.exec(name[1]);
93
+ return quoted === null ? name[1] : quoted[2];
94
+ }
76
95
  static fromSlug(scope, slug) {
77
96
  registerImport(scope, { kind: "skill", ref: slug }, slug);
78
97
  return { skillId: slug, _resourceId: () => slug };
@@ -1026,6 +1026,8 @@ async function applyEnvironment(resource, apis, outcomes, applied, reserved, con
1026
1026
  outcomes.push({ path: resource.path, kind: "environment", slug: resource.slug, id, action });
1027
1027
  applied.set(refToken("environment", resource.path.split("/")), id);
1028
1028
  }
1029
+ /** The bound the platform stores for an `onStartAgent` hook that declares none. */
1030
+ const ON_START_AGENT_DEFAULT_TIMEOUT_SECONDS = 120;
1029
1031
  /**
1030
1032
  * `policyVersion` is server-managed, so it is excluded. The platform leaves an empty `image`, `command` or
1031
1033
  * `setup` out of a read, so the empty value orchestra sends for an undeclared one matches an absent live one.
@@ -1046,7 +1048,19 @@ export function environmentUnchanged(desired, live) {
1046
1048
  }
1047
1049
  }
1048
1050
  }
1049
- return subsetDeepEqual(compare, live);
1051
+ if (isEmptyLaunchValue(compare.workingDirectory) && isEmptyLaunchValue(live.workingDirectory)) {
1052
+ delete compare.workingDirectory;
1053
+ }
1054
+ // An undeclared bound is compared as the one the platform stores for it, so removing a declared bound is a
1055
+ // change: the agent hook's default, and no bound for the machine hook, whose bound is then the deployment's.
1056
+ const hooks = compare.hooks;
1057
+ for (const [name, undeclared] of [["onStartAgent", ON_START_AGENT_DEFAULT_TIMEOUT_SECONDS], ["onStartMachine", null]]) {
1058
+ const hook = hooks?.[name];
1059
+ if (hook && hook.timeoutSeconds === undefined)
1060
+ hook.timeoutSeconds = undeclared;
1061
+ }
1062
+ // A read leaves out `hooks` when it holds none, which the declared nulls match.
1063
+ return subsetDeepEqual(compare, { ...live, hooks: live.hooks ?? {} });
1050
1064
  }
1051
1065
  function isEmptyLaunchValue(value) {
1052
1066
  return value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0);