@forgezero/agent 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -118,12 +118,30 @@ root-only control socket or signed outbound claim path, then the deployment
118
118
  manager validates the checked-in definition and submits it to the common keyed
119
119
  queue.
120
120
 
121
+ ## Project context for any repository
122
+
123
+ Conversation memory is not a project database. Initialize one vendor-neutral,
124
+ Git-owned context and generate the small files each AI product discovers:
125
+
126
+ ```bash
127
+ fz project init
128
+ # edit .forgezero/project.json and create its named truth sources
129
+ fz project sync
130
+ fz project check
131
+ ```
132
+
133
+ `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, Copilot instructions and the Cursor rule
134
+ are generated adapters. They contain no independent architecture or progress.
135
+ `fz project check` fails when a truth source is missing or an adapter was edited
136
+ by hand. Tools and skills remain optional execution aids; accepted decisions and
137
+ status live in the repository and therefore survive switching AI agents.
138
+
121
139
  ## Deployment definitions
122
140
 
123
- A repository may commit `.fz/deploy.yaml` with its own roles, prerequisite
141
+ A repository may commit `.fz/deploy.json` with its own profiles, prerequisite
124
142
  checks, commands, and the exact secret names each step needs. ForgeZero does not
125
143
  choose a tenant's database, framework, or deploy shape. The agent validates the
126
- file before executing any command, prepares only the selected role, and gives a
144
+ file before executing any command, prepares only the selected profile, and gives a
127
145
  step only the vault values it explicitly names. `await` returns the complete
128
146
  pipeline result; no polling service or persistent queue is required.
129
147
 
@@ -131,7 +149,9 @@ The daemon owns source checkout and command execution. Bootstrap explicitly
131
149
  awaits release one because the API does not exist yet, then the Agent consumes a
132
150
  one-use platform enrolment capability. There is no branch watcher. Every normal
133
151
  platform or tenant release is one durable API delivery atomically expanded to
134
- one row for every explicit compute binding:
152
+ one row for every independently attached compute target. Release-scoped steps
153
+ run on one target elected by stable target ordering, never on a user-declared
154
+ coordinator:
135
155
  `pending` is written before dispatch, `running` and a fenced lease before project
136
156
  code, and only an awaited successful pipeline writes `deployed`. An expired
137
157
  claim can be recovered; its stale token cannot renew or finish.
@@ -188,7 +208,7 @@ An operator may also force a deployment and await the complete result over the
188
208
  private control socket:
189
209
 
190
210
  ```bash
191
- fz-agent deploy --revision=<full-40-character-commit> --coordinator
211
+ fz-agent deploy --revision=<full-40-character-commit> --release-executor
192
212
  fz-agent status
193
213
  fz-agent pause
194
214
  fz-agent pause-key --key=project:production
@@ -429,7 +429,7 @@ async function postSignedNode(options, path, body) {
429
429
  }
430
430
 
431
431
  // src/version.ts
432
- var VERSION2 = "0.1.28";
432
+ var VERSION2 = "0.1.30";
433
433
 
434
434
  // src/agent-heartbeat.ts
435
435
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
@@ -44,7 +44,7 @@ export interface InstallOptions {
44
44
  controlSocketPath?: string;
45
45
  repository?: string;
46
46
  branch?: string;
47
- role?: string;
47
+ profile?: string;
48
48
  deployRoot?: string;
49
49
  publicApiUrl?: string;
50
50
  deploymentEnvironment?: Record<string, string>;
@@ -1,14 +1,20 @@
1
1
  import type { Pipeline, PipelineStep } from './pipeline';
2
2
  import { type SoftwareRequirement } from './software';
3
- export declare const PIPELINE_VERSION: 1;
4
- export interface PipelineRole {
5
- name: string;
3
+ /**
4
+ * Version two separates a repository's deploy recipe from the computes that use
5
+ * it. A target chooses one named profile in the control plane; compute names,
6
+ * counts and cluster leadership never belong in Git.
7
+ */
8
+ export declare const PIPELINE_VERSION: 2;
9
+ export interface PipelineProfile {
6
10
  software: readonly SoftwareRequirement[];
7
11
  }
8
12
  export interface DeployStep extends PipelineStep {
9
13
  phase: 'build' | 'release' | 'migrate' | 'health';
10
- /** The coordinator runs this on one selected node, never on every replica. */
11
- once?: boolean;
14
+ /** Run on this target, or on the one deterministic release executor. */
15
+ scope: 'target' | 'release';
16
+ /** Optional profile filter. An omitted list applies to every profile. */
17
+ profiles?: readonly string[];
12
18
  /** Run only when every named non-secret deployment coordinate has this value. */
13
19
  when?: Readonly<Record<string, string>>;
14
20
  }
@@ -16,12 +22,12 @@ export interface DeployDefinition {
16
22
  version: typeof PIPELINE_VERSION;
17
23
  name: string;
18
24
  requireAttestation?: boolean;
19
- roles: readonly PipelineRole[];
25
+ profiles: Readonly<Record<string, PipelineProfile>>;
20
26
  steps: readonly DeployStep[];
21
27
  }
22
28
  export declare class DefinitionError extends Error {
23
29
  constructor(message: string);
24
30
  }
25
- /** Validate parsed YAML before any command from it is allowed to run. */
31
+ /** Validate parsed JSON before any command from it is allowed to run. */
26
32
  export declare function parseDeployDefinition(value: unknown): DeployDefinition;
27
- export declare function phasePipeline(definition: DeployDefinition, phase: DeployStep['phase']): Pipeline;
33
+ export declare function phasePipeline(definition: DeployDefinition, phase: DeployStep['phase'], profile: string, executeRelease?: boolean): Pipeline;
@@ -3,6 +3,16 @@ import { readFileSync } from "node:fs";
3
3
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
4
4
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
5
5
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
6
+ var OS_CATALOG = [
7
+ { id: "ubuntu", version: "26.04", architecture: "x64", status: "active" }
8
+ ];
9
+ var SOFTWARE_CATALOG = [
10
+ { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
11
+ { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
12
+ { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
13
+ { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
14
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
15
+ ];
6
16
  var UBUNTU_2604_X64 = [
7
17
  {
8
18
  requirement: { id: "bun", version: "1.3.14" },
@@ -41,7 +51,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
41
51
  architecture
42
52
  };
43
53
  }
44
- function validateSoftwareRequirements(value) {
54
+ function validateSoftwareRequirements(value, options = {}) {
45
55
  if (!Array.isArray(value) || value.length > 32)
46
56
  throw new Error("software requirements must be an array of at most 32 entries");
47
57
  const seen = new Set;
@@ -59,13 +69,18 @@ function validateSoftwareRequirements(value) {
59
69
  if (seen.has(requirement.id))
60
70
  throw new Error(`duplicate software requirement: ${requirement.id}`);
61
71
  seen.add(requirement.id);
72
+ const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
73
+ if (!catalog || catalog.status === "retired" || options.channel !== "development" && catalog.status !== "active") {
74
+ throw new Error(`software requirement is not available for ${options.channel ?? "production"}: ${requirement.id}@${requirement.version}`);
75
+ }
62
76
  return requirement;
63
77
  });
64
78
  }
65
79
  async function ensureSoftwareRequirements(requirementsInput, options) {
66
80
  const requirements = validateSoftwareRequirements(requirementsInput);
67
81
  const observation = options.observation ?? observeSoftwareHost();
68
- if (observation.os.id !== "ubuntu" || observation.os.versionId !== "26.04" || observation.architecture !== "x64") {
82
+ const os = OS_CATALOG.find((candidate) => candidate.id === observation.os.id && candidate.version === observation.os.versionId && candidate.architecture === observation.architecture);
83
+ if (!os || os.status !== "active") {
69
84
  throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
70
85
  }
71
86
  const results = [];
@@ -90,7 +105,7 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
90
105
  }
91
106
 
92
107
  // src/definition.ts
93
- var PIPELINE_VERSION = 1;
108
+ var PIPELINE_VERSION = 2;
94
109
 
95
110
  class DefinitionError extends Error {
96
111
  constructor(message) {
@@ -115,6 +130,7 @@ var exactKeys = (value, allowed, where) => {
115
130
  if (unknown.length > 0)
116
131
  throw new DefinitionError(`${where} contains unknown field(s): ${unknown.join(", ")}.`);
117
132
  };
133
+ var NAME = /^[a-z][a-z0-9-]{0,62}$/;
118
134
  var RESERVED_STEP_ENV = new Set([
119
135
  "PATH",
120
136
  "HOME",
@@ -129,37 +145,52 @@ var RESERVED_STEP_ENV = new Set([
129
145
  ]);
130
146
  function parseDeployDefinition(value) {
131
147
  const root = record(value, "pipeline");
132
- exactKeys(root, ["version", "name", "requireAttestation", "roles", "steps"], "pipeline");
148
+ exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
149
+ if (root.$schema !== undefined && (typeof root.$schema !== "string" || !root.$schema.startsWith("https://"))) {
150
+ throw new DefinitionError("pipeline.$schema must be an HTTPS URL.");
151
+ }
133
152
  if (root.version !== PIPELINE_VERSION) {
134
153
  throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
135
154
  }
136
- if (!Array.isArray(root.roles) || root.roles.length === 0) {
137
- throw new DefinitionError("pipeline.roles must contain at least one role.");
155
+ const rawProfiles = record(root.profiles, "pipeline.profiles");
156
+ const profileEntries = Object.entries(rawProfiles);
157
+ if (profileEntries.length === 0 || profileEntries.length > 32) {
158
+ throw new DefinitionError("pipeline.profiles must contain from 1 to 32 named profiles.");
138
159
  }
139
160
  if (!Array.isArray(root.steps) || root.steps.length === 0) {
140
161
  throw new DefinitionError("pipeline.steps must contain at least one step.");
141
162
  }
142
- const roles = root.roles.map((raw, index) => {
143
- const role = record(raw, `roles[${index}]`);
144
- exactKeys(role, ["name", "software"], `roles[${index}]`);
145
- if (!Array.isArray(role.software)) {
146
- throw new DefinitionError(`roles[${index}].software must be an array.`);
163
+ const profiles = {};
164
+ for (const [name, raw] of profileEntries) {
165
+ if (!NAME.test(name))
166
+ throw new DefinitionError(`pipeline profile name is invalid: ${name}.`);
167
+ const profile = record(raw, `profiles.${name}`);
168
+ exactKeys(profile, ["software"], `profiles.${name}`);
169
+ if (!Array.isArray(profile.software)) {
170
+ throw new DefinitionError(`profiles.${name}.software must be an array.`);
147
171
  }
148
- return {
149
- name: text(role.name, `roles[${index}].name`),
150
- software: validateSoftwareRequirements(role.software)
151
- };
152
- });
153
- if (new Set(roles.map((role) => role.name)).size !== roles.length) {
154
- throw new DefinitionError("pipeline.roles must have unique names.");
172
+ profiles[name] = { software: validateSoftwareRequirements(profile.software) };
155
173
  }
156
174
  const phases = new Set(["build", "release", "migrate", "health"]);
157
175
  const steps = root.steps.map((raw, index) => {
158
176
  const step = record(raw, `steps[${index}]`);
159
- exactKeys(step, ["name", "run", "phase", "secrets", "once", "always", "timeoutMs", "when"], `steps[${index}]`);
177
+ exactKeys(step, ["name", "run", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
160
178
  const phase = text(step.phase, `steps[${index}].phase`);
161
179
  if (!phases.has(phase))
162
180
  throw new DefinitionError(`steps[${index}].phase is not supported.`);
181
+ if (step.scope !== "target" && step.scope !== "release") {
182
+ throw new DefinitionError(`steps[${index}].scope must be target or release.`);
183
+ }
184
+ let selectedProfiles;
185
+ if (step.profiles !== undefined) {
186
+ if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name) => typeof name !== "string" || !Object.hasOwn(profiles, name))) {
187
+ throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
188
+ }
189
+ selectedProfiles = [...step.profiles];
190
+ if (new Set(selectedProfiles).size !== selectedProfiles.length) {
191
+ throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
192
+ }
193
+ }
163
194
  if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name) => typeof name !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name)))) {
164
195
  throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
165
196
  }
@@ -190,8 +221,9 @@ function parseDeployDefinition(value) {
190
221
  name: text(step.name, `steps[${index}].name`),
191
222
  run: text(step.run, `steps[${index}].run`),
192
223
  phase,
224
+ scope: step.scope,
225
+ profiles: selectedProfiles,
193
226
  secrets: step.secrets,
194
- once: step.once === true,
195
227
  always: step.always === true,
196
228
  timeoutMs,
197
229
  when
@@ -204,15 +236,18 @@ function parseDeployDefinition(value) {
204
236
  version: PIPELINE_VERSION,
205
237
  name: text(root.name, "pipeline.name"),
206
238
  requireAttestation: root.requireAttestation === true,
207
- roles,
239
+ profiles,
208
240
  steps
209
241
  };
210
242
  }
211
- function phasePipeline(definition, phase) {
243
+ function phasePipeline(definition, phase, profile, executeRelease = false) {
244
+ if (!Object.hasOwn(definition.profiles, profile)) {
245
+ throw new DefinitionError(`pipeline profile does not exist: ${profile}.`);
246
+ }
212
247
  return {
213
248
  name: `${definition.name}:${phase}`,
214
249
  requireAttestation: definition.requireAttestation,
215
- steps: definition.steps.filter((step) => step.phase === phase)
250
+ steps: definition.steps.filter((step) => step.phase === phase && (!step.profiles || step.profiles.includes(profile)) && (step.scope === "target" || executeRelease))
216
251
  };
217
252
  }
218
253
  export {
@@ -7,12 +7,12 @@ export interface RemoteDeploymentClaim {
7
7
  claimToken: string;
8
8
  claimExpiresAtTs: number;
9
9
  attempt: number;
10
- /** True on exactly one compute in a clustered delivery. */
11
- coordinator: boolean;
10
+ /** True on the one target deterministically elected for this release. */
11
+ releaseExecutor: boolean;
12
12
  source: {
13
13
  repository: string;
14
14
  branch: string;
15
- role: string;
15
+ profile: string;
16
16
  knownHosts?: string;
17
17
  auth?: GitSourceAuth;
18
18
  };
@@ -16,8 +16,8 @@ export interface CommandResult {
16
16
  export interface DeploymentRequest {
17
17
  /** Optional exact commit from a verified webhook. Never a branch name. */
18
18
  revision?: string;
19
- /** Whether this node is the coordinator allowed to execute `once` steps. */
20
- coordinator?: boolean;
19
+ /** True only for the target deterministically elected for release-scoped steps. */
20
+ releaseExecutor?: boolean;
21
21
  }
22
22
  export interface DeploymentResult {
23
23
  key: string;
@@ -46,7 +46,7 @@ export interface DeploymentOptions {
46
46
  key: string;
47
47
  repository: string;
48
48
  branch: string;
49
- role: string;
49
+ profile: string;
50
50
  root: string;
51
51
  publicApiUrl?: string;
52
52
  sourceAuth?: GitSourceAuth;
@@ -74,8 +74,8 @@ export declare class DeploymentError extends Error {
74
74
  /**
75
75
  * One source and one pipeline owner.
76
76
  *
77
- * A request may choose an exact commit and whether this assigned node is the
78
- * coordinator. It cannot choose a repository, branch, working directory or
77
+ * A request may choose an exact commit and carry the control plane's
78
+ * deterministic release-executor decision. It cannot choose a repository, branch, working directory or
79
79
  * command: those are sealed into the agent unit and the checked-out definition.
80
80
  */
81
81
  export declare function createDeploymentManager(options: DeploymentOptions): {