@forgezero/agent 0.1.29 → 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 +24 -4
- package/dist/agent-heartbeat.js +1 -1
- package/dist/cli/agent-install.d.ts +1 -1
- package/dist/definition.d.ts +14 -8
- package/dist/definition.js +58 -23
- package/dist/deployment-pull.d.ts +3 -3
- package/dist/deployment.d.ts +5 -5
- package/dist/fz-agent.js +78 -44
- package/dist/fz.js +331 -19
- package/dist/index.d.ts +2 -2
- package/dist/project-context.d.ts +37 -0
- package/dist/project-context.js +266 -0
- package/dist/provision.d.ts +1 -1
- package/dist/provision.js +19 -4
- package/dist/software-helper.js +17 -2
- package/dist/software.d.ts +24 -2
- package/dist/software.js +20 -3
- package/dist/version.d.ts +1 -1
- package/package.json +6 -2
package/dist/fz.js
CHANGED
|
@@ -4871,8 +4871,8 @@ async function spawnWith(command, env, report = () => {}) {
|
|
|
4871
4871
|
|
|
4872
4872
|
// src/cli/index.ts
|
|
4873
4873
|
init_dist();
|
|
4874
|
-
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
4875
|
-
import { dirname } from "path";
|
|
4874
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4875
|
+
import { dirname as dirname2 } from "path";
|
|
4876
4876
|
import { fileURLToPath } from "url";
|
|
4877
4877
|
|
|
4878
4878
|
// src/agent-update.ts
|
|
@@ -4886,7 +4886,7 @@ var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-
|
|
|
4886
4886
|
var MAX_REQUEST_BYTES = 8 * 1024;
|
|
4887
4887
|
|
|
4888
4888
|
// src/version.ts
|
|
4889
|
-
var VERSION2 = "0.1.
|
|
4889
|
+
var VERSION2 = "0.1.30";
|
|
4890
4890
|
|
|
4891
4891
|
// src/software.ts
|
|
4892
4892
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
@@ -5363,7 +5363,7 @@ function agentUnit(options) {
|
|
|
5363
5363
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
5364
5364
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
5365
5365
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
5366
|
-
options.
|
|
5366
|
+
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
5367
5367
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
5368
5368
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
5369
5369
|
deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
|
|
@@ -9275,6 +9275,261 @@ async function resolveIdentity(selector, socketPath) {
|
|
|
9275
9275
|
return chosen;
|
|
9276
9276
|
}
|
|
9277
9277
|
|
|
9278
|
+
// src/project-context.ts
|
|
9279
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
9280
|
+
import { dirname, join as join2, resolve } from "path";
|
|
9281
|
+
var PROJECT_CONTEXT_VERSION = 1;
|
|
9282
|
+
var GENERATED = "<!-- Generated by @forgezero/agent project context. Edit .forgezero/project.json, then run `fz project sync`. -->";
|
|
9283
|
+
|
|
9284
|
+
class ProjectContextError extends Error {
|
|
9285
|
+
constructor(message) {
|
|
9286
|
+
super(message);
|
|
9287
|
+
this.name = "ProjectContextError";
|
|
9288
|
+
}
|
|
9289
|
+
}
|
|
9290
|
+
var text = (value, where) => {
|
|
9291
|
+
if (typeof value !== "string" || !value.trim() || /[\r\0]/.test(value)) {
|
|
9292
|
+
throw new ProjectContextError(`${where} must be non-empty text.`);
|
|
9293
|
+
}
|
|
9294
|
+
return value.trim();
|
|
9295
|
+
};
|
|
9296
|
+
var relativePath = (value, where) => {
|
|
9297
|
+
const path = text(value, where);
|
|
9298
|
+
if (path.startsWith("/") || path.split("/").includes("..")) {
|
|
9299
|
+
throw new ProjectContextError(`${where} must stay inside the repository.`);
|
|
9300
|
+
}
|
|
9301
|
+
return path.replace(/^\.\//, "");
|
|
9302
|
+
};
|
|
9303
|
+
var stringList = (value, where, paths = false) => {
|
|
9304
|
+
if (!Array.isArray(value) || value.length > 128) {
|
|
9305
|
+
throw new ProjectContextError(`${where} must be an array of at most 128 entries.`);
|
|
9306
|
+
}
|
|
9307
|
+
const items = value.map((item, index) => paths ? relativePath(item, `${where}[${index}]`) : text(item, `${where}[${index}]`));
|
|
9308
|
+
if (new Set(items).size !== items.length)
|
|
9309
|
+
throw new ProjectContextError(`${where} must not contain duplicates.`);
|
|
9310
|
+
return items;
|
|
9311
|
+
};
|
|
9312
|
+
function parseProjectContext(value) {
|
|
9313
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9314
|
+
throw new ProjectContextError("project context must be an object.");
|
|
9315
|
+
}
|
|
9316
|
+
const row = value;
|
|
9317
|
+
const allowed = ["schemaVersion", "name", "purpose", "truth", "readFirst", "verify", "rules", "nonAuthoritative"];
|
|
9318
|
+
const unknown = Object.keys(row).filter((key) => !allowed.includes(key));
|
|
9319
|
+
if (unknown.length)
|
|
9320
|
+
throw new ProjectContextError(`project context contains unknown field(s): ${unknown.join(", ")}.`);
|
|
9321
|
+
if (row.schemaVersion !== PROJECT_CONTEXT_VERSION) {
|
|
9322
|
+
throw new ProjectContextError(`project context schemaVersion must be ${PROJECT_CONTEXT_VERSION}.`);
|
|
9323
|
+
}
|
|
9324
|
+
if (!Array.isArray(row.truth) || row.truth.length === 0 || row.truth.length > 64) {
|
|
9325
|
+
throw new ProjectContextError("project context truth must contain from 1 to 64 sources.");
|
|
9326
|
+
}
|
|
9327
|
+
const truth = row.truth.map((item, index) => {
|
|
9328
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
9329
|
+
throw new ProjectContextError(`truth[${index}] must be an object.`);
|
|
9330
|
+
}
|
|
9331
|
+
const source = item;
|
|
9332
|
+
if (Object.keys(source).some((key) => !["area", "path", "description"].includes(key))) {
|
|
9333
|
+
throw new ProjectContextError(`truth[${index}] contains an unknown field.`);
|
|
9334
|
+
}
|
|
9335
|
+
return {
|
|
9336
|
+
area: text(source.area, `truth[${index}].area`),
|
|
9337
|
+
path: relativePath(source.path, `truth[${index}].path`),
|
|
9338
|
+
description: text(source.description, `truth[${index}].description`)
|
|
9339
|
+
};
|
|
9340
|
+
});
|
|
9341
|
+
const areas = truth.map((source) => source.area);
|
|
9342
|
+
if (new Set(areas).size !== areas.length)
|
|
9343
|
+
throw new ProjectContextError("project context truth areas must be unique.");
|
|
9344
|
+
return {
|
|
9345
|
+
schemaVersion: PROJECT_CONTEXT_VERSION,
|
|
9346
|
+
name: text(row.name, "project context name"),
|
|
9347
|
+
purpose: text(row.purpose, "project context purpose"),
|
|
9348
|
+
truth,
|
|
9349
|
+
readFirst: stringList(row.readFirst, "project context readFirst", true),
|
|
9350
|
+
verify: stringList(row.verify, "project context verify"),
|
|
9351
|
+
rules: stringList(row.rules, "project context rules"),
|
|
9352
|
+
nonAuthoritative: stringList(row.nonAuthoritative, "project context nonAuthoritative", true)
|
|
9353
|
+
};
|
|
9354
|
+
}
|
|
9355
|
+
function defaultProjectContext(root = process.cwd()) {
|
|
9356
|
+
let name = root.split("/").filter(Boolean).at(-1) ?? "project";
|
|
9357
|
+
let verify = ["npm test"];
|
|
9358
|
+
const manifestPath = join2(root, "package.json");
|
|
9359
|
+
if (existsSync(manifestPath)) {
|
|
9360
|
+
try {
|
|
9361
|
+
const pkg = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
9362
|
+
name = pkg.name ?? name;
|
|
9363
|
+
const runner = existsSync(join2(root, "bun.lock")) ? "bun run" : "npm run";
|
|
9364
|
+
verify = ["check", "test", "build"].filter((script) => pkg.scripts?.[script]).map((script) => `${runner} ${script}`);
|
|
9365
|
+
if (verify.length === 0)
|
|
9366
|
+
verify = [existsSync(join2(root, "bun.lock")) ? "bun test" : "npm test"];
|
|
9367
|
+
} catch {}
|
|
9368
|
+
}
|
|
9369
|
+
return {
|
|
9370
|
+
schemaVersion: PROJECT_CONTEXT_VERSION,
|
|
9371
|
+
name,
|
|
9372
|
+
purpose: "Describe the product outcome here; implementation details belong in the truth sources below.",
|
|
9373
|
+
truth: [
|
|
9374
|
+
{ area: "architecture", path: "docs/architecture.md", description: "Current system boundaries and decisions." },
|
|
9375
|
+
{ area: "progress", path: "docs/progress.md", description: "Evidence-backed delivery state and next work." }
|
|
9376
|
+
],
|
|
9377
|
+
readFirst: [".forgezero/PROJECT.md"],
|
|
9378
|
+
verify,
|
|
9379
|
+
rules: [
|
|
9380
|
+
"Inspect the current worktree before editing and preserve unrelated changes.",
|
|
9381
|
+
"Update a truth source instead of copying architecture or progress into another document.",
|
|
9382
|
+
"Never report a feature as complete without running its declared verification."
|
|
9383
|
+
],
|
|
9384
|
+
nonAuthoritative: ["audit/"]
|
|
9385
|
+
};
|
|
9386
|
+
}
|
|
9387
|
+
function renderProjectContext(manifest) {
|
|
9388
|
+
const truth = manifest.truth.map((source) => `| ${source.area} | \`${source.path}\` | ${source.description} |`).join(`
|
|
9389
|
+
`);
|
|
9390
|
+
return `${GENERATED}
|
|
9391
|
+
# ${manifest.name} \u2014 project context
|
|
9392
|
+
|
|
9393
|
+
${manifest.purpose}
|
|
9394
|
+
|
|
9395
|
+
## Read first
|
|
9396
|
+
|
|
9397
|
+
${manifest.readFirst.map((path) => `- \`${path}\``).join(`
|
|
9398
|
+
`) || "- No additional entry points."}
|
|
9399
|
+
|
|
9400
|
+
## Sources of truth
|
|
9401
|
+
|
|
9402
|
+
| Area | Path | Authority |
|
|
9403
|
+
|---|---|---|
|
|
9404
|
+
${truth}
|
|
9405
|
+
|
|
9406
|
+
If two files disagree, the file named in this table wins. Fix or regenerate the
|
|
9407
|
+
other file in the same change. Conversation memory, audit snapshots and generated
|
|
9408
|
+
output never override repository truth.
|
|
9409
|
+
|
|
9410
|
+
## Project rules
|
|
9411
|
+
|
|
9412
|
+
${manifest.rules.map((rule) => `- ${rule}`).join(`
|
|
9413
|
+
`) || "- No additional project rules."}
|
|
9414
|
+
|
|
9415
|
+
## Verification
|
|
9416
|
+
|
|
9417
|
+
${manifest.verify.map((command) => `- \`${command}\``).join(`
|
|
9418
|
+
`) || "- No verification command declared."}
|
|
9419
|
+
|
|
9420
|
+
## Non-authoritative material
|
|
9421
|
+
|
|
9422
|
+
${manifest.nonAuthoritative.map((path) => `- \`${path}\``).join(`
|
|
9423
|
+
`) || "- None declared."}
|
|
9424
|
+
|
|
9425
|
+
Tools, skills and AI vendors may change. They are execution aids, not memory.
|
|
9426
|
+
Persist every accepted decision and status change in the source of truth that
|
|
9427
|
+
owns it, then run \`fz project check\` before handoff.
|
|
9428
|
+
`;
|
|
9429
|
+
}
|
|
9430
|
+
var adapter = (name) => `${GENERATED}
|
|
9431
|
+
# ${name} project instructions
|
|
9432
|
+
|
|
9433
|
+
Read \`.forgezero/PROJECT.md\` completely before acting. It is generated from
|
|
9434
|
+
\`.forgezero/project.json\`, the vendor-neutral project context. Follow every
|
|
9435
|
+
source of truth and verification command it names.
|
|
9436
|
+
|
|
9437
|
+
Do not treat this adapter, conversation memory, an audit report, generated
|
|
9438
|
+
output, a tool, or a skill as architectural authority. When work changes an
|
|
9439
|
+
accepted decision or delivery state, update the named Git source in the same
|
|
9440
|
+
change and run \`fz project check\`.
|
|
9441
|
+
`;
|
|
9442
|
+
function projectContextFiles(manifestInput) {
|
|
9443
|
+
const manifest = parseProjectContext(manifestInput);
|
|
9444
|
+
return [
|
|
9445
|
+
{ path: ".forgezero/PROJECT.md", content: renderProjectContext(manifest) },
|
|
9446
|
+
{ path: "AGENTS.md", content: adapter("AI agent") },
|
|
9447
|
+
{ path: "CLAUDE.md", content: adapter("Claude") },
|
|
9448
|
+
{ path: "GEMINI.md", content: adapter("Gemini") },
|
|
9449
|
+
{ path: ".github/copilot-instructions.md", content: adapter("GitHub Copilot") },
|
|
9450
|
+
{ path: ".cursor/rules/project-context.mdc", content: `${GENERATED}
|
|
9451
|
+
---
|
|
9452
|
+
description: Repository source-of-truth contract
|
|
9453
|
+
alwaysApply: true
|
|
9454
|
+
---
|
|
9455
|
+
|
|
9456
|
+
${adapter("Cursor").replace(`${GENERATED}
|
|
9457
|
+
`, "")}` }
|
|
9458
|
+
];
|
|
9459
|
+
}
|
|
9460
|
+
var atomicWrite = (path, content) => {
|
|
9461
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
9462
|
+
const next = `${path}.${process.pid}.next`;
|
|
9463
|
+
writeFileSync(next, content, { mode: 420 });
|
|
9464
|
+
renameSync(next, path);
|
|
9465
|
+
};
|
|
9466
|
+
function initializeProjectContext(rootInput, manifestInput = defaultProjectContext(rootInput), options = {}) {
|
|
9467
|
+
const root = resolve(rootInput);
|
|
9468
|
+
const manifest = parseProjectContext(manifestInput);
|
|
9469
|
+
const manifestPath = join2(root, ".forgezero", "project.json");
|
|
9470
|
+
const files = projectContextFiles(manifest);
|
|
9471
|
+
const collisions = [manifestPath, ...files.map((file) => join2(root, file.path))].filter((path) => {
|
|
9472
|
+
if (!existsSync(path))
|
|
9473
|
+
return false;
|
|
9474
|
+
if (path === manifestPath)
|
|
9475
|
+
return true;
|
|
9476
|
+
return !readFileSync(path, "utf8").startsWith(GENERATED);
|
|
9477
|
+
});
|
|
9478
|
+
if (collisions.length && !options.force) {
|
|
9479
|
+
throw new ProjectContextError(`refusing to replace existing project context: ${collisions.join(", ")}`);
|
|
9480
|
+
}
|
|
9481
|
+
atomicWrite(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
9482
|
+
`);
|
|
9483
|
+
for (const file of files)
|
|
9484
|
+
atomicWrite(join2(root, file.path), file.content);
|
|
9485
|
+
return files;
|
|
9486
|
+
}
|
|
9487
|
+
function syncProjectContext(rootInput) {
|
|
9488
|
+
const root = resolve(rootInput);
|
|
9489
|
+
const manifestPath = join2(root, ".forgezero", "project.json");
|
|
9490
|
+
if (!existsSync(manifestPath))
|
|
9491
|
+
throw new ProjectContextError("No .forgezero/project.json. Run `fz project init`.");
|
|
9492
|
+
const manifest = parseProjectContext(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
9493
|
+
const files = projectContextFiles(manifest);
|
|
9494
|
+
for (const file of files) {
|
|
9495
|
+
const path = join2(root, file.path);
|
|
9496
|
+
if (existsSync(path) && !readFileSync(path, "utf8").startsWith(GENERATED)) {
|
|
9497
|
+
throw new ProjectContextError(`refusing to replace non-generated adapter: ${file.path}`);
|
|
9498
|
+
}
|
|
9499
|
+
atomicWrite(path, file.content);
|
|
9500
|
+
}
|
|
9501
|
+
return files;
|
|
9502
|
+
}
|
|
9503
|
+
function checkProjectContext(rootInput) {
|
|
9504
|
+
const root = resolve(rootInput);
|
|
9505
|
+
const manifestPath = join2(root, ".forgezero", "project.json");
|
|
9506
|
+
if (!existsSync(manifestPath))
|
|
9507
|
+
return { ok: false, problems: ["missing .forgezero/project.json"] };
|
|
9508
|
+
let manifest;
|
|
9509
|
+
try {
|
|
9510
|
+
manifest = parseProjectContext(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
9511
|
+
} catch (cause) {
|
|
9512
|
+
return { ok: false, problems: [cause instanceof Error ? cause.message : String(cause)] };
|
|
9513
|
+
}
|
|
9514
|
+
const problems = [];
|
|
9515
|
+
for (const source of manifest.truth) {
|
|
9516
|
+
if (!existsSync(join2(root, source.path)))
|
|
9517
|
+
problems.push(`missing truth source: ${source.path}`);
|
|
9518
|
+
}
|
|
9519
|
+
for (const path of manifest.readFirst) {
|
|
9520
|
+
if (!existsSync(join2(root, path)))
|
|
9521
|
+
problems.push(`missing read-first file: ${path}`);
|
|
9522
|
+
}
|
|
9523
|
+
for (const file of projectContextFiles(manifest)) {
|
|
9524
|
+
const path = join2(root, file.path);
|
|
9525
|
+
if (!existsSync(path))
|
|
9526
|
+
problems.push(`missing generated adapter: ${file.path}`);
|
|
9527
|
+
else if (readFileSync(path, "utf8") !== file.content)
|
|
9528
|
+
problems.push(`drifted generated adapter: ${file.path}`);
|
|
9529
|
+
}
|
|
9530
|
+
return { ok: problems.length === 0, problems };
|
|
9531
|
+
}
|
|
9532
|
+
|
|
9278
9533
|
// src/cli/index.ts
|
|
9279
9534
|
var DEFAULT_MODE = THRESHOLD_MODES[0].id;
|
|
9280
9535
|
var RECOMMENDED_MODE = (THRESHOLD_MODES.find((mode) => mode.recommended) ?? THRESHOLD_MODES[0]).id;
|
|
@@ -9290,7 +9545,9 @@ function parseOptions(argv) {
|
|
|
9290
9545
|
mode: DEFAULT_MODE,
|
|
9291
9546
|
user: process.env.FZ_USER ?? "operator",
|
|
9292
9547
|
email: process.env.FZ_EMAIL ?? "operator@localhost",
|
|
9293
|
-
preserveEnv: false
|
|
9548
|
+
preserveEnv: false,
|
|
9549
|
+
projectRoot: process.cwd(),
|
|
9550
|
+
force: false
|
|
9294
9551
|
};
|
|
9295
9552
|
const positional = [];
|
|
9296
9553
|
for (let index = 0;index < argv.length; index += 1) {
|
|
@@ -9309,6 +9566,14 @@ function parseOptions(argv) {
|
|
|
9309
9566
|
options.enrol = true;
|
|
9310
9567
|
else if (token === "--preserve-env")
|
|
9311
9568
|
options.preserveEnv = true;
|
|
9569
|
+
else if (token === "--root")
|
|
9570
|
+
options.projectRoot = argv[++index] ?? options.projectRoot;
|
|
9571
|
+
else if (token === "--name")
|
|
9572
|
+
options.projectName = argv[++index];
|
|
9573
|
+
else if (token === "--purpose")
|
|
9574
|
+
options.projectPurpose = argv[++index];
|
|
9575
|
+
else if (token === "--force")
|
|
9576
|
+
options.force = true;
|
|
9312
9577
|
else if (token === "--key")
|
|
9313
9578
|
options.key = argv[++index];
|
|
9314
9579
|
else if (token === "--mode")
|
|
@@ -9327,15 +9592,15 @@ function parseOptions(argv) {
|
|
|
9327
9592
|
return { command: positional[0] ?? "help", args: positional.slice(1), options };
|
|
9328
9593
|
}
|
|
9329
9594
|
var out = {
|
|
9330
|
-
line: (
|
|
9595
|
+
line: (text2 = "") => process.stdout.write(`${text2}
|
|
9331
9596
|
`),
|
|
9332
|
-
step: (
|
|
9597
|
+
step: (text2) => process.stdout.write(` ${text2}
|
|
9333
9598
|
`),
|
|
9334
|
-
warn: (
|
|
9599
|
+
warn: (text2) => process.stderr.write(` ! ${text2}
|
|
9335
9600
|
`),
|
|
9336
|
-
fail: (
|
|
9601
|
+
fail: (text2) => process.stderr.write(` \u2717 ${text2}
|
|
9337
9602
|
`),
|
|
9338
|
-
ok: (
|
|
9603
|
+
ok: (text2) => process.stdout.write(` \u2713 ${text2}
|
|
9339
9604
|
`)
|
|
9340
9605
|
};
|
|
9341
9606
|
var sessionCookie = null;
|
|
@@ -9490,7 +9755,7 @@ async function cmdAgent(options, args) {
|
|
|
9490
9755
|
controlSocketPath: process.env.FZ_CONTROL_SOCKET,
|
|
9491
9756
|
repository: process.env.FZ_DEPLOY_REPO,
|
|
9492
9757
|
branch: process.env.FZ_DEPLOY_BRANCH,
|
|
9493
|
-
|
|
9758
|
+
profile: process.env.FZ_DEPLOY_PROFILE,
|
|
9494
9759
|
deployRoot: process.env.FZ_DEPLOY_ROOT,
|
|
9495
9760
|
publicApiUrl: process.env.FZ_PUBLIC_API_URL,
|
|
9496
9761
|
deploymentEnvironment: parseAssignments(process.env.FZ_DEPLOY_ENV),
|
|
@@ -9540,17 +9805,17 @@ async function cmdAgent(options, args) {
|
|
|
9540
9805
|
return 0;
|
|
9541
9806
|
}
|
|
9542
9807
|
try {
|
|
9543
|
-
|
|
9808
|
+
writeFileSync2(plan.unitPath, plan.unit, { mode: 420 });
|
|
9544
9809
|
out.ok(`Wrote ${plan.unitPath}`);
|
|
9545
9810
|
for (const auxiliary of plan.auxiliaryUnits) {
|
|
9546
|
-
|
|
9547
|
-
|
|
9811
|
+
mkdirSync2(dirname2(auxiliary.path), { recursive: true, mode: 493 });
|
|
9812
|
+
writeFileSync2(auxiliary.path, auxiliary.unit, { mode: 420 });
|
|
9548
9813
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
9549
9814
|
}
|
|
9550
9815
|
if (options.enrol) {
|
|
9551
|
-
if (
|
|
9816
|
+
if (existsSync2(enrolTokenSourcePath)) {
|
|
9552
9817
|
const source = statSync(enrolTokenSourcePath);
|
|
9553
|
-
const token =
|
|
9818
|
+
const token = readFileSync2(enrolTokenSourcePath, "utf8").trim();
|
|
9554
9819
|
if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
|
|
9555
9820
|
throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
|
|
9556
9821
|
}
|
|
@@ -9563,7 +9828,7 @@ async function cmdAgent(options, args) {
|
|
|
9563
9828
|
if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
|
|
9564
9829
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
9565
9830
|
}
|
|
9566
|
-
|
|
9831
|
+
writeFileSync2(enrolTokenSourcePath, `${token}
|
|
9567
9832
|
`, { mode: 384, flag: "wx" });
|
|
9568
9833
|
}
|
|
9569
9834
|
}
|
|
@@ -9574,7 +9839,7 @@ async function cmdAgent(options, args) {
|
|
|
9574
9839
|
out.line();
|
|
9575
9840
|
out.line(" Add this machine-specific PUBLIC key as a read-only deploy key:");
|
|
9576
9841
|
out.line();
|
|
9577
|
-
out.line(` ${
|
|
9842
|
+
out.line(` ${readFileSync2(gitPublicKeyPath, "utf8").trim()}`);
|
|
9578
9843
|
out.line();
|
|
9579
9844
|
return 0;
|
|
9580
9845
|
} catch (cause) {
|
|
@@ -9691,6 +9956,43 @@ Is fz-agent running on this machine? \`fz agent install --apply\`.`);
|
|
|
9691
9956
|
out.line(describeInjection(merged, Object.keys(secrets).length));
|
|
9692
9957
|
return spawnWith(command, merged.env, (line) => out.line(line));
|
|
9693
9958
|
}
|
|
9959
|
+
function cmdProject(options, args) {
|
|
9960
|
+
const operation = args[0] ?? "check";
|
|
9961
|
+
try {
|
|
9962
|
+
if (operation === "init") {
|
|
9963
|
+
const manifest = defaultProjectContext(options.projectRoot);
|
|
9964
|
+
if (options.projectName)
|
|
9965
|
+
manifest.name = options.projectName;
|
|
9966
|
+
if (options.projectPurpose)
|
|
9967
|
+
manifest.purpose = options.projectPurpose;
|
|
9968
|
+
const files = initializeProjectContext(options.projectRoot, manifest, { force: options.force });
|
|
9969
|
+
out.ok(`Project context initialized; ${files.length} generated adapters now share one Git source.`);
|
|
9970
|
+
out.step("Edit .forgezero/project.json, create every named truth source, then run `fz project sync`.");
|
|
9971
|
+
return 0;
|
|
9972
|
+
}
|
|
9973
|
+
if (operation === "sync") {
|
|
9974
|
+
const files = syncProjectContext(options.projectRoot);
|
|
9975
|
+
out.ok(`Synchronized ${files.length} AI adapters from .forgezero/project.json.`);
|
|
9976
|
+
return 0;
|
|
9977
|
+
}
|
|
9978
|
+
if (operation === "check") {
|
|
9979
|
+
const result = checkProjectContext(options.projectRoot);
|
|
9980
|
+
if (options.json)
|
|
9981
|
+
out.line(JSON.stringify(result, null, 2));
|
|
9982
|
+
else if (result.ok)
|
|
9983
|
+
out.ok("Project context and every AI adapter are in sync.");
|
|
9984
|
+
else
|
|
9985
|
+
for (const problem of result.problems)
|
|
9986
|
+
out.fail(problem);
|
|
9987
|
+
return result.ok ? 0 : 1;
|
|
9988
|
+
}
|
|
9989
|
+
out.fail("Usage: fz project init|sync|check [--root <path>] [--force]");
|
|
9990
|
+
return 2;
|
|
9991
|
+
} catch (cause) {
|
|
9992
|
+
out.fail(cause instanceof Error ? cause.message : String(cause));
|
|
9993
|
+
return 1;
|
|
9994
|
+
}
|
|
9995
|
+
}
|
|
9694
9996
|
function usage() {
|
|
9695
9997
|
out.line(`
|
|
9696
9998
|
fz ${VERSION2} \u2014 ForgeZero control surface
|
|
@@ -9712,6 +10014,9 @@ function usage() {
|
|
|
9712
10014
|
fz agent install Install the node agent as a systemd service, so
|
|
9713
10015
|
applications on this box read secrets through a
|
|
9714
10016
|
local socket instead of holding an API key
|
|
10017
|
+
fz project init Create vendor-neutral, Git-persisted AI context
|
|
10018
|
+
fz project sync Regenerate Claude/Codex/Gemini/Copilot/Cursor adapters
|
|
10019
|
+
fz project check Fail when truth sources or generated adapters drift
|
|
9715
10020
|
|
|
9716
10021
|
CEREMONY OPTIONS
|
|
9717
10022
|
--key <fp|index> Which agent key to use for custody
|
|
@@ -9734,6 +10039,10 @@ function usage() {
|
|
|
9734
10039
|
--apply Write the unit rather than printing it (root)
|
|
9735
10040
|
--enrol Bind this machine with a one-time token prompted
|
|
9736
10041
|
securely by systemd (tenant-owned compute)
|
|
10042
|
+
--root <path> Project root for project init/sync/check
|
|
10043
|
+
--name <name> Project name during init
|
|
10044
|
+
--purpose <text> Product outcome during init
|
|
10045
|
+
--force Init may replace existing AI instruction files
|
|
9737
10046
|
|
|
9738
10047
|
CUSTODY FACTORS
|
|
9739
10048
|
Every custodian share is sealed TWICE and either envelope alone opens it:
|
|
@@ -9762,6 +10071,9 @@ async function runCli() {
|
|
|
9762
10071
|
case "agent":
|
|
9763
10072
|
code = await cmdAgent(options, args);
|
|
9764
10073
|
break;
|
|
10074
|
+
case "project":
|
|
10075
|
+
code = cmdProject(options, args);
|
|
10076
|
+
break;
|
|
9765
10077
|
case "genesis":
|
|
9766
10078
|
code = await cmdGenesis(options);
|
|
9767
10079
|
break;
|
|
@@ -9785,7 +10097,7 @@ async function runCli() {
|
|
|
9785
10097
|
usage();
|
|
9786
10098
|
code = 1;
|
|
9787
10099
|
}
|
|
9788
|
-
if (args.length > 0 && code === 0 && command !== "agent") {
|
|
10100
|
+
if (args.length > 0 && code === 0 && command !== "agent" && command !== "project") {
|
|
9789
10101
|
out.warn(`Ignored: ${args.join(" ")}`);
|
|
9790
10102
|
}
|
|
9791
10103
|
process.exit(code);
|
package/dist/index.d.ts
CHANGED
|
@@ -32,8 +32,8 @@ export type { AgentRelease, StagedAgentRelease, UpdateCommand, UpdateCommandResu
|
|
|
32
32
|
export { activateAgentRelease, probeAgentSocket, requestAgentUpdate, startAgentUpdateHelper, AGENT_UPDATE_GROUP, AGENT_UPDATE_HELPER_UNIT_PATH, AGENT_UPDATE_RECEIPT } from './agent-update-helper';
|
|
33
33
|
export type { AgentUpdateRequest, AgentUpdateResponse } from './agent-update-helper';
|
|
34
34
|
export { DEFAULT_SOFTWARE_HELPER_SOCKET, requestSoftware, startSoftwareHelper, SOFTWARE_HELPER_GROUP, SOFTWARE_HELPER_UNIT_PATH } from './software-helper';
|
|
35
|
-
export { ensureSoftwareRequirements, observeSoftwareHost, validateSoftwareRequirements } from './software';
|
|
36
|
-
export type { SoftwareCommandResult, SoftwareExec, SoftwareObservation, SoftwareRequirement } from './software';
|
|
35
|
+
export { ensureSoftwareRequirements, observeSoftwareHost, validateSoftwareRequirements, OS_CATALOG, SOFTWARE_CATALOG } from './software';
|
|
36
|
+
export type { CatalogStatus, DeploymentChannel, OsCatalogEntry, SoftwareCatalogEntry, SoftwareCommandResult, SoftwareExec, SoftwareId, SoftwareObservation, SoftwareRequirement } from './software';
|
|
37
37
|
export { heartbeatAgentOnce, observeAgentHost, startAgentHeartbeat } from './agent-heartbeat';
|
|
38
38
|
export type { AgentHeartbeatOptions, AgentHeartbeatResponse, AgentObservation } from './agent-heartbeat';
|
|
39
39
|
export { DEFAULT_DEPLOYMENT_RUNNER_SOCKET, requestDeploymentCommand, startDeploymentRunner } from './deployment-runner';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const PROJECT_CONTEXT_VERSION: 1;
|
|
2
|
+
export interface ProjectTruthSource {
|
|
3
|
+
area: string;
|
|
4
|
+
path: string;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProjectContextManifest {
|
|
8
|
+
schemaVersion: typeof PROJECT_CONTEXT_VERSION;
|
|
9
|
+
name: string;
|
|
10
|
+
purpose: string;
|
|
11
|
+
truth: readonly ProjectTruthSource[];
|
|
12
|
+
readFirst: readonly string[];
|
|
13
|
+
verify: readonly string[];
|
|
14
|
+
rules: readonly string[];
|
|
15
|
+
/** Files or directories that are generated, historical, or otherwise not authoritative. */
|
|
16
|
+
nonAuthoritative: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ContextFile {
|
|
19
|
+
path: string;
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ContextCheck {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
problems: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare class ProjectContextError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
export declare function parseProjectContext(value: unknown): ProjectContextManifest;
|
|
30
|
+
export declare function defaultProjectContext(root?: string): ProjectContextManifest;
|
|
31
|
+
export declare function renderProjectContext(manifest: ProjectContextManifest): string;
|
|
32
|
+
export declare function projectContextFiles(manifestInput: ProjectContextManifest): ContextFile[];
|
|
33
|
+
export declare function initializeProjectContext(rootInput: string, manifestInput?: ProjectContextManifest, options?: {
|
|
34
|
+
force?: boolean;
|
|
35
|
+
}): ContextFile[];
|
|
36
|
+
export declare function syncProjectContext(rootInput: string): ContextFile[];
|
|
37
|
+
export declare function checkProjectContext(rootInput: string): ContextCheck;
|