@phnx-labs/agents-cli 1.22.60 → 1.22.62
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 +53 -0
- package/README.md +6 -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/feed.js +3 -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/monitors.js +3 -3
- 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/send.js +4 -1
- package/dist/commands/teams.d.ts +1 -1
- package/dist/commands/teams.js +9 -3
- package/dist/index.js +9 -0
- package/dist/lib/accounting/rotate.d.ts +63 -0
- package/dist/lib/accounting/rotate.js +229 -13
- 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/channels/owner-forward.d.ts +14 -8
- package/dist/lib/channels/owner-forward.js +9 -5
- package/dist/lib/channels/registry.d.ts +2 -0
- package/dist/lib/channels/send.js +13 -1
- 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 +25 -16
- package/dist/lib/feed-broadcast.js +5 -12
- 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/humans.d.ts +9 -0
- package/dist/lib/humans.js +29 -8
- 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/notify.d.ts +3 -0
- package/dist/lib/notify.js +63 -21
- package/dist/lib/open-url.js +5 -3
- 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/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/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
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) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ActorKind } from '../actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'run.dispatched' | 'daemon.start' | 'daemon.stop' | 'daemon.error' | 'daemon.info' | 'routine.start' | 'routine.end' | 'watchdog.action' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.lease-denied' | 'secrets.lease-expire' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.failed' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'run.dispatched' | 'daemon.start' | 'daemon.stop' | 'daemon.error' | 'daemon.info' | 'routine.start' | 'routine.end' | 'watchdog.action' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.lease-denied' | 'secrets.lease-expire' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'rotation.unresolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.failed' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
18
18
|
export declare const EVENT_TYPES: readonly EventType[];
|
|
19
19
|
/** Runtime guard for an event kind arriving from outside this process. */
|
package/dist/lib/feed/events.js
CHANGED
|
@@ -19,25 +19,33 @@ import { gzipSync, gunzipSync } from 'node:zlib';
|
|
|
19
19
|
import { ensureLockTarget, withFileLock } from '../fs-atomic.js';
|
|
20
20
|
import { getUserAgentsDir } from '../state.js';
|
|
21
21
|
import { stampProvenance, resetEventProvenanceForTest } from '../event-provenance.js';
|
|
22
|
-
|
|
22
|
+
import { recordSample } from '../perf/spool.js';
|
|
23
|
+
/**
|
|
24
|
+
* Perf warehouse write. SYNCHRONOUS by design (PHNX-3497): `recordSample` is an
|
|
25
|
+
* `appendFileSync` that never opens SQLite (see perf/spool.ts), so calling it
|
|
26
|
+
* directly costs one file append and lands the row before this function returns.
|
|
27
|
+
* It MUST NOT be deferred behind a `void import().then(...)`: the `agent.run`
|
|
28
|
+
* timer ends at the very tail of `agents run`, and the CLI process exits before
|
|
29
|
+
* any deferred microtask runs — so a fire-and-forget spool write was silently
|
|
30
|
+
* lost on every foreground run, leaving the PHNX-3468 startup-phase surface with
|
|
31
|
+
* no data. (Hooks were unaffected only because their shim appends the spool line
|
|
32
|
+
* synchronously in bash.)
|
|
33
|
+
*/
|
|
23
34
|
function recordPerfTiming(payload) {
|
|
24
35
|
try {
|
|
25
36
|
const phases = payload.phases && Object.keys(payload.phases).length > 0 ? payload.phases : undefined;
|
|
26
37
|
const metaJson = phases ? JSON.stringify({ phases }) : undefined;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
metaJson,
|
|
39
|
-
});
|
|
40
|
-
}).catch(() => { });
|
|
38
|
+
recordSample({
|
|
39
|
+
kind: 'perf.timing',
|
|
40
|
+
label: payload.label,
|
|
41
|
+
durationMs: payload.durationMs,
|
|
42
|
+
status: payload.status,
|
|
43
|
+
agent: payload.agent,
|
|
44
|
+
agentVersion: payload.version,
|
|
45
|
+
sessionId: payload.sessionId,
|
|
46
|
+
cwd: payload.cwd,
|
|
47
|
+
metaJson,
|
|
48
|
+
});
|
|
41
49
|
}
|
|
42
50
|
catch {
|
|
43
51
|
// fail soft
|
|
@@ -127,6 +135,7 @@ const EVENT_TYPE_TABLE = {
|
|
|
127
135
|
'mcp.add': true, 'mcp.remove': true, 'mcp.register': true,
|
|
128
136
|
'resource.sync': true,
|
|
129
137
|
'rotation.resolved': true,
|
|
138
|
+
'rotation.unresolved': true,
|
|
130
139
|
'command.start': true, 'command.end': true,
|
|
131
140
|
'perf.timing': true,
|
|
132
141
|
'session.start': true, 'session.end': true,
|
|
@@ -156,7 +165,7 @@ const AUDIT_EVENTS = new Set([
|
|
|
156
165
|
'version.install', 'version.switch', 'version.remove',
|
|
157
166
|
'skill.install', 'skill.remove',
|
|
158
167
|
'mcp.add', 'mcp.remove', 'mcp.register',
|
|
159
|
-
'rotation.resolved',
|
|
168
|
+
'rotation.resolved', 'rotation.unresolved',
|
|
160
169
|
'session.start', 'session.end',
|
|
161
170
|
// Daemon lifecycle (always-on process start/stop/error is audit-relevant).
|
|
162
171
|
// browser.* / computer.action stay info — they are already on the event stream;
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
import { spawnSync } from 'child_process';
|
|
37
37
|
import { isOwnerAlias, readOwnerDest, resolveSendEnvelope, deliverEnvelope } from './channels/send.js';
|
|
38
38
|
import { lookupTransport } from './channels/resolve.js';
|
|
39
|
-
import { forwardOwnerNotifyToPeer } from './channels/owner-forward.js';
|
|
40
39
|
import { registerBuiltinProviders } from './channels/providers/index.js';
|
|
40
|
+
import { sendToOwner } from './notify.js';
|
|
41
41
|
const LEVEL_RANK = { milestone: 0, important: 1 };
|
|
42
42
|
/** Parse a `--level` value; anything unrecognized is a usage error, not a default. */
|
|
43
43
|
export function parseFeedPostLevel(raw) {
|
|
@@ -462,6 +462,10 @@ async function runChannelSink(sink, meta) {
|
|
|
462
462
|
// for a name that is, in fact, registered.
|
|
463
463
|
registerBuiltinProviders();
|
|
464
464
|
const owner = isOwnerAlias(sink.channel);
|
|
465
|
+
if (owner) {
|
|
466
|
+
const result = await sendToOwner(sink.text ?? '', { meta });
|
|
467
|
+
return { name, ok: result.ok, ...(result.error ? { error: result.error } : {}) };
|
|
468
|
+
}
|
|
465
469
|
const resolved = resolveSendEnvelope({
|
|
466
470
|
text: sink.text ?? '',
|
|
467
471
|
channel: owner ? undefined : sink.channel,
|
|
@@ -476,17 +480,6 @@ async function runChannelSink(sink, meta) {
|
|
|
476
480
|
const result = await deliverEnvelope(resolved.envelope, meta);
|
|
477
481
|
if (result.ok)
|
|
478
482
|
return { name, ok: true };
|
|
479
|
-
// The owner sink failed locally. When this box structurally cannot reach the
|
|
480
|
-
// owner (the rush-backed channel is macOS-only, so a headless Linux worker can
|
|
481
|
-
// never ring the phone — PHNX-3303), hand the delivery to a capable fleet peer
|
|
482
|
-
// over SSH rather than stranding the important post. Only the `owner` alias
|
|
483
|
-
// forwards: it resolves the peer's own fleet-synced owner destination, so a
|
|
484
|
-
// non-owner channel sink with an explicit recipient stays local.
|
|
485
|
-
if (owner) {
|
|
486
|
-
const forwarded = await forwardOwnerNotifyToPeer(sink.text ?? '', resolved.envelope.channel, meta);
|
|
487
|
-
if (forwarded?.ok)
|
|
488
|
-
return { name, ok: true };
|
|
489
|
-
}
|
|
490
483
|
return { name, ok: false, error: result.error };
|
|
491
484
|
}
|
|
492
485
|
/**
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The delegate behind the `gh` PATH shim (`agents __gh --real-gh <path> -- <argv>`).
|
|
3
|
+
*
|
|
4
|
+
* Agents are statically trained to type `gh pr checks …`; the shim intercepts that
|
|
5
|
+
* habit and routes it here so the rate-limit-prone read runs over REST instead of
|
|
6
|
+
* GraphQL — no new command for an agent to remember. This is the Tier-1 scope:
|
|
7
|
+
* only `gh pr checks` is handled; every other invocation execs the real gh
|
|
8
|
+
* byte-for-byte (the shim already passes non-`pr checks` verbs straight to real gh,
|
|
9
|
+
* and this file passes through defensively for anything it cannot cleanly serve).
|
|
10
|
+
*
|
|
11
|
+
* Two switch modes (see the PHNX-3501 plan):
|
|
12
|
+
* - `--watch` → EAGER REST: own the poll loop, re-anchored to the live head SHA
|
|
13
|
+
* each tick, so a superseded run's red can never be reported (PHNX-3042).
|
|
14
|
+
* - one-shot → LAZY: run real gh first; only translate to REST when it fails with
|
|
15
|
+
* the exact GraphQL rate-limit signal. An over-budget GraphQL call is rejected
|
|
16
|
+
* at 0 points, so this is nearly free and never reproduces gh's happy path.
|
|
17
|
+
*
|
|
18
|
+
* Fail-open is the rule: anything not cleanly resolvable (no PR number/URL, an
|
|
19
|
+
* unmappable request) execs real gh rather than returning wrong data.
|
|
20
|
+
*/
|
|
21
|
+
import { type RollupItem } from './rest.js';
|
|
22
|
+
interface Parsed {
|
|
23
|
+
realGh: string;
|
|
24
|
+
ghArgs: string[];
|
|
25
|
+
}
|
|
26
|
+
/** Split `--real-gh <path> -- <gh argv>`; defaults realGh to bare `gh`. */
|
|
27
|
+
export declare function parseDelegateArgs(argv: string[]): Parsed;
|
|
28
|
+
export interface Target {
|
|
29
|
+
repo: string;
|
|
30
|
+
number: number;
|
|
31
|
+
}
|
|
32
|
+
/** owner/repo from a git remote URL, or null. */
|
|
33
|
+
export declare function repoFromRemote(remoteUrl: string): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve `{repo, number}` from the `gh pr checks` argv + cwd, over git/REST only.
|
|
36
|
+
* Returns null (→ caller passes through to real gh) when it can't be resolved
|
|
37
|
+
* cleanly, e.g. no number and no open PR for the current branch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveTarget(ghArgs: string[], cwd: string, realGh: string): Promise<Target | null>;
|
|
40
|
+
/** Render one rollup as `gh pr checks`-ish lines (or JSON when asked). */
|
|
41
|
+
export declare function renderRollup(input: RollupItem[], json: boolean): string;
|
|
42
|
+
/**
|
|
43
|
+
* True once CI has settled for this SHA.
|
|
44
|
+
*
|
|
45
|
+
* Check-suites only disambiguate an EMPTY rollup: no checks + no pending suites is
|
|
46
|
+
* "genuinely none" (settled); no checks + a pending suite is "not registered yet"
|
|
47
|
+
* (wait). Once real checks exist we decide on THEM alone and ignore suites — some
|
|
48
|
+
* App integrations (claude/cursor reviewers) register a suite that stays `queued`
|
|
49
|
+
* forever and never posts a run, exactly what `gh pr checks` also ignores.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isSettled(rollup: RollupItem[], pendingSuites: number): boolean;
|
|
52
|
+
/** Eager REST watch: poll to a terminal state, re-anchored to the live head SHA. */
|
|
53
|
+
export declare function watchChecks(target: Target, json: boolean, realGh: string): Promise<number>;
|
|
54
|
+
/** Lazy one-shot: real gh first; translate to REST only on the exact rate-limit signal. */
|
|
55
|
+
export declare function checksOnce(target: Target | null, json: boolean, realGh: string, ghArgs: string[]): Promise<number>;
|
|
56
|
+
/** Entry point for the `__gh` early branch in index.ts. */
|
|
57
|
+
export declare function runGhOverload(argv: string[], cwd?: string): Promise<number>;
|
|
58
|
+
export {};
|