@phnx-labs/agents-cli 1.22.53 → 1.22.55
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/CHANGELOG.md +208 -0
- package/README.md +59 -9
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +219 -173
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +10 -8
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +2 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +7 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/routines.js +31 -2
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +64 -20
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +79 -3
- package/dist/commands/share.js +347 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +18 -2
- package/dist/commands/status.js +14 -0
- package/dist/commands/view.d.ts +11 -1
- package/dist/commands/view.js +35 -7
- package/dist/lib/account-registry.js +15 -3
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +68 -1
- package/dist/lib/accounting/usage.js +116 -10
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/cdp.d.ts +1 -1
- package/dist/lib/browser/cdp.js +1 -1
- package/dist/lib/browser/ffmpeg.d.ts +12 -0
- package/dist/lib/browser/ffmpeg.js +184 -0
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/browser/service.js +119 -25
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
- package/dist/lib/daemon/browser-task-reap-service.js +26 -0
- package/dist/lib/daemon/daemon.js +87 -176
- package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
- package/dist/lib/daemon/heartbeat-service.js +26 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
- package/dist/lib/daemon/monitor-engine-service.js +15 -7
- package/dist/lib/daemon/runner.d.ts +15 -0
- package/dist/lib/daemon/runner.js +23 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
- package/dist/lib/daemon/secrets-broker-service.js +17 -32
- package/dist/lib/daemon/service.d.ts +2 -2
- package/dist/lib/daemon/service.js +1 -1
- package/dist/lib/daemon/session-state-service.d.ts +21 -0
- package/dist/lib/daemon/session-state-service.js +34 -0
- package/dist/lib/daemon/supervisor.d.ts +17 -7
- package/dist/lib/daemon/supervisor.js +87 -14
- package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
- package/dist/lib/daemon/tmux-reap-service.js +28 -0
- package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
- package/dist/lib/daemon/webhook-receiver-service.js +17 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +25 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +5 -5
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/harness-inventory.js +5 -2
- package/dist/lib/exec.d.ts +28 -0
- package/dist/lib/exec.js +73 -7
- package/dist/lib/feed/feed.d.ts +1 -1
- package/dist/lib/feed/feed.js +23 -1
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.d.ts +5 -1
- package/dist/lib/monitors/engine.js +13 -4
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/scheduling/routines.d.ts +47 -0
- package/dist/lib/scheduling/routines.js +70 -1
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.js +1 -4
- package/dist/lib/session/db.d.ts +41 -5
- package/dist/lib/session/db.js +132 -30
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +119 -25
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/share/capture.js +11 -2
- package/dist/lib/share/publish.d.ts +56 -5
- package/dist/lib/share/publish.js +126 -18
- package/dist/lib/share/worker-template.d.ts +3 -12
- package/dist/lib/share/worker-template.js +860 -59
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +16 -0
- package/dist/lib/state.js +178 -46
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +7 -0
- package/package.json +9 -2
package/dist/lib/help.js
CHANGED
|
@@ -7,6 +7,27 @@ const commandGroupRegistry = new WeakMap();
|
|
|
7
7
|
export function registerCommandGroups(parent, groups) {
|
|
8
8
|
commandGroupRegistry.set(parent, groups);
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Front-door command groups shown on `agents --help`. Derived from measured
|
|
12
|
+
* reach: tier 1 = setup/run/sessions/view, tier 2 = teams/browser/secrets/
|
|
13
|
+
* devices/accounts/add. The remaining groups stay discoverable through the
|
|
14
|
+
* pointer rendered below these groups.
|
|
15
|
+
*/
|
|
16
|
+
export const FRONT_DOOR_COMMAND_GROUPS = [
|
|
17
|
+
{
|
|
18
|
+
title: 'Quick start',
|
|
19
|
+
names: ['setup', 'view', 'run', 'sessions'],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: 'Most-used',
|
|
23
|
+
names: ['teams', 'browser', 'secrets', 'devices', 'accounts', 'add'],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const compactRootHelp = new WeakMap();
|
|
27
|
+
/** Mark the root program so its help only renders front-door groups + a pointer. */
|
|
28
|
+
export function setCompactRootHelp(program) {
|
|
29
|
+
compactRootHelp.set(program, true);
|
|
30
|
+
}
|
|
10
31
|
const helpSectionRegistry = new WeakMap();
|
|
11
32
|
/**
|
|
12
33
|
* Attach an Examples block (rendered between the description and Arguments)
|
|
@@ -136,7 +157,14 @@ function formatHelpCommandsFirst(cmd, helper) {
|
|
|
136
157
|
output = output.concat([`${title}:`, formatList(subs.map(renderCommand)), '']);
|
|
137
158
|
}
|
|
138
159
|
const remaining = visibleCommands.filter((s) => !placed.has(s.name()));
|
|
139
|
-
if (
|
|
160
|
+
if (compactRootHelp.get(cmd)) {
|
|
161
|
+
output = output.concat([
|
|
162
|
+
'Commands:',
|
|
163
|
+
` See "${cmd.name()} --help-all" for every command.`,
|
|
164
|
+
'',
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
else if (remaining.length > 0) {
|
|
140
168
|
output = output.concat(['Commands:', formatList(remaining.map(renderCommand)), '']);
|
|
141
169
|
}
|
|
142
170
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* "not supported" message — never commander's raw `unknown option`.
|
|
18
18
|
*/
|
|
19
19
|
import { type Host } from './types.js';
|
|
20
|
-
import { type
|
|
20
|
+
import { type DeviceRegistry } from '../devices/registry.js';
|
|
21
21
|
import { runLocalCommand, runOnDevice } from '../devices/fleet.js';
|
|
22
22
|
/** Re-export for callers that historically imported flagValue from this module. */
|
|
23
23
|
export { flagValue, hasHostRoutingFlag } from './routing-flag.js';
|
|
@@ -69,15 +69,6 @@ export interface FleetPassthroughOptions {
|
|
|
69
69
|
/** Override this machine's id (tests). Defaults to `machineId()`. */
|
|
70
70
|
self?: string;
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Prefix a fan-out remote command so the far side sees AGENTS_FLEET_REMOTE=1 —
|
|
74
|
-
* the same marker the single-target dispatch sets via env. `wrapRemoteCommand`
|
|
75
|
-
* joins the argv with spaces (POSIX) or base64-encodes it for PowerShell, so a
|
|
76
|
-
* shell-appropriate leading token rides through both: `env VAR=1 …` on POSIX,
|
|
77
|
-
* `$env:VAR='1'; …` on PowerShell. Only remote (non-self) targets get it; the
|
|
78
|
-
* self target runs locally and must stay ungated.
|
|
79
|
-
*/
|
|
80
|
-
export declare function markFleetRemote(cmd: string[], device: DeviceProfile): string[];
|
|
81
72
|
/** Run `agents <command> …` across every registered device and render the roster. */
|
|
82
73
|
export declare function runFleetPassthrough(command: string, allArgs: string[], spec: RemoteSpec, opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
83
74
|
/**
|
|
@@ -32,6 +32,7 @@ import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, }
|
|
|
32
32
|
import { flagValue, hasHostRoutingFlag } from './routing-flag.js';
|
|
33
33
|
import { loadDevices } from '../devices/registry.js';
|
|
34
34
|
import { isSelfHost } from '../devices/self-host.js';
|
|
35
|
+
import { markFleetRemote } from '../devices/connect.js';
|
|
35
36
|
import { fanOutDevices, planFleetTargets, runLocalCommand, runOnDevice, } from '../devices/fleet.js';
|
|
36
37
|
import { platformGroupLabel } from '../devices/health-report.js';
|
|
37
38
|
import { isKnownTopLevelCommand } from '../startup/command-registry.js';
|
|
@@ -368,19 +369,6 @@ function renderFleetRoster(command, forwarded, results, self) {
|
|
|
368
369
|
console.log(chalk.gray(summaryParts.join(' · ')));
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
|
-
/**
|
|
372
|
-
* Prefix a fan-out remote command so the far side sees AGENTS_FLEET_REMOTE=1 —
|
|
373
|
-
* the same marker the single-target dispatch sets via env. `wrapRemoteCommand`
|
|
374
|
-
* joins the argv with spaces (POSIX) or base64-encodes it for PowerShell, so a
|
|
375
|
-
* shell-appropriate leading token rides through both: `env VAR=1 …` on POSIX,
|
|
376
|
-
* `$env:VAR='1'; …` on PowerShell. Only remote (non-self) targets get it; the
|
|
377
|
-
* self target runs locally and must stay ungated.
|
|
378
|
-
*/
|
|
379
|
-
export function markFleetRemote(cmd, device) {
|
|
380
|
-
return device.shell === 'powershell'
|
|
381
|
-
? [`$env:AGENTS_FLEET_REMOTE='1';`, ...cmd]
|
|
382
|
-
: ['env', 'AGENTS_FLEET_REMOTE=1', ...cmd];
|
|
383
|
-
}
|
|
384
372
|
/** Run `agents <command> …` across every registered device and render the roster. */
|
|
385
373
|
export async function runFleetPassthrough(command, allArgs, spec, opts = {}) {
|
|
386
374
|
const self = opts.self ?? machineId();
|
|
@@ -49,7 +49,7 @@ import { emit } from '../feed/events.js';
|
|
|
49
49
|
import { safeJoin } from '../paths.js';
|
|
50
50
|
import { readSkillSourceCommandMarker, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill, } from '../command-skills.js';
|
|
51
51
|
import { getWriter, getDetector } from '../staleness/registry.js';
|
|
52
|
-
import { syncMemoryToVersionHome } from '../memory.js';
|
|
52
|
+
import { syncMemoryToVersionHome, syncClaudeProjectMemoryDir } from '../memory.js';
|
|
53
53
|
import { listPluginSkillNames, resolveCommandSource, resolveSkillSource } from '../staleness/writers/sources.js';
|
|
54
54
|
import { syncProjectResourcesToAgent } from '../project-resources.js';
|
|
55
55
|
import { installClaudeStatusLine } from '../claude-statusline.js';
|
|
@@ -2860,6 +2860,14 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2860
2860
|
if (supports(agent, 'memory', version).ok) {
|
|
2861
2861
|
syncMemoryToVersionHome(agent, versionHome, cwd);
|
|
2862
2862
|
}
|
|
2863
|
+
// Claude Code's own NATIVE per-project auto-memory (.claude/projects/<key>/memory/,
|
|
2864
|
+
// PHNX-2817) is a separate, unmanaged directory Claude writes into itself — make it
|
|
2865
|
+
// version-independent the same way project-level rules already are, via a shared
|
|
2866
|
+
// symlink, so a note survives an agent version upgrade instead of vanishing into a
|
|
2867
|
+
// fresh, empty version home.
|
|
2868
|
+
if (agent === 'claude') {
|
|
2869
|
+
syncClaudeProjectMemoryDir(versionHome, cwd);
|
|
2870
|
+
}
|
|
2863
2871
|
// Prune resources deleted from source (RUSH-2438). Runs only on a repo-scope
|
|
2864
2872
|
// reconcile (`options.prune`) with a caller selection — a full sync
|
|
2865
2873
|
// (`!userPassedSelection`) already sweeps orphans above, and an additive
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Path of the AppArmor knob that gates unprivileged userns on Ubuntu 23.10+. */
|
|
2
|
+
export declare const APPARMOR_USERNS_SYSCTL_PATH = "/proc/sys/kernel/apparmor_restrict_unprivileged_userns";
|
|
3
|
+
export type UsernsState =
|
|
4
|
+
/** A new user namespace with a uid map can be created — Codex's sandbox works. */
|
|
5
|
+
'ok'
|
|
6
|
+
/** Unprivileged userns is restricted — Codex's bwrap sandbox cannot start. */
|
|
7
|
+
| 'blocked'
|
|
8
|
+
/** Could not determine (non-Linux, or the probe could not run). */
|
|
9
|
+
| 'unknown';
|
|
10
|
+
export interface UsernsStatus {
|
|
11
|
+
state: UsernsState;
|
|
12
|
+
/** One-line human reason, present when `blocked` or `unknown`. */
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
/** The raw signals the pure interpreter reasons over. */
|
|
16
|
+
export interface UsernsInputs {
|
|
17
|
+
platform: NodeJS.Platform;
|
|
18
|
+
/**
|
|
19
|
+
* Contents of {@link APPARMOR_USERNS_SYSCTL_PATH} trimmed, or null when the
|
|
20
|
+
* file is absent (older kernels / no AppArmor userns mediation).
|
|
21
|
+
*/
|
|
22
|
+
apparmorRestrict: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Result of actually attempting to create a user namespace with a uid map:
|
|
25
|
+
* - 'ok' → the probe created the namespace and mapped root.
|
|
26
|
+
* - 'denied' → the kernel refused the uid_map write (the restricted case).
|
|
27
|
+
* - 'no-tool' → the probe binary (`unshare`) was missing or failed to spawn.
|
|
28
|
+
*/
|
|
29
|
+
unshareProbe: 'ok' | 'denied' | 'no-tool';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Decide userns availability from raw signals. Pure — no I/O.
|
|
33
|
+
*
|
|
34
|
+
* The definitive signal is the actual probe: if we successfully created a userns
|
|
35
|
+
* and wrote a uid map, the sandbox works regardless of the sysctl (an AppArmor
|
|
36
|
+
* profile may grant a specific binary `userns` even while the global knob is 1).
|
|
37
|
+
* A denied probe is a hard `blocked`. When the probe tool is missing we fall back
|
|
38
|
+
* to the sysctl: `1` → `blocked`, `0`/absent → `unknown` (we could not prove it,
|
|
39
|
+
* and refuse to claim `ok` we did not observe).
|
|
40
|
+
*/
|
|
41
|
+
export declare function interpretUsernsInputs(inputs: UsernsInputs): UsernsStatus;
|
|
42
|
+
/** Read the AppArmor userns sysctl, or null when the file is absent. */
|
|
43
|
+
export declare function readApparmorRestrict(sysctlPath?: string): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Actually try to create a user namespace and map root inside it — the same
|
|
46
|
+
* operation bwrap performs (`unshare --user --map-root-user`). This is the ground
|
|
47
|
+
* truth: it observes exactly what the kernel/AppArmor policy permits for *this*
|
|
48
|
+
* process, rather than inferring from the sysctl alone.
|
|
49
|
+
*/
|
|
50
|
+
export declare function probeUnshare(): 'ok' | 'denied' | 'no-tool';
|
|
51
|
+
/**
|
|
52
|
+
* Resolve whether an unprivileged user namespace can be created on this host,
|
|
53
|
+
* cached for the process (the answer is a stable property of the box). Non-Linux
|
|
54
|
+
* short-circuits to `ok` without spawning anything.
|
|
55
|
+
*/
|
|
56
|
+
export declare function probeUnprivilegedUserns(platform?: NodeJS.Platform): UsernsStatus;
|
|
57
|
+
/** Test-only: drop the process cache so a test can re-probe. */
|
|
58
|
+
export declare function resetUsernsCacheForTests(): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unprivileged user-namespace availability on Linux — the capability Codex's
|
|
3
|
+
* Linux sandbox needs, and the one Ubuntu 23.10+ restricts by default.
|
|
4
|
+
*
|
|
5
|
+
* Codex ≥0.146 implements its `read-only` and `workspace-write` sandbox modes on
|
|
6
|
+
* Linux with a bundled **bubblewrap** (`bwrap`), extracted per-run to
|
|
7
|
+
* `$CODEX_HOME/tmp/arg0/codex-XXXX/` and exec'd from a memfd. bwrap sets up its
|
|
8
|
+
* mounts inside a fresh **unprivileged user namespace** (`--unshare-user`, then a
|
|
9
|
+
* write to `/proc/self/uid_map`). Ubuntu 24.04 ships
|
|
10
|
+
* `kernel.apparmor_restrict_unprivileged_userns=1`, which denies that to an
|
|
11
|
+
* unconfined binary — so bwrap dies with `bwrap: setting up uid map: Permission
|
|
12
|
+
* denied` and a headless Codex run lands zero tools (no file writes, no shell).
|
|
13
|
+
* `danger-full-access` (our `skip` mode) drops the sandbox and is the only mode
|
|
14
|
+
* that avoids bwrap; the legacy Landlock backend is gone (`use_linux_sandbox_bwrap`
|
|
15
|
+
* is `removed`, `use_legacy_landlock` panics under the permission-profile model).
|
|
16
|
+
*
|
|
17
|
+
* This module is the single detector. It is pure at its core
|
|
18
|
+
* ({@link interpretUsernsInputs}) so the decision is unit-testable without a
|
|
19
|
+
* shell, and {@link probeUnprivilegedUserns} gathers the real inputs once per
|
|
20
|
+
* process. See PHNX-3285.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'child_process';
|
|
23
|
+
import * as fs from 'fs';
|
|
24
|
+
/** Path of the AppArmor knob that gates unprivileged userns on Ubuntu 23.10+. */
|
|
25
|
+
export const APPARMOR_USERNS_SYSCTL_PATH = '/proc/sys/kernel/apparmor_restrict_unprivileged_userns';
|
|
26
|
+
/**
|
|
27
|
+
* Decide userns availability from raw signals. Pure — no I/O.
|
|
28
|
+
*
|
|
29
|
+
* The definitive signal is the actual probe: if we successfully created a userns
|
|
30
|
+
* and wrote a uid map, the sandbox works regardless of the sysctl (an AppArmor
|
|
31
|
+
* profile may grant a specific binary `userns` even while the global knob is 1).
|
|
32
|
+
* A denied probe is a hard `blocked`. When the probe tool is missing we fall back
|
|
33
|
+
* to the sysctl: `1` → `blocked`, `0`/absent → `unknown` (we could not prove it,
|
|
34
|
+
* and refuse to claim `ok` we did not observe).
|
|
35
|
+
*/
|
|
36
|
+
export function interpretUsernsInputs(inputs) {
|
|
37
|
+
if (inputs.platform !== 'linux')
|
|
38
|
+
return { state: 'ok' };
|
|
39
|
+
if (inputs.unshareProbe === 'ok')
|
|
40
|
+
return { state: 'ok' };
|
|
41
|
+
if (inputs.unshareProbe === 'denied') {
|
|
42
|
+
const via = inputs.apparmorRestrict === '1'
|
|
43
|
+
? ' (kernel.apparmor_restrict_unprivileged_userns=1)'
|
|
44
|
+
: '';
|
|
45
|
+
return {
|
|
46
|
+
state: 'blocked',
|
|
47
|
+
reason: `the kernel denied creating an unprivileged user namespace${via}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Probe tool unavailable — lean on the AppArmor knob.
|
|
51
|
+
if (inputs.apparmorRestrict === '1') {
|
|
52
|
+
return {
|
|
53
|
+
state: 'blocked',
|
|
54
|
+
reason: 'unprivileged user namespaces are AppArmor-restricted ' +
|
|
55
|
+
'(kernel.apparmor_restrict_unprivileged_userns=1) and `unshare` was not available to confirm',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
state: 'unknown',
|
|
60
|
+
reason: '`unshare` was not available to probe user-namespace support',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Read the AppArmor userns sysctl, or null when the file is absent. */
|
|
64
|
+
export function readApparmorRestrict(sysctlPath = APPARMOR_USERNS_SYSCTL_PATH) {
|
|
65
|
+
try {
|
|
66
|
+
return fs.readFileSync(sysctlPath, 'utf8').trim();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Actually try to create a user namespace and map root inside it — the same
|
|
74
|
+
* operation bwrap performs (`unshare --user --map-root-user`). This is the ground
|
|
75
|
+
* truth: it observes exactly what the kernel/AppArmor policy permits for *this*
|
|
76
|
+
* process, rather than inferring from the sysctl alone.
|
|
77
|
+
*/
|
|
78
|
+
export function probeUnshare() {
|
|
79
|
+
try {
|
|
80
|
+
execFileSync('unshare', ['--user', '--map-root-user', 'true'], {
|
|
81
|
+
stdio: 'ignore',
|
|
82
|
+
timeout: 5000,
|
|
83
|
+
});
|
|
84
|
+
return 'ok';
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// ENOENT / spawn failure → the tool isn't here; anything else (nonzero exit
|
|
88
|
+
// from the denied uid_map write) is the restricted case.
|
|
89
|
+
const code = err?.code;
|
|
90
|
+
if (code === 'ENOENT')
|
|
91
|
+
return 'no-tool';
|
|
92
|
+
return 'denied';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let cached = null;
|
|
96
|
+
/**
|
|
97
|
+
* Resolve whether an unprivileged user namespace can be created on this host,
|
|
98
|
+
* cached for the process (the answer is a stable property of the box). Non-Linux
|
|
99
|
+
* short-circuits to `ok` without spawning anything.
|
|
100
|
+
*/
|
|
101
|
+
export function probeUnprivilegedUserns(platform = process.platform) {
|
|
102
|
+
if (platform !== 'linux')
|
|
103
|
+
return { state: 'ok' };
|
|
104
|
+
if (cached)
|
|
105
|
+
return cached;
|
|
106
|
+
cached = interpretUsernsInputs({
|
|
107
|
+
platform,
|
|
108
|
+
apparmorRestrict: readApparmorRestrict(),
|
|
109
|
+
unshareProbe: probeUnshare(),
|
|
110
|
+
});
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
/** Test-only: drop the process cache so a test can re-probe. */
|
|
114
|
+
export function resetUsernsCacheForTests() {
|
|
115
|
+
cached = null;
|
|
116
|
+
}
|
package/dist/lib/memory.d.ts
CHANGED
|
@@ -53,3 +53,29 @@ export declare function rebuildMemoryIndex(dir: string): void;
|
|
|
53
53
|
export declare function memoryTargetDir(agent: AgentId): string;
|
|
54
54
|
/** Copy canonical layered memory into one version home. Returns fact names written. */
|
|
55
55
|
export declare function syncMemoryToVersionHome(agent: AgentId, versionHome: string, cwd?: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Canonical shared dir for Claude Code's NATIVE per-project auto-memory —
|
|
58
|
+
* `<versionHome>/.claude/projects/<project-key>/memory/*.md`, the freeform
|
|
59
|
+
* notes Claude writes for itself during a session. Distinct from the layered
|
|
60
|
+
* `memory` resource above (~/.agents/memory/ facts synced into
|
|
61
|
+
* `.claude/memory/`): this dir is keyed by project (via
|
|
62
|
+
* {@link claudeProjectDirName}), not by agent version, and Claude Code itself
|
|
63
|
+
* decides what goes in it — agents-cli only makes the directory
|
|
64
|
+
* version-independent, never writes into it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function getClaudeProjectMemoryDir(cwd: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Make Claude Code's native per-project memory dir version-independent by
|
|
69
|
+
* symlinking `<versionHome>/.claude/projects/<project-key>/memory/` into the
|
|
70
|
+
* one canonical dir every installed Claude version's home shares for this
|
|
71
|
+
* project (PHNX-2817). Without this, `getVersionHomePath` gives every
|
|
72
|
+
* installed version its own isolated HOME, so a note written under one
|
|
73
|
+
* version is invisible under another — the directory is just empty there.
|
|
74
|
+
*
|
|
75
|
+
* Idempotent and safe to call on every sync: a dir already linked to the
|
|
76
|
+
* canonical target is left alone; a PRE-EXISTING real directory with content
|
|
77
|
+
* (the common case today, since this bug has always left one behind) has its
|
|
78
|
+
* files migrated into the canonical dir first — never discarded — before
|
|
79
|
+
* being replaced by the symlink.
|
|
80
|
+
*/
|
|
81
|
+
export declare function syncClaudeProjectMemoryDir(versionHome: string, cwd?: string): void;
|
package/dist/lib/memory.js
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import { getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, ensureAgentsDir, } from './state.js';
|
|
16
|
+
import { getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, ensureAgentsDir, getRuntimeStateDir, } from './state.js';
|
|
17
17
|
import { agentConfigDirName } from './agents.js';
|
|
18
18
|
import { supports } from './capabilities.js';
|
|
19
|
+
import { claudeProjectDirName } from './project-key.js';
|
|
19
20
|
/** User-layer memory root (~/.agents/memory/). */
|
|
20
21
|
export function getUserMemoryDir() {
|
|
21
22
|
return path.join(getUserAgentsDir(), 'memory');
|
|
@@ -273,3 +274,81 @@ export function syncMemoryToVersionHome(agent, versionHome, cwd = process.cwd())
|
|
|
273
274
|
catch { /* best-effort */ }
|
|
274
275
|
return written;
|
|
275
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Canonical shared dir for Claude Code's NATIVE per-project auto-memory —
|
|
279
|
+
* `<versionHome>/.claude/projects/<project-key>/memory/*.md`, the freeform
|
|
280
|
+
* notes Claude writes for itself during a session. Distinct from the layered
|
|
281
|
+
* `memory` resource above (~/.agents/memory/ facts synced into
|
|
282
|
+
* `.claude/memory/`): this dir is keyed by project (via
|
|
283
|
+
* {@link claudeProjectDirName}), not by agent version, and Claude Code itself
|
|
284
|
+
* decides what goes in it — agents-cli only makes the directory
|
|
285
|
+
* version-independent, never writes into it.
|
|
286
|
+
*/
|
|
287
|
+
export function getClaudeProjectMemoryDir(cwd) {
|
|
288
|
+
const projectKey = claudeProjectDirName(path.resolve(cwd));
|
|
289
|
+
return path.join(getRuntimeStateDir(), 'claude-project-memory', projectKey);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Make Claude Code's native per-project memory dir version-independent by
|
|
293
|
+
* symlinking `<versionHome>/.claude/projects/<project-key>/memory/` into the
|
|
294
|
+
* one canonical dir every installed Claude version's home shares for this
|
|
295
|
+
* project (PHNX-2817). Without this, `getVersionHomePath` gives every
|
|
296
|
+
* installed version its own isolated HOME, so a note written under one
|
|
297
|
+
* version is invisible under another — the directory is just empty there.
|
|
298
|
+
*
|
|
299
|
+
* Idempotent and safe to call on every sync: a dir already linked to the
|
|
300
|
+
* canonical target is left alone; a PRE-EXISTING real directory with content
|
|
301
|
+
* (the common case today, since this bug has always left one behind) has its
|
|
302
|
+
* files migrated into the canonical dir first — never discarded — before
|
|
303
|
+
* being replaced by the symlink.
|
|
304
|
+
*/
|
|
305
|
+
export function syncClaudeProjectMemoryDir(versionHome, cwd = process.cwd()) {
|
|
306
|
+
const projectKey = claudeProjectDirName(path.resolve(cwd));
|
|
307
|
+
const canonicalDir = path.join(getRuntimeStateDir(), 'claude-project-memory', projectKey);
|
|
308
|
+
const projectDir = path.join(versionHome, agentConfigDirName('claude'), 'projects', projectKey);
|
|
309
|
+
const nativeMemoryDir = path.join(projectDir, 'memory');
|
|
310
|
+
fs.mkdirSync(canonicalDir, { recursive: true, mode: 0o700 });
|
|
311
|
+
let existing;
|
|
312
|
+
try {
|
|
313
|
+
existing = fs.lstatSync(nativeMemoryDir);
|
|
314
|
+
}
|
|
315
|
+
catch { /* nothing there yet */ }
|
|
316
|
+
if (existing?.isSymbolicLink()) {
|
|
317
|
+
let currentTarget;
|
|
318
|
+
try {
|
|
319
|
+
currentTarget = fs.readlinkSync(nativeMemoryDir);
|
|
320
|
+
}
|
|
321
|
+
catch { /* dangling link */ }
|
|
322
|
+
if (currentTarget === canonicalDir)
|
|
323
|
+
return; // already wired correctly
|
|
324
|
+
fs.unlinkSync(nativeMemoryDir); // stale/foreign link — replace below
|
|
325
|
+
}
|
|
326
|
+
else if (existing?.isDirectory()) {
|
|
327
|
+
// Migrate first (never clobber content already promoted to canonical by
|
|
328
|
+
// an earlier-synced version home), then remove the now-redundant copy.
|
|
329
|
+
fs.cpSync(nativeMemoryDir, canonicalDir, { recursive: true, force: false, errorOnExist: false });
|
|
330
|
+
fs.rmSync(nativeMemoryDir, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
else if (existing) {
|
|
333
|
+
return; // an unexpected file at this path — leave it alone rather than destroy it
|
|
334
|
+
}
|
|
335
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
336
|
+
try {
|
|
337
|
+
fs.symlinkSync(canonicalDir, nativeMemoryDir, process.platform === 'win32' ? 'junction' : undefined);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
// A concurrent sync (e.g. two `agents run claude` launches racing on a
|
|
341
|
+
// first-ever project) can win this exact link between our lstat above
|
|
342
|
+
// and this call. If it landed the same canonical target, that's the
|
|
343
|
+
// outcome we wanted — treat it as success rather than throwing.
|
|
344
|
+
if (err?.code !== 'EEXIST')
|
|
345
|
+
throw err;
|
|
346
|
+
let racedTarget;
|
|
347
|
+
try {
|
|
348
|
+
racedTarget = fs.readlinkSync(nativeMemoryDir);
|
|
349
|
+
}
|
|
350
|
+
catch { /* not even a symlink — fall through to rethrow */ }
|
|
351
|
+
if (racedTarget !== canonicalDir)
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -84,6 +84,17 @@ export interface ActionConfig {
|
|
|
84
84
|
notifyChannel?: string;
|
|
85
85
|
/** webhook-out: URL to POST the event to. */
|
|
86
86
|
url?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Shell command that must exit 0 after a `run`/`routine` action settles.
|
|
89
|
+
* Asserts the stated effect actually happened (PHNX-2842) — e.g.
|
|
90
|
+
* `gh pr view 1682 --json state --jq .state | grep -qx MERGED`. `{event}` is
|
|
91
|
+
* replaced with the fired event summary, same as the prompt. Evaluated once
|
|
92
|
+
* the dispatched run is no longer `running`; a failed check makes the fire
|
|
93
|
+
* `ok: false` with effect `none` (`postcondition not met`), not a healthy
|
|
94
|
+
* `completed`. Notify/webhook-out already have a synchronous ok and refuse
|
|
95
|
+
* this field.
|
|
96
|
+
*/
|
|
97
|
+
postcondition?: string;
|
|
87
98
|
}
|
|
88
99
|
/** Full monitor configuration (persisted as YAML in ~/.agents/monitors/). */
|
|
89
100
|
export interface MonitorConfig {
|
|
@@ -238,6 +238,14 @@ export function validateMonitor(config) {
|
|
|
238
238
|
errors.push(`action.url must be an absolute URL (got ${JSON.stringify(action.url)})`);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
if (action.postcondition !== undefined) {
|
|
242
|
+
if (action.type !== 'run' && action.type !== 'routine') {
|
|
243
|
+
errors.push("action.postcondition only applies to run or routine actions");
|
|
244
|
+
}
|
|
245
|
+
else if (typeof action.postcondition !== 'string' || action.postcondition.trim() === '') {
|
|
246
|
+
errors.push('action.postcondition must be a non-empty shell command');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
241
249
|
}
|
|
242
250
|
// ─── PLACEMENT ───────────────────────────────────────────────────────────────
|
|
243
251
|
if (config.device !== undefined && config.devices !== undefined) {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type MonitorConfig, type MonitorEvent } from './config.js';
|
|
15
15
|
import { type Observation } from './sources/index.js';
|
|
16
|
+
/** How often the engine wakes to check which monitors are due. */
|
|
17
|
+
export declare const MONITOR_ENGINE_TICK_MS = 5000;
|
|
16
18
|
/** Poll-model source types the engine actually evaluates on a cadence; ws/webhook are push-only and inert here. */
|
|
17
19
|
export declare const POLL_SOURCE_TYPES: Set<string>;
|
|
18
20
|
/**
|
|
@@ -58,7 +60,9 @@ export declare class MonitorEngine {
|
|
|
58
60
|
private ticking;
|
|
59
61
|
constructor(logFn?: LogFn);
|
|
60
62
|
/** Load owned+enabled monitors and start the tick loop. */
|
|
61
|
-
start(
|
|
63
|
+
start(options?: {
|
|
64
|
+
externalScheduler?: boolean;
|
|
65
|
+
}): void;
|
|
62
66
|
/** Reload monitor configs (SIGHUP). */
|
|
63
67
|
reload(): void;
|
|
64
68
|
/** Stop the tick loop. */
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
import { listMonitors, monitorRunsOnThisDevice, parseInterval, setMonitorEnabled, } from './config.js';
|
|
15
15
|
import { evaluateSource } from './sources/index.js';
|
|
16
16
|
import { hasChanged, readState, writeState, recordFireTime, writeFireRecord, recordCheck, markDroughtNotified, } from './state.js';
|
|
17
|
-
import { dispatchAction } from './dispatch.js';
|
|
17
|
+
import { dispatchAction, injectEvent } from './dispatch.js';
|
|
18
18
|
import { sendToOwner } from '../notify.js';
|
|
19
19
|
import { readRunMeta } from '../scheduling/routines.js';
|
|
20
20
|
/** How often the engine wakes to check which monitors are due. */
|
|
21
|
-
const
|
|
21
|
+
export const MONITOR_ENGINE_TICK_MS = 5_000;
|
|
22
22
|
/** Default evaluation cadence for sources that carry no explicit interval. */
|
|
23
23
|
const DEFAULT_INTERVAL_MS = 60_000;
|
|
24
24
|
/**
|
|
@@ -126,10 +126,12 @@ export class MonitorEngine {
|
|
|
126
126
|
this.logFn = logFn;
|
|
127
127
|
}
|
|
128
128
|
/** Load owned+enabled monitors and start the tick loop. */
|
|
129
|
-
start() {
|
|
129
|
+
start(options = {}) {
|
|
130
130
|
this.loadAll();
|
|
131
131
|
this.logFn('INFO', `Monitor engine started (${this.monitors.length} monitor(s) on this device)`);
|
|
132
|
-
|
|
132
|
+
if (!options.externalScheduler) {
|
|
133
|
+
this.timer = setInterval(() => void this.tick(), MONITOR_ENGINE_TICK_MS);
|
|
134
|
+
}
|
|
133
135
|
}
|
|
134
136
|
/** Reload monitor configs (SIGHUP). */
|
|
135
137
|
reload() {
|
|
@@ -282,12 +284,19 @@ export class MonitorEngine {
|
|
|
282
284
|
// exactly the fires whose `ok` needs revisiting; `resolveFireOutcome`
|
|
283
285
|
// (state.ts) never trusts this field — it re-reads the run fresh instead.
|
|
284
286
|
const runStatusAtFire = result.runId ? readRunMeta(monitor.name, result.runId)?.status : undefined;
|
|
287
|
+
// Snapshot the postcondition with `{event}` already interpolated so a later
|
|
288
|
+
// `resolveFireOutcome` can assert the stated effect without re-reading YAML
|
|
289
|
+
// (PHNX-2842). Notify/webhook-out have no run to settle, so they skip this.
|
|
290
|
+
const postcondition = (result.kind === 'run' || result.kind === 'routine') && monitor.action.postcondition
|
|
291
|
+
? injectEvent(monitor.action.postcondition, event)
|
|
292
|
+
: undefined;
|
|
285
293
|
writeFireRecord(event, {
|
|
286
294
|
...(result.runId ? { runId: result.runId } : {}),
|
|
287
295
|
action: result.kind,
|
|
288
296
|
ok: result.ok,
|
|
289
297
|
...(result.error ? { error: result.error } : {}),
|
|
290
298
|
...(runStatusAtFire ? { runStatusAtFire } : {}),
|
|
299
|
+
...(postcondition ? { postcondition } : {}),
|
|
291
300
|
});
|
|
292
301
|
writeState(monitor.name, decision.value, decision.dedupeKey, { lastFiredAt: event.firedAt, fireTimes });
|
|
293
302
|
this.logFn(result.ok ? 'INFO' : 'ERROR', `monitor '${monitor.name}' fired → ${result.kind}` +
|
|
@@ -118,6 +118,16 @@ export interface FireRecord extends MonitorEvent {
|
|
|
118
118
|
* run fresh on every call instead of trusting this snapshot.
|
|
119
119
|
*/
|
|
120
120
|
runStatusAtFire?: RunMeta['status'];
|
|
121
|
+
/**
|
|
122
|
+
* The action's postcondition command, snapshotted at fire time with `{event}`
|
|
123
|
+
* already interpolated (PHNX-2842). `resolveFireOutcome` runs this once the
|
|
124
|
+
* dispatched run has settled `completed`.
|
|
125
|
+
*/
|
|
126
|
+
postcondition?: string;
|
|
127
|
+
/** Result of the postcondition check, persisted after the first evaluation. */
|
|
128
|
+
postconditionOk?: boolean;
|
|
129
|
+
/** stderr/stdout snippet when `postconditionOk` is false. */
|
|
130
|
+
postconditionError?: string;
|
|
121
131
|
}
|
|
122
132
|
/** List a monitor's fire history, chronologically ascending. */
|
|
123
133
|
export declare function listFires(name: string): FireRecord[];
|
|
@@ -127,10 +137,28 @@ export interface ReconciledFireOutcome {
|
|
|
127
137
|
ok: boolean;
|
|
128
138
|
/** The run's live terminal status, when a runId is present and resolvable. */
|
|
129
139
|
runStatus?: RunMeta['status'];
|
|
140
|
+
/**
|
|
141
|
+
* Present when a `completed` run had a postcondition to assert (PHNX-2842).
|
|
142
|
+
* `met` = the command exited 0; `none` = ran but the intended effect did not
|
|
143
|
+
* happen (the fire must not read as `ok`).
|
|
144
|
+
*/
|
|
145
|
+
effect?: 'met' | 'none';
|
|
146
|
+
/** Why the fire is not ok, when the postcondition failed. */
|
|
147
|
+
error?: string;
|
|
130
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Run a fire's postcondition command. Exit 0 means the intended effect happened;
|
|
151
|
+
* anything else (nonzero, timeout, spawn error, empty command) is "no effect".
|
|
152
|
+
* Real `/bin/sh -c` (or `cmd /c`) — the same seam command sources use.
|
|
153
|
+
*/
|
|
154
|
+
export declare function evaluatePostcondition(command: string): {
|
|
155
|
+
ok: boolean;
|
|
156
|
+
error?: string;
|
|
157
|
+
};
|
|
131
158
|
/**
|
|
132
159
|
* Reconcile a fire's frozen `ok` against its dispatched run's REAL, current
|
|
133
|
-
* status — the render-time fix for RUSH-2690
|
|
160
|
+
* status — the render-time fix for RUSH-2690 — and, when the run has settled
|
|
161
|
+
* `completed`, against a declared postcondition (PHNX-2842).
|
|
134
162
|
*
|
|
135
163
|
* `writeFireRecord` (this module) persists `ok` once, at fire time, from
|
|
136
164
|
* `dispatchAction`'s synchronous return. For a `run`/`routine` action that
|
|
@@ -146,5 +174,13 @@ export interface ReconciledFireOutcome {
|
|
|
146
174
|
* A fire with no `runId` (a `notify`/`webhook-out` action, or a `run`/`routine`
|
|
147
175
|
* dispatch that never got a runId at all) has nothing to reconcile against —
|
|
148
176
|
* its frozen `ok` is the only signal and is returned as-is.
|
|
177
|
+
*
|
|
178
|
+
* `completed` is not success by itself. An agent that exits 0 without doing
|
|
179
|
+
* the job (merge-on-green that never merged) used to record `ok` because
|
|
180
|
+
* `OK_RUN_STATUSES` treated `completed` as healthy. When the fire carries a
|
|
181
|
+
* `postcondition` command, this function runs it once the run has settled and
|
|
182
|
+
* returns `ok: false, effect: 'none'` when it fails — distinguishing "ran but
|
|
183
|
+
* no effect" from a working fire. The result is persisted on the fire record
|
|
184
|
+
* so later listings do not re-exec the command.
|
|
149
185
|
*/
|
|
150
186
|
export declare function resolveFireOutcome(jobName: string, fire: FireRecord): ReconciledFireOutcome;
|