@phnx-labs/agents-cli 1.22.59 → 1.22.61
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 +71 -0
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +2 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/exec.js +35 -1
- package/dist/commands/harness-hooks.d.ts +55 -0
- package/dist/commands/harness-hooks.js +104 -0
- package/dist/commands/harness-wizard.d.ts +33 -14
- package/dist/commands/harness-wizard.js +53 -23
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +86 -5
- package/dist/commands/perf.js +10 -0
- package/dist/commands/reminders.d.ts +9 -0
- package/dist/commands/reminders.js +49 -0
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +13 -0
- package/dist/commands/sessions-picker.d.ts +13 -0
- package/dist/commands/sessions-picker.js +17 -8
- package/dist/commands/sessions.js +13 -11
- package/dist/commands/teams-picker.js +20 -6
- package/dist/commands/teams.d.ts +3 -3
- package/dist/commands/teams.js +86 -24
- package/dist/index.js +9 -0
- package/dist/lib/accounting/rotate.d.ts +63 -0
- package/dist/lib/accounting/rotate.js +240 -16
- package/dist/lib/accounting/usage-sync.d.ts +12 -2
- package/dist/lib/accounting/usage-sync.js +34 -6
- package/dist/lib/browser/drivers/local.d.ts +11 -0
- package/dist/lib/browser/drivers/local.js +26 -0
- package/dist/lib/browser/profiles.js +8 -6
- package/dist/lib/browser/service.d.ts +12 -8
- package/dist/lib/browser/service.js +38 -10
- package/dist/lib/claude-statusline.d.ts +14 -1
- package/dist/lib/claude-statusline.js +27 -2
- package/dist/lib/daemon/runner.js +17 -2
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/doctor-findings.js +22 -4
- package/dist/lib/doctor-diff.d.ts +21 -5
- package/dist/lib/doctor-diff.js +242 -76
- package/dist/lib/feed/events.d.ts +1 -1
- package/dist/lib/feed/events.js +28 -15
- package/dist/lib/github/gh-overload.d.ts +58 -0
- package/dist/lib/github/gh-overload.js +246 -0
- package/dist/lib/github/rest.d.ts +64 -0
- package/dist/lib/github/rest.js +111 -0
- package/dist/lib/harness-connection-test.d.ts +57 -0
- package/dist/lib/harness-connection-test.js +80 -0
- package/dist/lib/heal.js +8 -3
- package/dist/lib/installations/shims.d.ts +22 -0
- package/dist/lib/installations/shims.js +104 -0
- package/dist/lib/linear-project-counts.js +8 -0
- package/dist/lib/linear-rate-limit.d.ts +26 -0
- package/dist/lib/linear-rate-limit.js +163 -0
- package/dist/lib/mcp.d.ts +9 -0
- package/dist/lib/mcp.js +37 -1
- package/dist/lib/open-url.js +5 -3
- package/dist/lib/perf/db.d.ts +1 -1
- package/dist/lib/perf/db.js +53 -2
- package/dist/lib/perf/types.d.ts +14 -0
- package/dist/lib/permissions.d.ts +28 -0
- package/dist/lib/permissions.js +156 -1
- package/dist/lib/refresh.js +9 -1
- package/dist/lib/reminders.d.ts +29 -0
- package/dist/lib/reminders.js +88 -0
- package/dist/lib/resource-content-diff.d.ts +33 -0
- package/dist/lib/resource-content-diff.js +103 -0
- package/dist/lib/rules/compile.d.ts +7 -0
- package/dist/lib/rules/compile.js +7 -1
- package/dist/lib/session/active.d.ts +41 -4
- package/dist/lib/session/active.js +58 -7
- package/dist/lib/session/host-link.d.ts +22 -0
- package/dist/lib/session/host-link.js +40 -4
- package/dist/lib/session/live-metadata.js +3 -3
- package/dist/lib/session/trajectory.d.ts +42 -0
- package/dist/lib/session/trajectory.js +46 -27
- package/dist/lib/ssh-exec.d.ts +30 -0
- package/dist/lib/ssh-exec.js +37 -5
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/subagents-registry.d.ts +18 -0
- package/dist/lib/subagents-registry.js +79 -0
- package/dist/lib/teams/agents.d.ts +12 -0
- package/dist/lib/teams/agents.js +51 -0
- package/dist/lib/teams/api.d.ts +8 -0
- package/dist/lib/teams/api.js +50 -6
- package/dist/lib/teams/delivery.d.ts +14 -4
- package/dist/lib/teams/delivery.js +15 -5
- package/dist/lib/traces/schema2-build.d.ts +85 -0
- package/dist/lib/traces/schema2-build.js +637 -0
- package/dist/lib/traces/schema2-danger.d.ts +36 -0
- package/dist/lib/traces/schema2-danger.js +185 -0
- package/dist/lib/traces/schema2.d.ts +149 -0
- package/dist/lib/traces/schema2.js +20 -0
- package/dist/lib/traces/sync.d.ts +93 -0
- package/dist/lib/traces/sync.js +75 -22
- package/dist/lib/traces/worker-template.js +5 -0
- package/dist/lib/uninstall.js +10 -1
- package/dist/lib/workflows.d.ts +11 -0
- package/dist/lib/workflows.js +67 -8
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ export declare const CLAUDE_STATUSLINE_COMMAND = "agents __claude-statusline";
|
|
|
9
9
|
export declare function isStatusLineSelfReference(command: string): boolean;
|
|
10
10
|
interface ClaudeStatusLinePayload {
|
|
11
11
|
cwd?: string;
|
|
12
|
+
session_id?: string;
|
|
12
13
|
workspace?: {
|
|
13
14
|
current_dir?: string;
|
|
14
15
|
};
|
|
@@ -29,7 +30,19 @@ interface ClaudeStatusLinePayload {
|
|
|
29
30
|
}
|
|
30
31
|
export declare function ingestClaudeStatusLineUsage(payload: ClaudeStatusLinePayload, versionHome: string): boolean;
|
|
31
32
|
export declare function renderDelegate(payload: string, versionHome: string): string;
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Format a reminder as a dimmed statusline part (empty string when none). The
|
|
35
|
+
* ◆ marker distinguishes it from the host/model/usage parts, and the ANSI dim
|
|
36
|
+
* keeps it quiet next to the live figures.
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatReminderPart(short: string | undefined): string;
|
|
39
|
+
export declare function renderClaudeStatusLine(payload: ClaudeStatusLinePayload, host?: string, delegated?: string, reminder?: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the per-session reminder for the statusline, or '' when none is
|
|
42
|
+
* configured. A malformed reminders file is swallowed here on purpose — a broken
|
|
43
|
+
* prompt is worse than a missing reminder — while `agents reminders` surfaces it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveReminderPart(sessionId?: string): string;
|
|
33
46
|
export declare function runClaudeStatusLine(): Promise<number>;
|
|
34
47
|
export declare function installClaudeStatusLine(versionHome: string): {
|
|
35
48
|
changed: boolean;
|
|
@@ -5,6 +5,7 @@ import { spawnSync } from 'child_process';
|
|
|
5
5
|
import { readClaudeHomeConfig } from './agent-spec/agents.js';
|
|
6
6
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
7
7
|
import { mergeClaudeUsageCacheWindows } from './accounting/usage.js';
|
|
8
|
+
import { loadReminders, pickReminderForSession } from './reminders.js';
|
|
8
9
|
export const CLAUDE_STATUSLINE_COMMAND = 'agents __claude-statusline';
|
|
9
10
|
const DELEGATE_FILE = path.join('.agents', 'claude-statusline-delegate');
|
|
10
11
|
// The private subcommand this feature runs. It is only ever invoked internally,
|
|
@@ -103,7 +104,16 @@ export function renderDelegate(payload, versionHome) {
|
|
|
103
104
|
});
|
|
104
105
|
return result.status === 0 ? result.stdout.trim() : '';
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Format a reminder as a dimmed statusline part (empty string when none). The
|
|
109
|
+
* ◆ marker distinguishes it from the host/model/usage parts, and the ANSI dim
|
|
110
|
+
* keeps it quiet next to the live figures.
|
|
111
|
+
*/
|
|
112
|
+
export function formatReminderPart(short) {
|
|
113
|
+
const text = short?.trim();
|
|
114
|
+
return text ? `\x1b[2m◆ ${text}\x1b[22m` : '';
|
|
115
|
+
}
|
|
116
|
+
export function renderClaudeStatusLine(payload, host = os.hostname().split('.')[0] || os.hostname(), delegated = '', reminder = '') {
|
|
107
117
|
const model = payload.model?.display_name?.trim() || payload.model?.id?.trim() || 'model pending';
|
|
108
118
|
const parts = [host, model];
|
|
109
119
|
if (delegated)
|
|
@@ -114,8 +124,23 @@ export function renderClaudeStatusLine(payload, host = os.hostname().split('.')[
|
|
|
114
124
|
parts.push(`5h ${Math.round(fiveHour)}%`);
|
|
115
125
|
if (Number.isFinite(sevenDay))
|
|
116
126
|
parts.push(`7d ${Math.round(sevenDay)}%`);
|
|
127
|
+
if (reminder)
|
|
128
|
+
parts.push(reminder);
|
|
117
129
|
return parts.join(' · ');
|
|
118
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the per-session reminder for the statusline, or '' when none is
|
|
133
|
+
* configured. A malformed reminders file is swallowed here on purpose — a broken
|
|
134
|
+
* prompt is worse than a missing reminder — while `agents reminders` surfaces it.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveReminderPart(sessionId) {
|
|
137
|
+
try {
|
|
138
|
+
return formatReminderPart(pickReminderForSession(loadReminders(), sessionId)?.short);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
119
144
|
export async function runClaudeStatusLine() {
|
|
120
145
|
const raw = await new Promise((resolve, reject) => {
|
|
121
146
|
let input = '';
|
|
@@ -135,7 +160,7 @@ export async function runClaudeStatusLine() {
|
|
|
135
160
|
const versionHome = versionHomeFromEnv(process.env);
|
|
136
161
|
if (versionHome)
|
|
137
162
|
ingestClaudeStatusLineUsage(payload, versionHome);
|
|
138
|
-
process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : ''));
|
|
163
|
+
process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : '', resolveReminderPart(payload.session_id)));
|
|
139
164
|
return 0;
|
|
140
165
|
}
|
|
141
166
|
export function installClaudeStatusLine(versionHome) {
|
|
@@ -36,7 +36,7 @@ import { ensureLockTarget } from '../fs-atomic.js';
|
|
|
36
36
|
import { walkForFiles } from '../fs-walk.js';
|
|
37
37
|
import { getBinaryPath, isVersionInstalled, resolveVersion, getVersionHomePath } from '../installations/versions.js';
|
|
38
38
|
import { resolveClaudeSetupToken } from '../claude-account-token.js';
|
|
39
|
-
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from '../accounting/rotate.js';
|
|
39
|
+
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, formatNoVerifiedUsageError, } from '../accounting/rotate.js';
|
|
40
40
|
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
|
|
41
41
|
import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
|
|
42
42
|
import { isCustomHarnessName, readProfile } from '../profiles.js';
|
|
@@ -959,12 +959,21 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
959
959
|
let version;
|
|
960
960
|
let rotation = null;
|
|
961
961
|
let exhausted;
|
|
962
|
+
let noVerifiedUsage = false;
|
|
962
963
|
try {
|
|
963
964
|
const resolved = await (deps.resolveRunVersion ?? resolveRunVersion)(agent, strategy, cwd);
|
|
964
965
|
version = resolved.version ?? undefined;
|
|
965
966
|
rotation = resolved.rotation;
|
|
966
967
|
exhausted = resolved.exhausted;
|
|
967
|
-
|
|
968
|
+
noVerifiedUsage = resolved.noVerifiedUsage ?? false;
|
|
969
|
+
if (noVerifiedUsage) {
|
|
970
|
+
// Entirely stale usage (PHNX-2526): a routine is unattended, so there is
|
|
971
|
+
// no picker to divert to — it fails loud below rather than launch on a
|
|
972
|
+
// stale number. Do NOT log `rotation.picked` as a pick; it is the refused
|
|
973
|
+
// stale candidate, kept only for the failover chain.
|
|
974
|
+
process.stderr.write(`[agents] routine ${config.name}: ${strategy} found no ${agent} account with fresh usage — refusing to route on stale data\n`);
|
|
975
|
+
}
|
|
976
|
+
else if (rotation) {
|
|
968
977
|
const label = rotation.picked.email
|
|
969
978
|
? `${rotation.picked.email} · ${agent}@${rotation.picked.version}`
|
|
970
979
|
: `${agent}@${rotation.picked.version}`;
|
|
@@ -996,6 +1005,12 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
996
1005
|
if (exhausted) {
|
|
997
1006
|
throw new Error(formatNoHealthyAccountError(agent, strategy, exhausted));
|
|
998
1007
|
}
|
|
1008
|
+
// Entirely stale usage is also NOT a "fall back to the default pin" case — the
|
|
1009
|
+
// default is exactly the account whose stale number can't be trusted. Fail the
|
|
1010
|
+
// run loud (NO_VERIFIED_USAGE) rather than hammer it every tick (PHNX-2526).
|
|
1011
|
+
if (noVerifiedUsage) {
|
|
1012
|
+
throw new Error(formatNoVerifiedUsageError(agent, strategy, rotation?.healthy ?? []));
|
|
1013
|
+
}
|
|
999
1014
|
if (!version) {
|
|
1000
1015
|
version = resolveVersion(agent, cwd) ?? undefined;
|
|
1001
1016
|
}
|
|
@@ -6,7 +6,7 @@ import type { OwnerSinkStatus } from '../channels/owner-sink.js';
|
|
|
6
6
|
import { type WindowsSshEnrollmentAudit } from './windows-ssh-enrollment.js';
|
|
7
7
|
import type { SyncStatusRow, OrphanRow } from '../drift.js';
|
|
8
8
|
import type { FetchStatusMarker } from '../auto-pull.js';
|
|
9
|
-
import type
|
|
9
|
+
import { type VersionResourceReport } from '../doctor-diff.js';
|
|
10
10
|
import type { FleetDivergence, FleetHookRuntimeState, FleetVersionSignIn } from './fleet-divergence.js';
|
|
11
11
|
export type FindingSeverity = 'critical' | 'warning';
|
|
12
12
|
/** A machine-stable class for a finding — drives {@link remediationFor} and lets
|
|
@@ -44,6 +44,24 @@ import { loginHint } from '../signin-badge.js';
|
|
|
44
44
|
import { CONFIG_ENV_ISOLATED_AGENTS } from '../installations/shims.js';
|
|
45
45
|
import { padToWidth, stringWidth } from '../text/width.js';
|
|
46
46
|
import { windowsSshEnrollmentProblem } from './windows-ssh-enrollment.js';
|
|
47
|
+
import { DOCTOR_ALL_KINDS } from '../doctor-diff.js';
|
|
48
|
+
/**
|
|
49
|
+
* Singular noun per resource kind for finding messages. A naive `kind.replace(/s$/,'')`
|
|
50
|
+
* mangles `memory` → `memor` and `mcp` → `mcp` inconsistently, so the mapping is
|
|
51
|
+
* explicit. Keyed by `DoctorKind` so a new kind fails the type check until named.
|
|
52
|
+
*/
|
|
53
|
+
const KIND_SINGULAR = {
|
|
54
|
+
commands: 'command',
|
|
55
|
+
skills: 'skill',
|
|
56
|
+
hooks: 'hook',
|
|
57
|
+
rules: 'rule',
|
|
58
|
+
mcp: 'mcp',
|
|
59
|
+
permissions: 'permission',
|
|
60
|
+
subagents: 'subagent',
|
|
61
|
+
plugins: 'plugin',
|
|
62
|
+
workflows: 'workflow',
|
|
63
|
+
memory: 'memory',
|
|
64
|
+
};
|
|
47
65
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
48
66
|
/** Agents with NO per-version credential isolation: their login is shared across
|
|
49
67
|
* every installed version, so a "log into THIS version" remediation would be a
|
|
@@ -87,8 +105,8 @@ export const ALL_FINDING_KINDS = [
|
|
|
87
105
|
'hook-runtime-broken', // a wired hook's generated shim wrapper is missing/unusable
|
|
88
106
|
'hook-runtime-visibility-unavailable', // remote CLI cannot report generated wrapper health
|
|
89
107
|
'cli-missing', // a managed agent whose binary won't resolve
|
|
90
|
-
'missing-resource', // a missing command/skill/rule/mcp/permission/subagent
|
|
91
|
-
'content-drift', // a resource diverged from source
|
|
108
|
+
'missing-resource', // a missing command/skill/rule/mcp/permission/subagent/workflow/memory
|
|
109
|
+
'content-drift', // a resource (any synced kind) diverged from source
|
|
92
110
|
'never-synced', // installed but never synced
|
|
93
111
|
'stale', // sources changed since last sync
|
|
94
112
|
'repo-behind', // a config repo behind origin
|
|
@@ -392,8 +410,8 @@ export function buildLocalFindings(input) {
|
|
|
392
410
|
const missingPlugins = [];
|
|
393
411
|
const missingOther = [];
|
|
394
412
|
const drifted = [];
|
|
395
|
-
for (const kind of
|
|
396
|
-
const singular = kind
|
|
413
|
+
for (const kind of DOCTOR_ALL_KINDS) {
|
|
414
|
+
const singular = KIND_SINGULAR[kind];
|
|
397
415
|
for (const r of report.kinds[kind] ?? []) {
|
|
398
416
|
if (r.status === 'missing') {
|
|
399
417
|
if (kind === 'hooks')
|
|
@@ -9,21 +9,37 @@
|
|
|
9
9
|
* - missing — resolved source exists, not present in home
|
|
10
10
|
* - extra — present in home, no source in any layer
|
|
11
11
|
*
|
|
12
|
-
* Coverage
|
|
12
|
+
* Coverage — every kind `syncResourcesToVersion` writes is content-aware, so a
|
|
13
|
+
* byte change under an unchanged name is `diff`, never a false `ok` (PHNX-3504):
|
|
13
14
|
* commands, skills, hooks, rules — full content compare with source layer.
|
|
14
|
-
* mcp
|
|
15
|
+
* mcp — structural compare of the home server def vs resolved source.
|
|
16
|
+
* subagents — re-render source through the registry transform, byte-compare.
|
|
17
|
+
* workflows — layout-aware per-harness content compare (dir tree or file).
|
|
18
|
+
* memory — knowledge facts (~/.agents/memory/*.md), per-fact byte-compare.
|
|
19
|
+
* plugins — per-item content compare of the marketplace mirror.
|
|
20
|
+
* permissions — per-rule compare in the harness's native vocabulary for the
|
|
21
|
+
* representable harnesses (claude/opencode/cursor/droid/openclaw); the lossy
|
|
22
|
+
* TOML/flag harnesses stay presence-only with an honest `detail`, never a
|
|
23
|
+
* faked `ok`.
|
|
24
|
+
* A completeness test binds `DOCTOR_ALL_KINDS` to the writer set so a future
|
|
25
|
+
* synced kind cannot silently become a blind spot. `promptcuts` is NOT a kind —
|
|
26
|
+
* it is not version-scoped, so there is nothing per-home to diff.
|
|
15
27
|
*
|
|
16
28
|
* Intentional asymmetries (must mirror sync):
|
|
17
29
|
* - hooks ignore the project layer (`syncResourcesToVersion` skips
|
|
18
30
|
* project/.agents/hooks/ for safety).
|
|
19
|
-
* - rules/AGENTS.md
|
|
20
|
-
*
|
|
31
|
+
* - rules/AGENTS.md is compared against the preset composition the rules writer
|
|
32
|
+
* emits (`composeRulesFromState`), re-rendered from the current `subrules/`
|
|
33
|
+
* fragments — so a fragment edit is caught even though `rules/AGENTS.md`
|
|
34
|
+
* never changed. A non-@-import home compiled by `agents refresh-rules`
|
|
35
|
+
* carries a leading `COMPILED_HEADER`, which is stripped before comparing so
|
|
36
|
+
* a header-compiled home still reconciles (the header is not source content).
|
|
21
37
|
*/
|
|
22
38
|
import type { AgentId } from './types.js';
|
|
23
39
|
import type { DiscoveredPlugin } from './types.js';
|
|
24
40
|
import { type HookWiringReport } from './hooks/install.js';
|
|
25
41
|
import { type ResourceInventory } from './resource-inventory.js';
|
|
26
|
-
export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | '
|
|
42
|
+
export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'workflows' | 'memory';
|
|
27
43
|
export type DiffStatus = 'ok' | 'diff' | 'missing' | 'extra';
|
|
28
44
|
export type SourceLayer = 'project' | 'user' | 'system' | 'extra';
|
|
29
45
|
export interface ResourceDiff {
|
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -9,21 +9,44 @@
|
|
|
9
9
|
* - missing — resolved source exists, not present in home
|
|
10
10
|
* - extra — present in home, no source in any layer
|
|
11
11
|
*
|
|
12
|
-
* Coverage
|
|
12
|
+
* Coverage — every kind `syncResourcesToVersion` writes is content-aware, so a
|
|
13
|
+
* byte change under an unchanged name is `diff`, never a false `ok` (PHNX-3504):
|
|
13
14
|
* commands, skills, hooks, rules — full content compare with source layer.
|
|
14
|
-
* mcp
|
|
15
|
+
* mcp — structural compare of the home server def vs resolved source.
|
|
16
|
+
* subagents — re-render source through the registry transform, byte-compare.
|
|
17
|
+
* workflows — layout-aware per-harness content compare (dir tree or file).
|
|
18
|
+
* memory — knowledge facts (~/.agents/memory/*.md), per-fact byte-compare.
|
|
19
|
+
* plugins — per-item content compare of the marketplace mirror.
|
|
20
|
+
* permissions — per-rule compare in the harness's native vocabulary for the
|
|
21
|
+
* representable harnesses (claude/opencode/cursor/droid/openclaw); the lossy
|
|
22
|
+
* TOML/flag harnesses stay presence-only with an honest `detail`, never a
|
|
23
|
+
* faked `ok`.
|
|
24
|
+
* A completeness test binds `DOCTOR_ALL_KINDS` to the writer set so a future
|
|
25
|
+
* synced kind cannot silently become a blind spot. `promptcuts` is NOT a kind —
|
|
26
|
+
* it is not version-scoped, so there is nothing per-home to diff.
|
|
15
27
|
*
|
|
16
28
|
* Intentional asymmetries (must mirror sync):
|
|
17
29
|
* - hooks ignore the project layer (`syncResourcesToVersion` skips
|
|
18
30
|
* project/.agents/hooks/ for safety).
|
|
19
|
-
* - rules/AGENTS.md
|
|
20
|
-
*
|
|
31
|
+
* - rules/AGENTS.md is compared against the preset composition the rules writer
|
|
32
|
+
* emits (`composeRulesFromState`), re-rendered from the current `subrules/`
|
|
33
|
+
* fragments — so a fragment edit is caught even though `rules/AGENTS.md`
|
|
34
|
+
* never changed. A non-@-import home compiled by `agents refresh-rules`
|
|
35
|
+
* carries a leading `COMPILED_HEADER`, which is stripped before comparing so
|
|
36
|
+
* a header-compiled home still reconciles (the header is not source content).
|
|
21
37
|
*/
|
|
22
38
|
import * as fs from 'fs';
|
|
23
39
|
import * as path from 'path';
|
|
24
|
-
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
25
|
-
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir,
|
|
40
|
+
import { AGENTS, agentConfigDirName, getMcpConfigPathForHome } from './agents.js';
|
|
41
|
+
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir, getActiveRulesPreset, } from './state.js';
|
|
26
42
|
import { composeRulesFromState } from './rules/compose.js';
|
|
43
|
+
import { COMPILED_HEADER, supportsRulesImports } from './rules/compile.js';
|
|
44
|
+
import { dirsContentMatch, filesContentMatch } from './resource-content-diff.js';
|
|
45
|
+
import { subagentContentMatches } from './subagents-registry.js';
|
|
46
|
+
import { getMcpServersByName, mcpServerMatches } from './mcp.js';
|
|
47
|
+
import { permissionsGroupMatches, PERMISSIONS_REPRESENTABLE } from './permissions.js';
|
|
48
|
+
import { workflowContentMatches, resolveWorkflowRef } from './workflows.js';
|
|
49
|
+
import { listMemoryFacts, memoryTargetDir } from './memory.js';
|
|
27
50
|
import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, compareVersions, } from './installations/versions.js';
|
|
28
51
|
import { discoverPlugins, marketplaceSpecForName } from './plugins/plugins.js';
|
|
29
52
|
import { pluginInstallDir, repairableManifestFields } from './plugins/plugin-marketplace.js';
|
|
@@ -47,7 +70,8 @@ const ALL_KINDS = [
|
|
|
47
70
|
'permissions',
|
|
48
71
|
'subagents',
|
|
49
72
|
'plugins',
|
|
50
|
-
'
|
|
73
|
+
'workflows',
|
|
74
|
+
'memory',
|
|
51
75
|
];
|
|
52
76
|
function normalize(content) {
|
|
53
77
|
return content.replace(/\r\n/g, '\n').trim();
|
|
@@ -121,6 +145,36 @@ function buildLayerBases(cwd, kind, opts = {}) {
|
|
|
121
145
|
out.push({ layer: 'extra', path: path.join(e.dir, kind), alias: e.alias });
|
|
122
146
|
return out;
|
|
123
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve directory-shaped source resources of `kind` by name across the layers
|
|
150
|
+
* (project > user > system > extra; first layer wins a name collision), keeping
|
|
151
|
+
* only entries whose directory satisfies `predicate` (e.g. holds `AGENT.md`).
|
|
152
|
+
*/
|
|
153
|
+
function resolveSourceDirsByName(kind, cwd, excludeProject, predicate) {
|
|
154
|
+
const out = new Map();
|
|
155
|
+
for (const base of buildLayerBases(cwd, kind, { excludeProject })) {
|
|
156
|
+
if (!fs.existsSync(base.path))
|
|
157
|
+
continue;
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = fs.readdirSync(base.path, { withFileTypes: true });
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
167
|
+
continue;
|
|
168
|
+
if (out.has(entry.name))
|
|
169
|
+
continue;
|
|
170
|
+
const dir = path.join(base.path, entry.name);
|
|
171
|
+
if (!predicate(dir))
|
|
172
|
+
continue;
|
|
173
|
+
out.set(entry.name, { layer: base.layer, path: dir, alias: base.alias });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
124
178
|
// ─── commands ─────────────────────────────────────────────────────────────────
|
|
125
179
|
function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
126
180
|
const agentConfig = AGENTS[agent];
|
|
@@ -265,55 +319,6 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
265
319
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
266
320
|
}
|
|
267
321
|
// ─── skills ───────────────────────────────────────────────────────────────────
|
|
268
|
-
function dirsContentMatch(src, dst) {
|
|
269
|
-
const ignore = new Set(['.DS_Store', '.git', '.gitignore', '.venv', '__pycache__', 'node_modules']);
|
|
270
|
-
const srcEntries = (() => {
|
|
271
|
-
try {
|
|
272
|
-
return fs.readdirSync(src, { withFileTypes: true });
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
})();
|
|
278
|
-
const dstEntries = (() => {
|
|
279
|
-
try {
|
|
280
|
-
return fs.readdirSync(dst, { withFileTypes: true });
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
})();
|
|
286
|
-
if (!srcEntries || !dstEntries)
|
|
287
|
-
return false;
|
|
288
|
-
const filter = (es) => es.filter((e) => !e.isSymbolicLink() && !ignore.has(e.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
289
|
-
const srcF = filter(srcEntries);
|
|
290
|
-
const dstF = filter(dstEntries);
|
|
291
|
-
if (srcF.length !== dstF.length)
|
|
292
|
-
return false;
|
|
293
|
-
for (let i = 0; i < srcF.length; i++) {
|
|
294
|
-
if (srcF[i].name !== dstF[i].name)
|
|
295
|
-
return false;
|
|
296
|
-
const a = path.join(src, srcF[i].name);
|
|
297
|
-
const b = path.join(dst, dstF[i].name);
|
|
298
|
-
if (srcF[i].isDirectory()) {
|
|
299
|
-
if (!dstF[i].isDirectory())
|
|
300
|
-
return false;
|
|
301
|
-
if (!dirsContentMatch(a, b))
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
else if (srcF[i].isFile()) {
|
|
305
|
-
if (!dstF[i].isFile())
|
|
306
|
-
return false;
|
|
307
|
-
const ac = readSafe(a);
|
|
308
|
-
const bc = readSafe(b);
|
|
309
|
-
if (ac == null || bc == null)
|
|
310
|
-
return false;
|
|
311
|
-
if (normalize(ac) !== normalize(bc))
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
322
|
function diffSkills(agent, version, cwd, excludeProject = false) {
|
|
318
323
|
// Native ~/.agents/skills consumers (Gemini, …) read central skills directly.
|
|
319
324
|
// The orchestrator DELETES their version-home skills dir (syncResourcesToVersion)
|
|
@@ -523,11 +528,18 @@ function diffRules(agent, version, cwd, excludeProject = false) {
|
|
|
523
528
|
continue;
|
|
524
529
|
}
|
|
525
530
|
const expected = expectedRuleContent(agent, name, version, src.path);
|
|
526
|
-
|
|
531
|
+
let actual = readSafe(homePath);
|
|
527
532
|
if (expected == null || actual == null) {
|
|
528
533
|
rows.push({ kind: 'rules', name, status: 'diff', source: src.layer, sourcePath: src.path, homePath });
|
|
529
534
|
continue;
|
|
530
535
|
}
|
|
536
|
+
// A non-@-import home compiled by `agents refresh-rules` carries a leading
|
|
537
|
+
// COMPILED_HEADER the raw preset composition does not. Strip it before the
|
|
538
|
+
// compare so a header-compiled home still reconciles — the header is
|
|
539
|
+
// agents-cli provenance, not source content (PHNX-3504).
|
|
540
|
+
if (name === 'AGENTS' && !supportsRulesImports(agent) && actual.startsWith(COMPILED_HEADER)) {
|
|
541
|
+
actual = actual.slice(COMPILED_HEADER.length);
|
|
542
|
+
}
|
|
531
543
|
rows.push({
|
|
532
544
|
kind: 'rules',
|
|
533
545
|
name,
|
|
@@ -555,22 +567,175 @@ function diffRules(agent, version, cwd, excludeProject = false) {
|
|
|
555
567
|
}
|
|
556
568
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
557
569
|
}
|
|
558
|
-
// ───
|
|
559
|
-
function
|
|
570
|
+
// ─── mcp (content-aware) ────────────────────────────────────────────────────
|
|
571
|
+
function diffMcp(agent, version, cwd, available, synced) {
|
|
572
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
573
|
+
const syncedSet = new Set(synced);
|
|
560
574
|
const availableSet = new Set(available);
|
|
575
|
+
const homePath = getMcpConfigPathForHome(agent, versionHome);
|
|
576
|
+
// Resolve each available server's source def once (project trust off — this is
|
|
577
|
+
// a read-only diff, and the writer applies the same servers).
|
|
578
|
+
const sourceByName = new Map(getMcpServersByName(available, { cwd, enforceProjectTrust: false }).map((s) => [s.name, s]));
|
|
579
|
+
const rows = [];
|
|
580
|
+
for (const name of available) {
|
|
581
|
+
if (!syncedSet.has(name)) {
|
|
582
|
+
rows.push({ kind: 'mcp', name, status: 'missing', sourcePath: sourceByName.get(name)?.path });
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const source = sourceByName.get(name);
|
|
586
|
+
const matches = source ? mcpServerMatches(agent, versionHome, name, source.config) : false;
|
|
587
|
+
rows.push({
|
|
588
|
+
kind: 'mcp',
|
|
589
|
+
name,
|
|
590
|
+
status: matches ? 'ok' : 'diff',
|
|
591
|
+
sourcePath: source?.path,
|
|
592
|
+
homePath,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
for (const name of synced) {
|
|
596
|
+
if (!availableSet.has(name))
|
|
597
|
+
rows.push({ kind: 'mcp', name, status: 'extra', homePath });
|
|
598
|
+
}
|
|
599
|
+
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
600
|
+
}
|
|
601
|
+
// ─── permissions (content-aware where the native format allows) ──────────────
|
|
602
|
+
function diffPermissions(agent, version, available, synced) {
|
|
603
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
561
604
|
const syncedSet = new Set(synced);
|
|
605
|
+
const availableSet = new Set(available);
|
|
606
|
+
const representable = PERMISSIONS_REPRESENTABLE.has(agent);
|
|
562
607
|
const rows = [];
|
|
563
608
|
for (const name of available) {
|
|
609
|
+
if (!syncedSet.has(name)) {
|
|
610
|
+
rows.push({ kind: 'permissions', name, status: 'missing' });
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (representable) {
|
|
614
|
+
const matches = permissionsGroupMatches(agent, versionHome, name);
|
|
615
|
+
rows.push({ kind: 'permissions', name, status: matches ? 'ok' : 'diff' });
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
// Lossy TOML/flag harness — the on-disk format carries no faithful
|
|
619
|
+
// per-group rule provenance, so we can only attest presence. Say so
|
|
620
|
+
// explicitly rather than fake a verified `ok` (PHNX-3504).
|
|
621
|
+
rows.push({ kind: 'permissions', name, status: 'ok', detail: 'format cannot verify content' });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (const name of synced) {
|
|
625
|
+
if (!availableSet.has(name))
|
|
626
|
+
rows.push({ kind: 'permissions', name, status: 'extra' });
|
|
627
|
+
}
|
|
628
|
+
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
629
|
+
}
|
|
630
|
+
// ─── subagents (content-aware) ───────────────────────────────────────────────
|
|
631
|
+
function diffSubagents(agent, version, cwd, available, synced) {
|
|
632
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
633
|
+
const syncedSet = new Set(synced);
|
|
634
|
+
const availableSet = new Set(available);
|
|
635
|
+
// Resolve sources across the SAME layers `available` was built from
|
|
636
|
+
// (getAvailableResources is project-inclusive), so every available name finds
|
|
637
|
+
// its source dir for the content compare rather than false-diffing.
|
|
638
|
+
const sourceByName = resolveSourceDirsByName('subagents', cwd, false, (dir) => fs.existsSync(path.join(dir, 'AGENT.md')));
|
|
639
|
+
const rows = [];
|
|
640
|
+
for (const name of available) {
|
|
641
|
+
const src = sourceByName.get(name);
|
|
642
|
+
if (!syncedSet.has(name)) {
|
|
643
|
+
rows.push({ kind: 'subagents', name, status: 'missing', source: src?.layer, sourcePath: src?.path });
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const matches = src ? subagentContentMatches(agent, versionHome, name, src.path) : false;
|
|
564
647
|
rows.push({
|
|
565
|
-
kind,
|
|
648
|
+
kind: 'subagents',
|
|
566
649
|
name,
|
|
567
|
-
status:
|
|
650
|
+
status: matches ? 'ok' : 'diff',
|
|
651
|
+
source: src?.layer,
|
|
652
|
+
sourcePath: src?.path,
|
|
568
653
|
});
|
|
569
654
|
}
|
|
570
655
|
for (const name of synced) {
|
|
571
|
-
if (availableSet.has(name))
|
|
656
|
+
if (!availableSet.has(name))
|
|
657
|
+
rows.push({ kind: 'subagents', name, status: 'extra' });
|
|
658
|
+
}
|
|
659
|
+
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
660
|
+
}
|
|
661
|
+
// ─── workflows (content-aware) ───────────────────────────────────────────────
|
|
662
|
+
function diffWorkflows(agent, version, cwd, available, synced) {
|
|
663
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
664
|
+
const syncedSet = new Set(synced);
|
|
665
|
+
const availableSet = new Set(available);
|
|
666
|
+
const rows = [];
|
|
667
|
+
for (const name of available) {
|
|
668
|
+
const sourcePath = resolveWorkflowRef(name, cwd);
|
|
669
|
+
if (!syncedSet.has(name)) {
|
|
670
|
+
rows.push({ kind: 'workflows', name, status: 'missing', sourcePath: sourcePath ?? undefined });
|
|
572
671
|
continue;
|
|
573
|
-
|
|
672
|
+
}
|
|
673
|
+
const matches = sourcePath ? workflowContentMatches(agent, versionHome, name, sourcePath) : false;
|
|
674
|
+
rows.push({
|
|
675
|
+
kind: 'workflows',
|
|
676
|
+
name,
|
|
677
|
+
status: matches ? 'ok' : 'diff',
|
|
678
|
+
sourcePath: sourcePath ?? undefined,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
for (const name of synced) {
|
|
682
|
+
if (!availableSet.has(name))
|
|
683
|
+
rows.push({ kind: 'workflows', name, status: 'extra' });
|
|
684
|
+
}
|
|
685
|
+
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
686
|
+
}
|
|
687
|
+
// ─── memory (knowledge facts — content-aware) ────────────────────────────────
|
|
688
|
+
/**
|
|
689
|
+
* Diff synced knowledge-fact memory (~/.agents/memory/*.md fanned into each
|
|
690
|
+
* capable home under `memoryTargetDir`). Distinct from the rules-preset list
|
|
691
|
+
* that overloads `AvailableResources.memory` — this reads the canonical facts
|
|
692
|
+
* (`listMemoryFacts`) and the home copies bounded by the `.agents-cli-memory.json`
|
|
693
|
+
* manifest, so an untracked native memory file the user authored is never
|
|
694
|
+
* mistaken for drift (PHNX-3504).
|
|
695
|
+
*/
|
|
696
|
+
function diffMemory(agent, version, cwd) {
|
|
697
|
+
if (!supports(agent, 'memory', version).ok)
|
|
698
|
+
return [];
|
|
699
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
700
|
+
const targetDir = path.join(versionHome, memoryTargetDir(agent));
|
|
701
|
+
const facts = listMemoryFacts(cwd);
|
|
702
|
+
// The manifest names exactly the fact files agents-cli wrote (plus 'MEMORY').
|
|
703
|
+
const managedManifestPath = path.join(targetDir, '.agents-cli-memory.json');
|
|
704
|
+
let managed = [];
|
|
705
|
+
try {
|
|
706
|
+
const raw = JSON.parse(fs.readFileSync(managedManifestPath, 'utf-8'));
|
|
707
|
+
if (Array.isArray(raw.facts))
|
|
708
|
+
managed = raw.facts.filter((f) => typeof f === 'string');
|
|
709
|
+
}
|
|
710
|
+
catch { /* no manifest → nothing managed yet */ }
|
|
711
|
+
const managedSet = new Set(managed);
|
|
712
|
+
const rows = [];
|
|
713
|
+
const factNames = new Set(facts.map((f) => f.name));
|
|
714
|
+
for (const fact of facts) {
|
|
715
|
+
const homePath = path.join(targetDir, `${fact.name}.md`);
|
|
716
|
+
if (!fs.existsSync(homePath)) {
|
|
717
|
+
rows.push({ kind: 'memory', name: fact.name, status: 'missing', source: fact.layer, sourcePath: fact.path });
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const matches = filesContentMatch(fact.path, homePath);
|
|
721
|
+
rows.push({
|
|
722
|
+
kind: 'memory',
|
|
723
|
+
name: fact.name,
|
|
724
|
+
status: matches ? 'ok' : 'diff',
|
|
725
|
+
source: fact.layer,
|
|
726
|
+
sourcePath: fact.path,
|
|
727
|
+
homePath,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
// A managed fact still on disk but no longer in the canonical set is an orphan
|
|
731
|
+
// the next sync would prune — surface it as extra. Never flag 'MEMORY' (the
|
|
732
|
+
// index, always regenerated) or an unmanaged native file (not ours to reconcile).
|
|
733
|
+
for (const name of managedSet) {
|
|
734
|
+
if (name === 'MEMORY' || factNames.has(name))
|
|
735
|
+
continue;
|
|
736
|
+
const homePath = path.join(targetDir, `${name}.md`);
|
|
737
|
+
if (fs.existsSync(homePath))
|
|
738
|
+
rows.push({ kind: 'memory', name, status: 'extra', homePath });
|
|
574
739
|
}
|
|
575
740
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
576
741
|
}
|
|
@@ -683,12 +848,6 @@ function diffPlugins(agent, version, cwd) {
|
|
|
683
848
|
}
|
|
684
849
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
685
850
|
}
|
|
686
|
-
function diffPromptcuts() {
|
|
687
|
-
const sourcePath = getEffectivePromptcutsPath();
|
|
688
|
-
if (!fs.existsSync(sourcePath))
|
|
689
|
-
return [];
|
|
690
|
-
return [{ kind: 'promptcuts', name: 'promptcuts.yaml', status: 'ok', sourcePath }];
|
|
691
|
-
}
|
|
692
851
|
export function diffVersionResources(agent, version, options = {}) {
|
|
693
852
|
const rawCwd = options.cwd ?? process.cwd();
|
|
694
853
|
const excludeProject = options.excludeProject ?? false;
|
|
@@ -709,7 +868,8 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
709
868
|
permissions: [],
|
|
710
869
|
subagents: [],
|
|
711
870
|
plugins: [],
|
|
712
|
-
|
|
871
|
+
workflows: [],
|
|
872
|
+
memory: [],
|
|
713
873
|
};
|
|
714
874
|
if (requested.has('commands'))
|
|
715
875
|
empty.commands = diffCommands(agent, version, cwd, excludeProject);
|
|
@@ -725,20 +885,26 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
725
885
|
if (requested.has('rules'))
|
|
726
886
|
empty.rules = diffRules(agent, version, cwd, excludeProject);
|
|
727
887
|
if (requested.has('mcp'))
|
|
728
|
-
empty.mcp =
|
|
729
|
-
if (requested.has('permissions'))
|
|
730
|
-
empty.permissions =
|
|
888
|
+
empty.mcp = diffMcp(agent, version, cwd, available.mcp, synced.mcp);
|
|
889
|
+
if (requested.has('permissions')) {
|
|
890
|
+
empty.permissions = diffPermissions(agent, version, supports(agent, 'allowlist', version).ok ? available.permissions : [], synced.permissions);
|
|
891
|
+
}
|
|
731
892
|
// Subagents are version-gated (e.g. kimi >= 0.29.0). A version below the floor
|
|
732
893
|
// is never written any subagent by the sync writer, so counting the source
|
|
733
894
|
// ones as "missing" is phantom drift no sync can clear (PHNX-3186). Zero the
|
|
734
895
|
// available set when unsupported; a stale installed copy still surfaces `extra`.
|
|
735
896
|
if (requested.has('subagents')) {
|
|
736
|
-
empty.subagents =
|
|
897
|
+
empty.subagents = diffSubagents(agent, version, cwd, supports(agent, 'subagents', version).ok ? available.subagents : [], synced.subagents);
|
|
737
898
|
}
|
|
738
899
|
if (requested.has('plugins'))
|
|
739
900
|
empty.plugins = diffPlugins(agent, version, cwd);
|
|
740
|
-
|
|
741
|
-
|
|
901
|
+
// Workflows are version-gated too (e.g. grok >= 0.2.111); zero the available
|
|
902
|
+
// set below the floor so a source workflow is not phantom-missing drift.
|
|
903
|
+
if (requested.has('workflows')) {
|
|
904
|
+
empty.workflows = diffWorkflows(agent, version, cwd, supports(agent, 'workflows', version).ok ? available.workflows : [], synced.workflows);
|
|
905
|
+
}
|
|
906
|
+
if (requested.has('memory'))
|
|
907
|
+
empty.memory = diffMemory(agent, version, cwd);
|
|
742
908
|
let ok = 0, diff = 0, missing = 0, extra = 0;
|
|
743
909
|
for (const list of Object.values(empty)) {
|
|
744
910
|
for (const r of list) {
|