@kylecheng3146/agent-ops 0.5.3 → 0.7.0
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 +103 -1
- package/dist/packages/cli/src/args.js +183 -3
- package/dist/packages/cli/src/bin.js +76 -19
- package/dist/packages/cli/src/cli.js +53 -8
- package/dist/packages/cli/src/codex-loop-process.js +19 -2
- package/dist/packages/cli/src/commands/advance.js +42 -5
- package/dist/packages/cli/src/commands/init.js +2 -0
- package/dist/packages/cli/src/commands/office.js +20 -0
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +100 -9
- package/dist/packages/cli/src/commands/run.js +115 -0
- package/dist/packages/cli/src/commands/task.js +61 -17
- package/dist/packages/cli/src/commands/update.js +8 -0
- package/dist/packages/cli/src/commands/verify.js +1 -0
- package/dist/packages/cli/src/context.js +64 -1
- package/dist/packages/cli/src/hook-entry.js +2 -1
- package/dist/packages/cli/src/hook-process.js +54 -3
- package/dist/packages/cli/src/loop-entry.js +2 -1
- package/dist/packages/cli/src/office-entry.js +230 -0
- package/dist/packages/cli/src/owned-run-step.js +65 -0
- package/dist/packages/cli/src/parallel-deps.js +18 -5
- package/dist/packages/cli/src/run-deps.js +321 -0
- package/dist/packages/cli/src/run-entry.js +952 -0
- package/dist/packages/cli/src/run-setup-entry.js +32 -0
- package/dist/packages/cli/src/run-step-entry.js +58 -0
- package/dist/packages/cli/src/wizard.js +14 -2
- package/dist/runtime/src/adapters/agy/config.js +4 -3
- package/dist/runtime/src/adapters/agy/events.js +9 -0
- package/dist/runtime/src/adapters/claude/config.js +3 -3
- package/dist/runtime/src/adapters/claude/events.js +8 -0
- package/dist/runtime/src/adapters/codex/config.js +3 -3
- package/dist/runtime/src/adapters/codex/events.js +8 -0
- package/dist/runtime/src/adapters/opencode/config.js +5 -4
- package/dist/runtime/src/adapters/opencode/events.js +8 -0
- package/dist/runtime/src/config/explain.js +11 -0
- package/dist/runtime/src/config/hash.js +1 -1
- package/dist/runtime/src/config/merge.js +9 -1
- package/dist/runtime/src/contracts.js +2 -2
- package/dist/runtime/src/hooks/codex-loop.js +4 -0
- package/dist/runtime/src/hooks/completion-gate.js +8 -2
- package/dist/runtime/src/install/doctor.js +2 -0
- package/dist/runtime/src/install/harness.js +44 -1
- package/dist/runtime/src/install/plan.js +16 -11
- package/dist/runtime/src/install/preauth.js +44 -18
- package/dist/runtime/src/install/profiles.js +8 -2
- package/dist/runtime/src/install/update.js +1 -0
- package/dist/runtime/src/office/collect.js +233 -0
- package/dist/runtime/src/office/page.js +873 -0
- package/dist/runtime/src/office/scene.js +168 -0
- package/dist/runtime/src/office/server.js +139 -0
- package/dist/runtime/src/office/sessions.js +115 -0
- package/dist/runtime/src/office/snapshot.js +107 -0
- package/dist/runtime/src/parallel/finish.js +85 -22
- package/dist/runtime/src/parallel/guard.js +12 -2
- package/dist/runtime/src/parallel/integrate.js +288 -10
- package/dist/runtime/src/parallel/manage.js +4 -1
- package/dist/runtime/src/parallel/receipt.js +71 -5
- package/dist/runtime/src/parallel/service.js +41 -1
- package/dist/runtime/src/review/attestation.js +36 -11
- package/dist/runtime/src/review/execute.js +24 -1
- package/dist/runtime/src/review/packet.js +35 -1
- package/dist/runtime/src/review/runner.js +12 -1
- package/dist/runtime/src/review/scope.js +18 -1
- package/dist/runtime/src/review/slots.js +35 -10
- package/dist/runtime/src/run/authorization.js +60 -0
- package/dist/runtime/src/run/budget.js +30 -0
- package/dist/runtime/src/run/controls.js +139 -0
- package/dist/runtime/src/run/convergence.js +177 -0
- package/dist/runtime/src/run/hosts/claude.js +180 -0
- package/dist/runtime/src/run/hosts/codex.js +279 -0
- package/dist/runtime/src/run/hosts/index.js +4 -0
- package/dist/runtime/src/run/hosts/protocol.js +254 -0
- package/dist/runtime/src/run/hosts/types.js +1 -0
- package/dist/runtime/src/run/hosts/util.js +136 -0
- package/dist/runtime/src/run/integration.js +204 -0
- package/dist/runtime/src/run/macOS.js +189 -0
- package/dist/runtime/src/run/ownership.js +22 -0
- package/dist/runtime/src/run/phase.js +37 -0
- package/dist/runtime/src/run/planning.js +25 -0
- package/dist/runtime/src/run/policy-transition.js +1076 -0
- package/dist/runtime/src/run/policy.js +176 -0
- package/dist/runtime/src/run/ratchet.js +71 -0
- package/dist/runtime/src/run/scheduler.js +260 -0
- package/dist/runtime/src/run/service.js +433 -0
- package/dist/runtime/src/run/supervisor.js +635 -0
- package/dist/runtime/src/run/transport.js +159 -0
- package/dist/runtime/src/run/types.js +1 -0
- package/dist/runtime/src/run/usage.js +81 -0
- package/dist/runtime/src/run/verification.js +64 -0
- package/dist/runtime/src/run/worker-gate.js +48 -0
- package/dist/runtime/src/schema/acceptance.js +91 -0
- package/dist/runtime/src/schema/validate.js +80 -9
- package/dist/runtime/src/task/completion.js +14 -2
- package/dist/runtime/src/task/contract.js +17 -0
- package/dist/runtime/src/task/pin-finding.js +81 -0
- package/dist/runtime/src/task/service.js +316 -13
- package/dist/runtime/src/task/store.js +57 -7
- package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
- package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
- package/dist/runtime/src/verify/acceptance-replay.js +239 -0
- package/dist/runtime/src/verify/adapters/index.js +5 -0
- package/dist/runtime/src/verify/adapters/jest.js +142 -0
- package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
- package/dist/runtime/src/verify/adapters/node.js +254 -0
- package/dist/runtime/src/verify/adapters/pytest.js +188 -0
- package/dist/runtime/src/verify/adapters/rust.js +84 -0
- package/dist/runtime/src/verify/adapters/shared.js +82 -0
- package/dist/runtime/src/verify/adapters/vitest.js +218 -0
- package/dist/runtime/src/verify/evidence.js +5 -2
- package/dist/runtime/src/verify/service.js +24 -8
- package/dist/runtime/src/verify/source-fingerprint.js +1 -1
- package/dist/runtime/src/verify/spawn.js +13 -2
- package/docs/en/spec/acceptance-and-evidence.md +48 -0
- package/docs/en/spec/harness-adapters.md +113 -1
- package/docs/en/spec/loop-engineering.md +49 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
- package/docs/zh-TW/spec/harness-adapters.md +93 -1
- package/docs/zh-TW/spec/loop-engineering.md +22 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +230 -19
- package/schemas/evidence.schema.json +129 -6
- package/schemas/task.schema.json +265 -6
- package/templates/acceptance/agent_ops_acceptance.rs +36 -0
- package/templates/acceptance/agent_ops_pytest.py +46 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { taskContractHash } from "../../../../runtime/src/task/contract.js";
|
|
1
3
|
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
2
4
|
import { renderTaskMarkdown, safeTaskText } from "../../../../runtime/src/task/render.js";
|
|
3
5
|
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
@@ -12,20 +14,21 @@ function parseCriterion(source) {
|
|
|
12
14
|
catch {
|
|
13
15
|
throw new AgentOpsError("TASK_CRITERION_INVALID", "Each --criterion value must be a JSON criterion object.");
|
|
14
16
|
}
|
|
15
|
-
if (!isRecord(value)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
if (!isRecord(value))
|
|
18
|
+
throw new AgentOpsError("TASK_CRITERION_INVALID", "Criterion must be an object.");
|
|
19
|
+
// TaskService normalizes the creation baseline, then uses the shared strict validator.
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
async function jsonFile(path) {
|
|
23
|
+
const status = await lstat(path);
|
|
24
|
+
if (!status.isFile() || status.isSymbolicLink() || status.size > 65536)
|
|
25
|
+
throw new AgentOpsError("TASK_PAYLOAD_INVALID", "Payload must be a regular JSON file at most 64 KiB.");
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new AgentOpsError("TASK_PAYLOAD_INVALID", "Invalid JSON payload.");
|
|
23
31
|
}
|
|
24
|
-
return {
|
|
25
|
-
id: value.id,
|
|
26
|
-
description: value.description,
|
|
27
|
-
verifierIds: [...value.verifierIds]
|
|
28
|
-
};
|
|
29
32
|
}
|
|
30
33
|
function parseEvidence(values) {
|
|
31
34
|
const evidence = {};
|
|
@@ -57,7 +60,7 @@ function taskAction(args) {
|
|
|
57
60
|
"complete",
|
|
58
61
|
"create",
|
|
59
62
|
"export",
|
|
60
|
-
"status"
|
|
63
|
+
"status", "revise", "replan", "pin-finding"
|
|
61
64
|
].includes(args.action)) {
|
|
62
65
|
throw new AgentOpsError("TASK_ACTION_INVALID", "A supported task action is required.");
|
|
63
66
|
}
|
|
@@ -68,7 +71,8 @@ function taskEnvelope(action, code, message, record) {
|
|
|
68
71
|
action,
|
|
69
72
|
message,
|
|
70
73
|
record,
|
|
71
|
-
|
|
74
|
+
contractHash: taskContractHash(record.task),
|
|
75
|
+
text: renderTaskMarkdown(record) + "\nContract: " + taskContractHash(record.task)
|
|
72
76
|
});
|
|
73
77
|
}
|
|
74
78
|
function renderTaskList(records) {
|
|
@@ -93,7 +97,10 @@ export async function runTaskCommand(options) {
|
|
|
93
97
|
const record = await (worktree?.service ?? options.service).create({
|
|
94
98
|
title: options.args.title,
|
|
95
99
|
...(options.args.intent === undefined ? {} : { intent: options.args.intent }),
|
|
96
|
-
criteria: (options.args.criteria ?? []).map(parseCriterion),
|
|
100
|
+
criteria: [...(options.args.criteria ?? []).map(parseCriterion),
|
|
101
|
+
...await Promise.all((options.args.criterionFiles ?? []).map(async (path) => await jsonFile(path)))]
|
|
102
|
+
.map(c => c.acceptance === undefined || options.args.baseline === undefined ? c :
|
|
103
|
+
{ ...c, acceptance: { ...c.acceptance, baselineCommit: c.acceptance.baselineCommit ?? options.args.baseline } }),
|
|
97
104
|
...(options.policyConfigHash === undefined
|
|
98
105
|
? {}
|
|
99
106
|
: { policyConfigHash: options.policyConfigHash }),
|
|
@@ -127,6 +134,35 @@ export async function runTaskCommand(options) {
|
|
|
127
134
|
}
|
|
128
135
|
};
|
|
129
136
|
}
|
|
137
|
+
if (action === "revise") {
|
|
138
|
+
const payload = await jsonFile(options.args.criteriaFile);
|
|
139
|
+
if (!Array.isArray(payload))
|
|
140
|
+
throw new AgentOpsError("TASK_PAYLOAD_INVALID", "Criteria file must contain an array.");
|
|
141
|
+
const record = await options.service.revise(requireTaskId(options.args), {
|
|
142
|
+
expectedContractHash: options.args.expectedContract, criteria: payload,
|
|
143
|
+
reason: options.args.reason, ...(options.args.baseline === undefined ? {} : { baseline: options.args.baseline })
|
|
144
|
+
});
|
|
145
|
+
return taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain new verification and fresh review.", record);
|
|
146
|
+
}
|
|
147
|
+
if (action === "replan") {
|
|
148
|
+
const payload = await jsonFile(options.args.planFile);
|
|
149
|
+
if (!Array.isArray(payload))
|
|
150
|
+
throw new AgentOpsError("TASK_PAYLOAD_INVALID", "Plan file must contain split task objects.");
|
|
151
|
+
const records = await options.service.replan(requireTaskId(options.args), {
|
|
152
|
+
expectedTreeContractHash: options.args.expectedTreeContract, reason: options.args.reason,
|
|
153
|
+
tasks: payload
|
|
154
|
+
});
|
|
155
|
+
return okEnvelope("TASK_REPLANNED", { action, message: "Task tree split atomically; reverify and review.", records, text: renderTaskList(records) });
|
|
156
|
+
}
|
|
157
|
+
if (action === "pin-finding") {
|
|
158
|
+
const criterion = parseCriterion(JSON.stringify(await jsonFile(options.args.criterionFiles[0])));
|
|
159
|
+
const pinned = await options.service.pinFinding(requireTaskId(options.args), options.args.expectedContract, options.args.findingReference, criterion, options.args.reason);
|
|
160
|
+
if (pinned.record === null)
|
|
161
|
+
return { code: "NEEDS_REPLAN", status: "error",
|
|
162
|
+
data: { action, message: "Pin requires atomic replan to retain two to five criteria.", pendingCriterion: pinned.pendingCriterion, text: "NEEDS_REPLAN: include the pending criterion in task replan; no task was changed." },
|
|
163
|
+
errors: [{ code: "NEEDS_REPLAN", message: "Replan the full current tree and pending pin." }] };
|
|
164
|
+
return taskEnvelope(action, "TASK_FINDING_PINNED", "Pinned saved finding; reverify and review the new contract.", pinned.record);
|
|
165
|
+
}
|
|
130
166
|
if (action === "status") {
|
|
131
167
|
if (options.args.taskId === undefined && sessionId === undefined) {
|
|
132
168
|
const parentTaskId = options.args.parentTaskId;
|
|
@@ -143,7 +179,15 @@ export async function runTaskCommand(options) {
|
|
|
143
179
|
const record = await options.service.status(options.args.taskId === undefined
|
|
144
180
|
? { sessionId }
|
|
145
181
|
: { taskId: options.args.taskId });
|
|
146
|
-
|
|
182
|
+
const coverage = await options.service.coverage(record.task.id);
|
|
183
|
+
const proven = coverage.rows.filter(row => row.status === "proven").length;
|
|
184
|
+
const mechanical = coverage.rows.filter(row => row.mode === "behavioral" || row.mode === "invariant").length;
|
|
185
|
+
const reviewOnly = coverage.rows.filter(row => row.mode === "review-only").length;
|
|
186
|
+
const legacy = coverage.rows.filter(row => row.mode === "legacy").length;
|
|
187
|
+
const envelope = taskEnvelope(action, "TASK_STATUS", `Read task ${record.task.id}.`, record);
|
|
188
|
+
return { ...envelope, data: { ...envelope.data, coverage, treeContractHash: await options.service.treeContract(record.task.id),
|
|
189
|
+
text: envelope.data.text + "\nAcceptance: " + proven + "/" + mechanical + " mechanical proven; " + reviewOnly + " review-only; " + legacy + " legacy; originally mechanical " + coverage.originalMechanical +
|
|
190
|
+
"; reviewed " + coverage.reviewed + "; undischarged " + coverage.rows.filter(row => row.status !== "proven" && !coverage.reviewed).length } };
|
|
147
191
|
}
|
|
148
192
|
if (action === "attach") {
|
|
149
193
|
if (sessionId === undefined) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { loadConfigFile } from "../../../../runtime/src/config/load.js";
|
|
3
4
|
import { detectWorktreeSetup } from "../../../../runtime/src/install/parallel-setup.js";
|
|
4
5
|
import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
|
|
5
6
|
import { okEnvelope } from "../output.js";
|
|
@@ -15,6 +16,7 @@ export function formatUpdatePlan(plan, trust) {
|
|
|
15
16
|
title: "Update plan",
|
|
16
17
|
metadata: [
|
|
17
18
|
`Target version: ${plan.targetVersion}`,
|
|
19
|
+
`Office (Preview): ${plan.installation.config.features.office?.enabled === true ? "enabled" : "disabled"}`,
|
|
18
20
|
`Harness: ${plan.installation.harness.join(", ")}`,
|
|
19
21
|
...(plan.installation.config.worktree === undefined
|
|
20
22
|
? []
|
|
@@ -95,9 +97,15 @@ export async function runUpdateCommand(options) {
|
|
|
95
97
|
};
|
|
96
98
|
}
|
|
97
99
|
}
|
|
100
|
+
let officeEnabled = options.args.office === undefined ? undefined : options.args.office === "on";
|
|
101
|
+
if (officeEnabled === undefined && options.promptOffice !== undefined && options.isTTY && !options.args.yes) {
|
|
102
|
+
const { config } = await loadConfigFile(join(options.root, ".agent-ops", "config.json"));
|
|
103
|
+
officeEnabled = await options.promptOffice("Enable Office (Preview)?", config.features.office?.enabled === true);
|
|
104
|
+
}
|
|
98
105
|
const plan = await createUpdatePlan({
|
|
99
106
|
root: options.root,
|
|
100
107
|
adapters: options.adapters,
|
|
108
|
+
...(officeEnabled === undefined ? {} : { officeEnabled }),
|
|
101
109
|
...(options.args.harness === undefined
|
|
102
110
|
? {}
|
|
103
111
|
: { harness: options.args.harness }),
|
|
@@ -7,13 +7,16 @@ import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
|
7
7
|
import { loadConfigFile } from "../../../runtime/src/config/load.js";
|
|
8
8
|
import { mergeConfigLayers } from "../../../runtime/src/config/merge.js";
|
|
9
9
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
10
|
-
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
10
|
+
import { localStatePaths, readPrivateFile } from "../../../runtime/src/security/permissions.js";
|
|
11
|
+
import { FileRunRepository } from "../../../runtime/src/run/service.js";
|
|
12
|
+
import { readRunPolicy, runRuntimeHash } from "../../../runtime/src/run/policy.js";
|
|
11
13
|
import { calculateTrustBinding, FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
12
14
|
export const DEFAULT_CONFIG = {
|
|
13
15
|
schemaVersion: 3,
|
|
14
16
|
profiles: [],
|
|
15
17
|
verification: { commands: [] },
|
|
16
18
|
features: {
|
|
19
|
+
office: { enabled: false },
|
|
17
20
|
stopVerification: {
|
|
18
21
|
enabled: false
|
|
19
22
|
},
|
|
@@ -72,6 +75,14 @@ async function loadOptionalConfig(path) {
|
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
export async function loadEffectiveConfig(root, scope, projectOverride) {
|
|
78
|
+
if (scope === "project" && projectOverride === undefined) {
|
|
79
|
+
const run = await runPolicyContext(root);
|
|
80
|
+
if (run !== null)
|
|
81
|
+
return mergeConfigLayers([defaultConfigLayer(), {
|
|
82
|
+
source: "project", sourcePath: join(run.state.commonDir, "agent-ops/runs", run.state.runId, "policies", run.state.policyBinding.artifactDigest + ".json"),
|
|
83
|
+
config: run.policy.config
|
|
84
|
+
}]);
|
|
85
|
+
}
|
|
75
86
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
76
87
|
const userPath = join(home, ".agent-ops", "config.json");
|
|
77
88
|
const projectPath = join(scope === "user" ? root : projectConfigRoot(root), ".agent-ops", "config.json");
|
|
@@ -184,6 +195,14 @@ export async function repositoryTrust(root, config, cliVersion) {
|
|
|
184
195
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
185
196
|
const state = localStatePaths(home);
|
|
186
197
|
try {
|
|
198
|
+
const run = await runPolicyContext(root);
|
|
199
|
+
if (run !== null) {
|
|
200
|
+
if (run.state.status !== "active" || run.state.disableRestart || run.state.awaitingResume ||
|
|
201
|
+
Date.parse(run.policy.expiresAt) <= Date.now() || calculateConfigHash(config) !== run.state.policyBinding.configHash)
|
|
202
|
+
return "STALE";
|
|
203
|
+
const store = new FileTrustStore(state.trustStore, state.anchorDirectory);
|
|
204
|
+
return (await store.status(run.policy.baseTrustBinding)).status;
|
|
205
|
+
}
|
|
187
206
|
const binding = await repositoryTrustBinding(root, config, cliVersion);
|
|
188
207
|
return (await new FileTrustStore(state.trustStore, state.anchorDirectory).status(binding)).status;
|
|
189
208
|
}
|
|
@@ -191,6 +210,50 @@ export async function repositoryTrust(root, config, cliVersion) {
|
|
|
191
210
|
return "UNTRUSTED";
|
|
192
211
|
}
|
|
193
212
|
}
|
|
213
|
+
/** A run-private capability is usable only by its registered checkout or proof group. */
|
|
214
|
+
export async function runPolicyContext(root) {
|
|
215
|
+
let common;
|
|
216
|
+
try {
|
|
217
|
+
common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const source = await readPrivateFile(join(root, ".agent-ops/tasks/worktree.json"), root);
|
|
223
|
+
const record = source === null ? null : JSON.parse(source);
|
|
224
|
+
const runId = process.env.AGENT_OPS_RUN_ID ?? record?.runId;
|
|
225
|
+
if (runId === undefined)
|
|
226
|
+
return null;
|
|
227
|
+
const repository = new FileRunRepository(join(common, "agent-ops/runs"), common);
|
|
228
|
+
const saved = await repository.read(runId);
|
|
229
|
+
if (saved === null || saved.policyBinding === undefined)
|
|
230
|
+
return null;
|
|
231
|
+
const here = realpathSync(root);
|
|
232
|
+
const worker = saved.workers.find(w => w.worktree === here);
|
|
233
|
+
const proofProcess = worker?.proofProcess ?? saved.proofProcess;
|
|
234
|
+
let proof = false;
|
|
235
|
+
if (proofProcess != null && String(proofProcess.processId) === process.env.AGENT_OPS_RUN_PROOF_PID) {
|
|
236
|
+
try {
|
|
237
|
+
proof = execFileSync("ps", ["-o", "pgid=", "-p", String(process.pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() === String(proofProcess.processId) &&
|
|
238
|
+
execFileSync("ps", ["-o", "lstart=", "-p", String(proofProcess.processId)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() === proofProcess.processIdentity;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
proof = false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if ((!proof && (worker === undefined || record?.runId !== runId || record.workerId !== worker.workerId ||
|
|
245
|
+
record.ownerSessionId !== worker.ownerSessionId || record.coordinatorId !== saved.coordinatorId || record.workerGeneration !== worker.generation)) ||
|
|
246
|
+
(worker === undefined && !(proof && saved.root === here)) ||
|
|
247
|
+
(process.env.AGENT_OPS_WORKER_ID !== undefined && (process.env.AGENT_OPS_WORKER_ID !== (worker?.workerId ?? saved.coordinatorId) ||
|
|
248
|
+
process.env.AGENT_OPS_WORKER_GENERATION !== String((worker ?? saved.workers.find(w => w.workerId === saved.coordinatorId))?.generation))))
|
|
249
|
+
throw new AgentOpsError("RUN_WORKER_STALE", "Run policy requires the current registered worker generation or proof process.");
|
|
250
|
+
if (saved.status !== "active" || saved.disableRestart || saved.awaitingResume)
|
|
251
|
+
throw new AgentOpsError("RUN_POLICY_DISABLED", "Run execution policy is inactive; explicit resume is required.");
|
|
252
|
+
const policy = await readRunPolicy(saved, await runRuntimeHash());
|
|
253
|
+
if (policy === null)
|
|
254
|
+
return null;
|
|
255
|
+
return { repository, state: saved, policy, worker };
|
|
256
|
+
}
|
|
194
257
|
export async function repositoryTrustBinding(root, config, cliVersion) {
|
|
195
258
|
return await calculateTrustBinding({
|
|
196
259
|
repositoryPath: root,
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
import { CLI_VERSION } from "./version.js";
|
|
5
5
|
import { runHookProcess } from "./hook-process.js";
|
|
6
6
|
import { worktreeDependencies } from "./parallel-deps.js";
|
|
7
|
+
import { observeOfficeSession } from "./office-entry.js";
|
|
7
8
|
process.exitCode = await runHookProcess(process.argv.slice(2), {
|
|
8
9
|
stdin: process.stdin,
|
|
9
10
|
writeStdout: (value) => process.stdout.write(value),
|
|
10
11
|
writeStderr: (value) => process.stderr.write(value)
|
|
11
|
-
}, CLI_VERSION, { worktree: worktreeDependencies() });
|
|
12
|
+
}, CLI_VERSION, { worktree: worktreeDependencies(), office: observeOfficeSession });
|
|
@@ -9,6 +9,7 @@ import { resolveContainedPath } from "../../../runtime/src/fs/paths.js";
|
|
|
9
9
|
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
10
10
|
import { claudeStopRecursionMarker } from "../../../runtime/src/adapters/claude/input.js";
|
|
11
11
|
import { HARNESS_IDS, harnessDescriptor } from "../../../runtime/src/install/harness.js";
|
|
12
|
+
import { officePhaseHint } from "./office-entry.js";
|
|
12
13
|
import { evaluateWorktreeEnter } from "../../../runtime/src/parallel/enter.js";
|
|
13
14
|
import { evaluateWorktreeWrite, resolveMainRoot } from "../../../runtime/src/parallel/guard.js";
|
|
14
15
|
import { ensureSessionWorktree, recordSessionOrigin } from "../../../runtime/src/parallel/service.js";
|
|
@@ -16,6 +17,9 @@ import { CompletionGateService } from "../../../runtime/src/hooks/completion-gat
|
|
|
16
17
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
17
18
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
18
19
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
20
|
+
import { readWorktreeRecord } from "../../../runtime/src/parallel/service.js";
|
|
21
|
+
import { FileRunRepository } from "../../../runtime/src/run/service.js";
|
|
22
|
+
import { validateWorkerStopHandoff } from "../../../runtime/src/run/worker-gate.js";
|
|
19
23
|
import { STOP_VERIFICATION_ENV, StopVerificationService } from "../../../runtime/src/hooks/stop-service.js";
|
|
20
24
|
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
21
25
|
import { normalizeHookInput, runHookCommand, HOOK_EVENTS } from "./commands/hook.js";
|
|
@@ -281,7 +285,11 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
281
285
|
parsedInput !== null &&
|
|
282
286
|
typeof parsedInput.projectRoot === "string"
|
|
283
287
|
? parsedInput.projectRoot
|
|
284
|
-
:
|
|
288
|
+
: typeof parsedInput === "object" &&
|
|
289
|
+
parsedInput !== null &&
|
|
290
|
+
typeof parsedInput.cwd === "string"
|
|
291
|
+
? parsedInput.cwd
|
|
292
|
+
: undefined;
|
|
285
293
|
if (inputRoot !== undefined) {
|
|
286
294
|
root = inputRoot;
|
|
287
295
|
}
|
|
@@ -323,6 +331,22 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
323
331
|
? await repositoryTrust(root, config, cliVersion)
|
|
324
332
|
: await dependencies.trust(root, config, cliVersion);
|
|
325
333
|
const trusted = trustStatus === "TRUSTED";
|
|
334
|
+
if (dependencies.office !== undefined && config.features.office?.enabled === true) {
|
|
335
|
+
const normalized = normalizeHookInput(harnessId, parsedInput);
|
|
336
|
+
if (normalized !== null) {
|
|
337
|
+
const phase = officePhaseHint(normalized);
|
|
338
|
+
void dependencies.office({
|
|
339
|
+
root,
|
|
340
|
+
harness: harnessId,
|
|
341
|
+
event: hookEvent,
|
|
342
|
+
input: parsedInput,
|
|
343
|
+
validated: true,
|
|
344
|
+
...(normalized.sessionId === undefined ? {} : { sessionId: normalized.sessionId }),
|
|
345
|
+
...(normalized.agentId === undefined ? {} : { agentId: normalized.agentId }),
|
|
346
|
+
...(phase === undefined ? {} : { phase })
|
|
347
|
+
}).catch(() => undefined);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
326
350
|
const gitRunner = dependencies.gitRunner ?? defaultGitRunner(root);
|
|
327
351
|
const processRunner = dependencies.processRunner ?? new NodeVerificationProcessRunner();
|
|
328
352
|
const stopVerification = shouldBuildStopVerification(harnessId, event, config, parsedInput)
|
|
@@ -343,7 +367,34 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
343
367
|
const completionGate = (harnessId === "agy" || harnessId === "claude") &&
|
|
344
368
|
config.features.completionGate.enabled
|
|
345
369
|
? dependencies.completionGate ?? {
|
|
346
|
-
handle: async (normalized) =>
|
|
370
|
+
handle: async (normalized) => {
|
|
371
|
+
if (normalized.event === "stop" && normalized.terminationReason === "model_stop" && normalized.fullyIdle === true && normalized.sessionId !== undefined) {
|
|
372
|
+
const record = await readWorktreeRecord(root);
|
|
373
|
+
if (record?.runId !== undefined && record.workerId !== undefined && record.workerGeneration !== undefined) {
|
|
374
|
+
const common = await gitRunner.run(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
375
|
+
if (common.exitCode === 0) {
|
|
376
|
+
const commonDir = Buffer.from(common.stdout).toString("utf8").trim();
|
|
377
|
+
const repository = new FileRunRepository(join(commonDir, "agent-ops", "runs"), commonDir);
|
|
378
|
+
const state = await repository.read(record.runId);
|
|
379
|
+
const worker = state?.workers.find(worker => worker.workerId === record.workerId);
|
|
380
|
+
if (worker !== undefined && worker.workerId !== state.coordinatorId && worker.stopIntent !== null) {
|
|
381
|
+
const head = await gitRunner.run(["rev-parse", "HEAD"]);
|
|
382
|
+
const tasks = new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root));
|
|
383
|
+
if (head.exitCode === 0 && Buffer.from(head.stdout).toString("utf8").trim() === worker.stopIntent.sourceCommit &&
|
|
384
|
+
await tasks.treeContract(worker.taskId) === worker.stopIntent.contractDigest) {
|
|
385
|
+
const handoff = await validateWorkerStopHandoff(repository, { root, sessionId: normalized.sessionId,
|
|
386
|
+
...(normalized.agentId === undefined ? {} : { agentId: normalized.agentId }), runId: record.runId,
|
|
387
|
+
workerId: record.workerId, generation: record.workerGeneration,
|
|
388
|
+
deliveryDigest: worker.stopIntent.deliveryDigest ?? undefined, contractDigest: worker.stopIntent.contractDigest ?? undefined });
|
|
389
|
+
if (handoff !== null)
|
|
390
|
+
return handoff;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return await completionGateFor(root, config, gitRunner, harnessId).handle(normalized);
|
|
397
|
+
}
|
|
347
398
|
}
|
|
348
399
|
: undefined;
|
|
349
400
|
const worktreeGuard = config.worktree?.mode === "auto"
|
|
@@ -353,7 +404,7 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
353
404
|
? { action: "continue", status: "UNKNOWN", code: "WORKTREE_GUARD_UNAVAILABLE" }
|
|
354
405
|
: await evaluateWorktreeWrite(mainRoot, write.paths, write.sessionId, dependencies.worktree === undefined
|
|
355
406
|
? undefined
|
|
356
|
-
: async (sessionId, agentId) => (await ensureSessionWorktree(dependencies.worktree, { cwd: mainRoot, sessionId, ...(agentId === undefined ? {} : { agentId }) })).path, write.agentId);
|
|
407
|
+
: async (sessionId, agentId) => (await ensureSessionWorktree(dependencies.worktree, { cwd: mainRoot, sessionId, ...(agentId === undefined ? {} : { agentId }) })).path, write.agentId, config.profiles.includes("run"));
|
|
357
408
|
}
|
|
358
409
|
: undefined;
|
|
359
410
|
const worktreeEnter = config.worktree?.mode === "auto"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Entry point used only by the generated project-local loop launchers.
|
|
3
3
|
import { runLoopProcess } from "./codex-loop-process.js";
|
|
4
|
+
import { observeOfficeSession } from "./office-entry.js";
|
|
4
5
|
process.exitCode = await runLoopProcess(process.argv.slice(2), {
|
|
5
6
|
stdin: process.stdin,
|
|
6
7
|
writeStdout: (value) => process.stdout.write(value),
|
|
7
8
|
writeStderr: (value) => process.stderr.write(value)
|
|
8
|
-
});
|
|
9
|
+
}, { office: observeOfficeSession });
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { collectOfficeInput } from "../../../runtime/src/office/collect.js";
|
|
6
|
+
import { claimOffice, createOfficeServer, ensureOffice, readLiveOffice, officeUrl, releaseOffice } from "../../../runtime/src/office/server.js";
|
|
7
|
+
import { buildOfficeSnapshot } from "../../../runtime/src/office/snapshot.js";
|
|
8
|
+
import { recordOfficeSession } from "../../../runtime/src/office/sessions.js";
|
|
9
|
+
import { resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
10
|
+
import { createLaunchdDescriptor, LaunchdController } from "../../../runtime/src/run/macOS.js";
|
|
11
|
+
import { ensurePrivateDirectory } from "../../../runtime/src/security/permissions.js";
|
|
12
|
+
import { worktreeDependencies } from "./parallel-deps.js";
|
|
13
|
+
import { normalizeShellHookEvent } from "../../../runtime/src/hooks/shell.js";
|
|
14
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
15
|
+
import { loadEffectiveConfig } from "./context.js";
|
|
16
|
+
const entry = fileURLToPath(import.meta.url);
|
|
17
|
+
/** Missing or invalid config keeps this Preview disabled. */
|
|
18
|
+
export async function officeEnabled(cwd) {
|
|
19
|
+
if (process.env.AGENT_OPS_DISABLE === "1")
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
return (await loadEffectiveConfig(cwd, "project")).config.features.office?.enabled === true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function requireOffice(cwd) {
|
|
29
|
+
if (!(await officeEnabled(cwd))) {
|
|
30
|
+
throw new AgentOpsError("OFFICE_PREVIEW_DISABLED", "Office (Preview) is disabled. Enable it with agent-ops update --office on.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function recordValue(input, key) {
|
|
34
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
35
|
+
return undefined;
|
|
36
|
+
return input[key];
|
|
37
|
+
}
|
|
38
|
+
function observationInput(input) {
|
|
39
|
+
return typeof input === "object" && input !== null && !Array.isArray(input);
|
|
40
|
+
}
|
|
41
|
+
function firstText(...values) {
|
|
42
|
+
return values.find((value) => typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\0\r\n]/u.test(value));
|
|
43
|
+
}
|
|
44
|
+
function sessionIdentity(options) {
|
|
45
|
+
const nested = recordValue(options.input, "input");
|
|
46
|
+
return firstText(options.sessionId, recordValue(options.input, "session_id"), recordValue(options.input, "sessionId"), recordValue(options.input, "conversationId"), recordValue(options.input, "thread_id"), recordValue(options.input, "threadId"), recordValue(nested, "sessionID"), recordValue(nested, "session_id"), process.env.AGENT_OPS_SESSION_ID, process.env.CODEX_THREAD_ID);
|
|
47
|
+
}
|
|
48
|
+
function officeEvent(event) {
|
|
49
|
+
return event === "SessionStart" ? "start" : event === "Stop" ? "stop" : "activity";
|
|
50
|
+
}
|
|
51
|
+
/** Phase hints come only from normalized native event kinds and exact agent-ops argv. */
|
|
52
|
+
export function officePhaseHint(event) {
|
|
53
|
+
if (event.event === "file-write" || event.event === "content")
|
|
54
|
+
return "implementing";
|
|
55
|
+
const commands = event.event === "command"
|
|
56
|
+
? [event]
|
|
57
|
+
: event.event === "command-batch"
|
|
58
|
+
? event.commands
|
|
59
|
+
: [];
|
|
60
|
+
let phase;
|
|
61
|
+
for (const command of commands) {
|
|
62
|
+
if (basename(command.command) !== "agent-ops")
|
|
63
|
+
continue;
|
|
64
|
+
const action = command.args[0];
|
|
65
|
+
const next = action === "verify"
|
|
66
|
+
? "verifying"
|
|
67
|
+
: action === "review"
|
|
68
|
+
? "reviewing"
|
|
69
|
+
: action === "worktree" && command.args[1] === "finish"
|
|
70
|
+
? "integrating"
|
|
71
|
+
: action === "task" && command.args[1] === "complete"
|
|
72
|
+
? "integrating"
|
|
73
|
+
: action === "task"
|
|
74
|
+
? "planning"
|
|
75
|
+
: undefined;
|
|
76
|
+
if (next === "integrating" || next === "reviewing" || next === "verifying")
|
|
77
|
+
phase = next;
|
|
78
|
+
else if (phase === undefined && next !== undefined)
|
|
79
|
+
phase = next;
|
|
80
|
+
}
|
|
81
|
+
return phase;
|
|
82
|
+
}
|
|
83
|
+
/** Loop adapters expose Bash and native edit events with the same safe shape. */
|
|
84
|
+
export function officeLoopPhaseHint(input, root) {
|
|
85
|
+
if (!observationInput(input))
|
|
86
|
+
return undefined;
|
|
87
|
+
const tool = input.tool_name;
|
|
88
|
+
if (tool === "Edit" || tool === "Write" || tool === "NotebookEdit" || tool === "FileWrite")
|
|
89
|
+
return "implementing";
|
|
90
|
+
if (tool !== "Bash")
|
|
91
|
+
return undefined;
|
|
92
|
+
const toolInput = recordValue(input, "tool_input");
|
|
93
|
+
const command = recordValue(toolInput, "command");
|
|
94
|
+
return typeof command === "string" ? officePhaseHint(normalizeShellHookEvent(command, root)) : undefined;
|
|
95
|
+
}
|
|
96
|
+
/** Records hook presence and starts Office at the production process boundary. */
|
|
97
|
+
export async function observeOfficeSession(options) {
|
|
98
|
+
if (process.env.AGENT_OPS_DISABLE === "1")
|
|
99
|
+
return;
|
|
100
|
+
if (options.validated !== true)
|
|
101
|
+
return;
|
|
102
|
+
// Host hooks with malformed or absent input are fail-open and must not
|
|
103
|
+
// create a server from an unrelated process cwd.
|
|
104
|
+
if (!observationInput(options.input))
|
|
105
|
+
return;
|
|
106
|
+
const identity = sessionIdentity(options);
|
|
107
|
+
if (identity === undefined)
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
if (!(await officeEnabled(options.root)))
|
|
111
|
+
return;
|
|
112
|
+
const { mainRoot, commonDir } = await checkouts(options.root);
|
|
113
|
+
if (identity !== undefined) {
|
|
114
|
+
await recordOfficeSession({
|
|
115
|
+
sessionId: identity,
|
|
116
|
+
harness: options.harness,
|
|
117
|
+
projectRoot: mainRoot,
|
|
118
|
+
commonDir,
|
|
119
|
+
event: officeEvent(options.event),
|
|
120
|
+
...(options.agentId === undefined ? {} : { agentId: options.agentId }),
|
|
121
|
+
...(process.env.AGENT_OPS_RUN_ID === undefined ? {} : { runId: process.env.AGENT_OPS_RUN_ID }),
|
|
122
|
+
...(process.env.AGENT_OPS_WORKER_ID === undefined ? {} : { workerId: process.env.AGENT_OPS_WORKER_ID }),
|
|
123
|
+
...(process.env.AGENT_OPS_TASK_ID === undefined ? {} : { taskId: process.env.AGENT_OPS_TASK_ID }),
|
|
124
|
+
...(options.phase === undefined ? {} : { phase: options.phase }),
|
|
125
|
+
host: options.harness
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// A resumed session may outlive the ten-minute idle shutdown. Reusing the
|
|
129
|
+
// same ensure path revives that repository server without creating a tab
|
|
130
|
+
// unless this call actually wins a new-server start claim.
|
|
131
|
+
if (options.event !== "Stop")
|
|
132
|
+
await ensureBackgroundOffice(options.root, undefined, openOfficeBrowser);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Office is display-only; a missing Git checkout, lock or launcher never blocks a host hook.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function checkouts(cwd) {
|
|
139
|
+
return await resolveCheckouts(worktreeDependencies(), cwd);
|
|
140
|
+
}
|
|
141
|
+
/** Serve the office until no run has been active for the idle window. */
|
|
142
|
+
export async function serveOffice(cwd, onUrl = () => { }, openBrowser) {
|
|
143
|
+
await requireOffice(cwd);
|
|
144
|
+
const { mainRoot, commonDir } = await checkouts(cwd);
|
|
145
|
+
// ponytail: one shared 1s snapshot so several polling tabs cost one git scan
|
|
146
|
+
let cached = null;
|
|
147
|
+
const snapshot = () => {
|
|
148
|
+
if (cached === null || Date.now() - cached.at > 1000)
|
|
149
|
+
cached = { at: Date.now(), value: collectOfficeInput(mainRoot, commonDir).then(buildOfficeSnapshot) };
|
|
150
|
+
return cached.value;
|
|
151
|
+
};
|
|
152
|
+
let finish = () => { };
|
|
153
|
+
const idle = new Promise((resolve) => { finish = resolve; });
|
|
154
|
+
const office = createOfficeServer({ snapshot, onIdle: () => finish() });
|
|
155
|
+
const record = await claimOffice(commonDir, office);
|
|
156
|
+
if (record === null) {
|
|
157
|
+
const live = await readLiveOffice(commonDir);
|
|
158
|
+
if (live !== null)
|
|
159
|
+
onUrl(officeUrl(live));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
onUrl(officeUrl(record));
|
|
163
|
+
if (openBrowser !== undefined)
|
|
164
|
+
void openBrowser(officeUrl(record)).catch(() => undefined);
|
|
165
|
+
const timer = setInterval(() => {
|
|
166
|
+
void officeEnabled(cwd).then(enabled => enabled ? office.tick() : finish());
|
|
167
|
+
}, 15_000);
|
|
168
|
+
await idle;
|
|
169
|
+
clearInterval(timer);
|
|
170
|
+
await releaseOffice(commonDir, record);
|
|
171
|
+
await office.close();
|
|
172
|
+
}
|
|
173
|
+
/** Reuse the live server or start one under launchd; null where launchd is unavailable. */
|
|
174
|
+
export async function ensureBackgroundOffice(cwd, launchd = new LaunchdController(), openBrowser) {
|
|
175
|
+
await requireOffice(cwd);
|
|
176
|
+
const { mainRoot, commonDir } = await checkouts(cwd);
|
|
177
|
+
const live = await readLiveOffice(commonDir);
|
|
178
|
+
if (live !== null)
|
|
179
|
+
return officeUrl(live);
|
|
180
|
+
let started = false;
|
|
181
|
+
const args = [entry, mainRoot];
|
|
182
|
+
if (!launchd.supported) {
|
|
183
|
+
const url = await ensureOffice(commonDir, async () => {
|
|
184
|
+
started = true;
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
const child = spawn(process.execPath, args, { cwd: mainRoot, detached: true, stdio: "ignore", windowsHide: true });
|
|
187
|
+
child.once("error", reject);
|
|
188
|
+
child.once("spawn", () => { child.unref(); resolve(); });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
if (started && openBrowser !== undefined)
|
|
192
|
+
void openBrowser(url).catch(() => undefined);
|
|
193
|
+
return url;
|
|
194
|
+
}
|
|
195
|
+
const privateDirectory = join(commonDir, "agent-ops", "office");
|
|
196
|
+
const descriptor = createLaunchdDescriptor({
|
|
197
|
+
runId: "office-" + createHash("sha256").update(commonDir).digest("hex").slice(0, 12), workerId: "server",
|
|
198
|
+
privateDirectory, command: process.execPath, args,
|
|
199
|
+
cwd: mainRoot, pathEnvironment: process.env.PATH
|
|
200
|
+
});
|
|
201
|
+
const url = await ensureOffice(commonDir, async () => {
|
|
202
|
+
started = true;
|
|
203
|
+
// The descriptor write anchors on this directory, so it must exist first.
|
|
204
|
+
await ensurePrivateDirectory(privateDirectory, commonDir);
|
|
205
|
+
await launchd.writeDescriptor(descriptor);
|
|
206
|
+
// A server that exited idle stays loaded in launchd; reload it with the current descriptor.
|
|
207
|
+
await launchd.bootout(descriptor).catch(() => { });
|
|
208
|
+
await launchd.bootstrap(descriptor);
|
|
209
|
+
});
|
|
210
|
+
if (started && openBrowser !== undefined)
|
|
211
|
+
void openBrowser(url).catch(() => undefined);
|
|
212
|
+
return url;
|
|
213
|
+
}
|
|
214
|
+
export function openOfficeBrowser(url) {
|
|
215
|
+
const command = process.platform === "darwin"
|
|
216
|
+
? "open"
|
|
217
|
+
: process.platform === "win32"
|
|
218
|
+
? "rundll32.exe"
|
|
219
|
+
: "xdg-open";
|
|
220
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
223
|
+
child.once("error", reject);
|
|
224
|
+
child.once("spawn", () => { child.unref(); resolve(); });
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (process.argv[1] === entry) {
|
|
228
|
+
await serveOffice(process.argv[2] ?? process.cwd(), () => { }, process.argv[3] === "--open" ? openOfficeBrowser : undefined);
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|