@phnx-labs/agents-cli 1.22.42 → 1.22.43
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 +72 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +25 -7
- package/dist/cli/command-registry.d.ts +0 -3
- package/dist/cli/command-registry.js +0 -8
- package/dist/commands/alias.d.ts +7 -4
- package/dist/commands/alias.js +33 -8
- package/dist/commands/auth.js +7 -3
- package/dist/commands/computer.js +20 -6
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +20 -5
- package/dist/commands/exec.js +3 -2
- package/dist/commands/feed.js +1 -30
- package/dist/commands/harness.js +3 -3
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/org.js +9 -8
- package/dist/commands/profiles.js +2 -2
- package/dist/commands/routines.js +43 -25
- package/dist/commands/sessions-picker.js +124 -122
- package/dist/commands/setup.js +5 -1
- package/dist/commands/snapshot.d.ts +1 -1
- package/dist/commands/snapshot.js +4 -4
- package/dist/commands/status.d.ts +4 -2
- package/dist/commands/status.js +10 -7
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +29 -2
- package/dist/commands/view.js +6 -0
- package/dist/commands/webhook.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +5 -3
- package/dist/lib/accounting/rotate.js +36 -7
- package/dist/lib/accounting/usage.d.ts +18 -4
- package/dist/lib/accounting/usage.js +138 -27
- package/dist/lib/agent-spec/agents.d.ts +7 -7
- package/dist/lib/agent-spec/agents.js +15 -15
- package/dist/lib/cloud/rush.d.ts +0 -15
- package/dist/lib/cloud/rush.js +0 -35
- package/dist/lib/crabbox/runtimes.d.ts +6 -2
- package/dist/lib/crabbox/runtimes.js +21 -5
- package/dist/lib/daemon/daemon.js +86 -58
- package/dist/lib/daemon/runner.d.ts +18 -1
- package/dist/lib/daemon/runner.js +105 -11
- package/dist/lib/devices/doctor-findings.js +1 -1
- package/dist/lib/devices/health-report.js +1 -1
- package/dist/lib/drift-sync.d.ts +1 -1
- package/dist/lib/drift-sync.js +2 -2
- package/dist/lib/drift.d.ts +1 -1
- package/dist/lib/exec.js +13 -3
- package/dist/lib/format.d.ts +9 -0
- package/dist/lib/format.js +16 -0
- package/dist/lib/github/pr-mergeable.d.ts +49 -0
- package/dist/lib/github/pr-mergeable.js +157 -0
- package/dist/lib/github/pr-verdict.d.ts +67 -0
- package/dist/lib/github/pr-verdict.js +72 -0
- package/dist/lib/harness/adapter.d.ts +6 -5
- package/dist/lib/hosts/passthrough.d.ts +10 -0
- package/dist/lib/hosts/passthrough.js +28 -6
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/installations/shims.js +1 -1
- package/dist/lib/installations/versions.d.ts +40 -0
- package/dist/lib/installations/versions.js +57 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.js +19 -8
- package/dist/lib/monitors/config.d.ts +2 -2
- package/dist/lib/monitors/config.js +3 -2
- package/dist/lib/origin-machine.d.ts +18 -0
- package/dist/lib/origin-machine.js +34 -0
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/perf/spool.js +1 -1
- package/dist/lib/profiles.d.ts +7 -0
- package/dist/lib/profiles.js +26 -1
- package/dist/lib/sandbox.d.ts +24 -0
- package/dist/lib/sandbox.js +101 -0
- package/dist/lib/scheduling/routines.d.ts +18 -9
- package/dist/lib/scheduling/routines.js +16 -8
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +9 -1
- package/dist/lib/secrets/index.js +1 -1
- package/dist/lib/secrets/list-filter.js +1 -1
- package/dist/lib/service-manifest.d.ts +20 -0
- package/dist/lib/service-manifest.js +34 -0
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.js +1 -1
- package/dist/lib/session/fork.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -1
- package/dist/lib/session/origin-machine.d.ts +1 -18
- package/dist/lib/session/origin-machine.js +4 -34
- package/dist/lib/session/relative-time.d.ts +1 -44
- package/dist/lib/session/relative-time.js +4 -120
- package/dist/lib/session/remote-list.js +1 -1
- package/dist/lib/session/remote.js +2 -2
- package/dist/lib/session/short-id.d.ts +1 -17
- package/dist/lib/session/short-id.js +4 -20
- package/dist/lib/session/width.d.ts +1 -29
- package/dist/lib/session/width.js +4 -101
- package/dist/lib/snapshot.d.ts +2 -2
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +4 -1
- package/dist/lib/startup/command-registry.js +11 -4
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/text/relative-time.d.ts +44 -0
- package/dist/lib/text/relative-time.js +120 -0
- package/dist/lib/text/short-id.d.ts +17 -0
- package/dist/lib/text/short-id.js +20 -0
- package/dist/lib/text/width.d.ts +29 -0
- package/dist/lib/text/width.js +101 -0
- package/dist/lib/triggers/handlers.d.ts +7 -0
- package/dist/lib/triggers/handlers.js +8 -0
- package/dist/lib/triggers/webhook.d.ts +1 -0
- package/dist/lib/triggers/webhook.js +2 -1
- package/dist/lib/view-types.d.ts +4 -0
- package/package.json +1 -1
- package/dist/commands/tickets.d.ts +0 -2
- package/dist/commands/tickets.js +0 -43
- package/dist/lib/observe-aliases.d.ts +0 -30
- package/dist/lib/observe-aliases.js +0 -38
- package/dist/lib/tickets/list.d.ts +0 -53
- package/dist/lib/tickets/list.js +0 -153
package/dist/lib/exec.js
CHANGED
|
@@ -37,6 +37,8 @@ import { applyAddDirs } from './add-dir.js';
|
|
|
37
37
|
import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
|
|
38
38
|
import { resolveHarnessAdapter, stripForeignConfigDir } from './harness/index.js';
|
|
39
39
|
import { resolveConfigVersion } from './harness/exec-config-version.js';
|
|
40
|
+
import { getAccountInfo } from './agents.js';
|
|
41
|
+
import { getUsageLookupKey, noteClaudeSessionLimit, parseClaudeSessionLimitReset } from './accounting/usage.js';
|
|
40
42
|
/**
|
|
41
43
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
42
44
|
*
|
|
@@ -2083,12 +2085,20 @@ export async function runWithFallback(options) {
|
|
|
2083
2085
|
}
|
|
2084
2086
|
throw err;
|
|
2085
2087
|
}
|
|
2086
|
-
|
|
2088
|
+
const output = `${result.stderr}\n${result.stdout}`;
|
|
2089
|
+
const sessionLimitReset = agent === 'claude' ? parseClaudeSessionLimitReset(output) : null;
|
|
2090
|
+
if (sessionLimitReset && version) {
|
|
2091
|
+
const account = await getAccountInfo(agent, getVersionHomePath(agent, version));
|
|
2092
|
+
const usageKey = getUsageLookupKey(account);
|
|
2093
|
+
if (usageKey)
|
|
2094
|
+
noteClaudeSessionLimit(usageKey, sessionLimitReset);
|
|
2095
|
+
}
|
|
2096
|
+
if (result.exitCode === 0 && !sessionLimitReset)
|
|
2087
2097
|
return 0;
|
|
2088
2098
|
const isLast = i === chain.length - 1;
|
|
2089
2099
|
if (isLast)
|
|
2090
|
-
return result.exitCode;
|
|
2091
|
-
if (!detectRateLimit(result.stderr) && !detectRateLimit(result.stdout)) {
|
|
2100
|
+
return result.exitCode || 1;
|
|
2101
|
+
if (!sessionLimitReset && !detectRateLimit(result.stderr) && !detectRateLimit(result.stdout)) {
|
|
2092
2102
|
return result.exitCode;
|
|
2093
2103
|
}
|
|
2094
2104
|
const next = chain[i + 1];
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -27,6 +27,15 @@ export declare function formatDie(msg: string, opts?: DieOptions): {
|
|
|
27
27
|
* keep the original red-stderr behavior.
|
|
28
28
|
*/
|
|
29
29
|
export declare function die(msg: string, code?: number, opts?: DieOptions): never;
|
|
30
|
+
/**
|
|
31
|
+
* Await a command action and turn a thrown Error into a clean `die(message)`
|
|
32
|
+
* instead of Node's raw stack dump — bootstrap's parseAsync catch deliberately
|
|
33
|
+
* rethrows plain Errors as engineering bugs, so a command whose helpers throw
|
|
34
|
+
* user-actionable errors (auth, org) wraps its `.action(...)` call site with
|
|
35
|
+
* this. The helpers keep throwing so tests can assert on them. Pass `json`
|
|
36
|
+
* so a `--json` caller still gets the structured `{"error"}` payload.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runOrDie(fn: () => void | Promise<void>, opts?: DieOptions): Promise<void>;
|
|
30
39
|
/**
|
|
31
40
|
* `die()` with a structured friction event attached. Use this at CLI error
|
|
32
41
|
* chokepoints so the nightly routine can classify and rank recurring failures
|
package/dist/lib/format.js
CHANGED
|
@@ -44,6 +44,22 @@ export function die(msg, code = 1, opts = {}) {
|
|
|
44
44
|
console.error(text);
|
|
45
45
|
process.exit(code);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Await a command action and turn a thrown Error into a clean `die(message)`
|
|
49
|
+
* instead of Node's raw stack dump — bootstrap's parseAsync catch deliberately
|
|
50
|
+
* rethrows plain Errors as engineering bugs, so a command whose helpers throw
|
|
51
|
+
* user-actionable errors (auth, org) wraps its `.action(...)` call site with
|
|
52
|
+
* this. The helpers keep throwing so tests can assert on them. Pass `json`
|
|
53
|
+
* so a `--json` caller still gets the structured `{"error"}` payload.
|
|
54
|
+
*/
|
|
55
|
+
export async function runOrDie(fn, opts = {}) {
|
|
56
|
+
try {
|
|
57
|
+
await fn();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
die(err instanceof Error ? err.message : String(err), 1, opts);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
47
63
|
/**
|
|
48
64
|
* `die()` with a structured friction event attached. Use this at CLI error
|
|
49
65
|
* chokepoints so the nightly routine can classify and rank recurring failures
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List this user's mergeable PRs across registered project repos.
|
|
3
|
+
*
|
|
4
|
+
* The built-in `pr-merge-on-green` monitor used to run `gh pr list --author @me`
|
|
5
|
+
* with no `--repo`. `gh` then infers the repository from the working directory,
|
|
6
|
+
* and the daemon's cwd is not a git repo, so every poll returned empty.
|
|
7
|
+
*
|
|
8
|
+
* This helper:
|
|
9
|
+
* 1. Collects GitHub slugs from `~/.agents/projects/*.yaml` (`repo` / `repos[].slug`).
|
|
10
|
+
* 2. Canonicalizes each slug (`gh repo view` — `phnx-labs/agents-cli` lists
|
|
11
|
+
* nothing; the live name is `phnx-labs/agi-cli`).
|
|
12
|
+
* 3. Runs `gh pr list --repo <slug> --author @me` (cwd-independent).
|
|
13
|
+
* 4. Keeps CI-green PRs with a merge-guard verdict (formal APPROVED review
|
|
14
|
+
* OR an APPROVE comment on THIS PR).
|
|
15
|
+
*
|
|
16
|
+
* Stdout is a single line of `owner/repo#n` refs (or empty). Empty is a silent
|
|
17
|
+
* observation under `condition.mode: every` — do not write to stderr on the
|
|
18
|
+
* no-match path, or command.ts will treat stderr as the observation and fire.
|
|
19
|
+
*/
|
|
20
|
+
import { type ProjectDef } from '../projects.js';
|
|
21
|
+
import { type MergeablePrInput, type StatusCheck } from './pr-verdict.js';
|
|
22
|
+
export type GhExec = (args: string[]) => Promise<string>;
|
|
23
|
+
/** Default runner: `gh` with a 30s timeout. Non-zero exit throws. */
|
|
24
|
+
export declare function ghExec(args: string[]): Promise<string>;
|
|
25
|
+
/** Unique GitHub slugs declared on project definitions. */
|
|
26
|
+
export declare function projectRepoSlugs(defs: readonly ProjectDef[]): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a slug to GitHub's current `nameWithOwner`. A renamed repo
|
|
29
|
+
* (`phnx-labs/agents-cli` → `phnx-labs/agi-cli`) lists zero PRs under the old
|
|
30
|
+
* name; `gh repo view` returns the live one. On error, keep the input slug.
|
|
31
|
+
*/
|
|
32
|
+
export declare function canonicalizeRepo(slug: string, gh: GhExec): Promise<string>;
|
|
33
|
+
interface ListedPr {
|
|
34
|
+
number: number;
|
|
35
|
+
reviewDecision?: string | null;
|
|
36
|
+
statusCheckRollup?: StatusCheck[] | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Given PRs already listed for one repo, attach verdict payloads only for the
|
|
40
|
+
* CI-green ones that lack a formal APPROVED reviewDecision, then select.
|
|
41
|
+
*/
|
|
42
|
+
export declare function selectListedMergeable(repo: string, listed: readonly ListedPr[], gh: GhExec): Promise<MergeablePrInput[]>;
|
|
43
|
+
/** List mergeable `owner/repo#n` refs, space-separated. Empty string if none. */
|
|
44
|
+
export declare function listMergeableRefs(opts?: {
|
|
45
|
+
gh?: GhExec;
|
|
46
|
+
defs?: ProjectDef[];
|
|
47
|
+
repos?: string[];
|
|
48
|
+
}): Promise<string>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List this user's mergeable PRs across registered project repos.
|
|
3
|
+
*
|
|
4
|
+
* The built-in `pr-merge-on-green` monitor used to run `gh pr list --author @me`
|
|
5
|
+
* with no `--repo`. `gh` then infers the repository from the working directory,
|
|
6
|
+
* and the daemon's cwd is not a git repo, so every poll returned empty.
|
|
7
|
+
*
|
|
8
|
+
* This helper:
|
|
9
|
+
* 1. Collects GitHub slugs from `~/.agents/projects/*.yaml` (`repo` / `repos[].slug`).
|
|
10
|
+
* 2. Canonicalizes each slug (`gh repo view` — `phnx-labs/agents-cli` lists
|
|
11
|
+
* nothing; the live name is `phnx-labs/agi-cli`).
|
|
12
|
+
* 3. Runs `gh pr list --repo <slug> --author @me` (cwd-independent).
|
|
13
|
+
* 4. Keeps CI-green PRs with a merge-guard verdict (formal APPROVED review
|
|
14
|
+
* OR an APPROVE comment on THIS PR).
|
|
15
|
+
*
|
|
16
|
+
* Stdout is a single line of `owner/repo#n` refs (or empty). Empty is a silent
|
|
17
|
+
* observation under `condition.mode: every` — do not write to stderr on the
|
|
18
|
+
* no-match path, or command.ts will treat stderr as the observation and fire.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from 'child_process';
|
|
21
|
+
import { promisify } from 'util';
|
|
22
|
+
import { listProjectDefs } from '../projects.js';
|
|
23
|
+
import { formatMergeableRef, hasApproveVerdict, isCiGreen, } from './pr-verdict.js';
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
/**
|
|
26
|
+
* `gh --json` is not color-safe when this fleet exports FORCE_COLOR /
|
|
27
|
+
* CLICOLOR_FORCE: gh paints the payload and JSON.parse / jq die on the ANSI
|
|
28
|
+
* prefix (live miss on the sibling poll script). Strip the force vars and
|
|
29
|
+
* pin GH_NO_COLOR for every gh spawn.
|
|
30
|
+
*/
|
|
31
|
+
function ghEnv() {
|
|
32
|
+
const env = { ...process.env };
|
|
33
|
+
env.CLICOLOR = '0';
|
|
34
|
+
env.NO_COLOR = '1';
|
|
35
|
+
env.GH_NO_COLOR = '1';
|
|
36
|
+
env.GH_PAGER = 'cat';
|
|
37
|
+
delete env.CLICOLOR_FORCE;
|
|
38
|
+
delete env.FORCE_COLOR;
|
|
39
|
+
delete env.GH_FORCE_TTY;
|
|
40
|
+
return env;
|
|
41
|
+
}
|
|
42
|
+
/** Default runner: `gh` with a 30s timeout. Non-zero exit throws. */
|
|
43
|
+
export async function ghExec(args) {
|
|
44
|
+
const { stdout } = await execFileAsync('gh', args, {
|
|
45
|
+
timeout: 30_000,
|
|
46
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
env: ghEnv(),
|
|
49
|
+
});
|
|
50
|
+
return String(stdout ?? '');
|
|
51
|
+
}
|
|
52
|
+
/** Unique GitHub slugs declared on project definitions. */
|
|
53
|
+
export function projectRepoSlugs(defs) {
|
|
54
|
+
const slugs = new Set();
|
|
55
|
+
for (const d of defs) {
|
|
56
|
+
if (d.repo)
|
|
57
|
+
slugs.add(d.repo);
|
|
58
|
+
for (const r of d.repos ?? []) {
|
|
59
|
+
if (r.slug)
|
|
60
|
+
slugs.add(r.slug);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...slugs].sort();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a slug to GitHub's current `nameWithOwner`. A renamed repo
|
|
67
|
+
* (`phnx-labs/agents-cli` → `phnx-labs/agi-cli`) lists zero PRs under the old
|
|
68
|
+
* name; `gh repo view` returns the live one. On error, keep the input slug.
|
|
69
|
+
*/
|
|
70
|
+
export async function canonicalizeRepo(slug, gh) {
|
|
71
|
+
try {
|
|
72
|
+
const out = (await gh([
|
|
73
|
+
'repo', 'view', slug, '--json', 'nameWithOwner', '--jq', '.nameWithOwner',
|
|
74
|
+
])).trim();
|
|
75
|
+
return out || slug;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return slug;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function fetchVerdict(repo, number, gh) {
|
|
82
|
+
const [reviewsRaw, commentsRaw] = await Promise.all([
|
|
83
|
+
gh(['api', `repos/${repo}/pulls/${number}/reviews`, '--cache', '60s']),
|
|
84
|
+
gh(['api', `repos/${repo}/issues/${number}/comments`, '--cache', '60s']),
|
|
85
|
+
]);
|
|
86
|
+
const reviews = JSON.parse(reviewsRaw);
|
|
87
|
+
const comments = JSON.parse(commentsRaw);
|
|
88
|
+
return {
|
|
89
|
+
reviews: Array.isArray(reviews) ? reviews : [],
|
|
90
|
+
comments: Array.isArray(comments) ? comments : [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Given PRs already listed for one repo, attach verdict payloads only for the
|
|
95
|
+
* CI-green ones that lack a formal APPROVED reviewDecision, then select.
|
|
96
|
+
*/
|
|
97
|
+
export async function selectListedMergeable(repo, listed, gh) {
|
|
98
|
+
const candidates = [];
|
|
99
|
+
for (const row of listed) {
|
|
100
|
+
if (!isCiGreen(row.statusCheckRollup))
|
|
101
|
+
continue;
|
|
102
|
+
const base = {
|
|
103
|
+
number: row.number,
|
|
104
|
+
repo,
|
|
105
|
+
reviewDecision: row.reviewDecision ?? '',
|
|
106
|
+
statusCheckRollup: row.statusCheckRollup,
|
|
107
|
+
reviews: [],
|
|
108
|
+
comments: [],
|
|
109
|
+
};
|
|
110
|
+
if (base.reviewDecision === 'APPROVED') {
|
|
111
|
+
candidates.push(base);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const extra = await fetchVerdict(repo, row.number, gh);
|
|
116
|
+
candidates.push({ ...base, ...extra });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// A single PR's reviews/comments probe failed — skip it, keep the rest.
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return candidates.filter((pr) => pr.reviewDecision === 'APPROVED' || hasApproveVerdict(pr.reviews, pr.comments));
|
|
124
|
+
}
|
|
125
|
+
/** List mergeable `owner/repo#n` refs, space-separated. Empty string if none. */
|
|
126
|
+
export async function listMergeableRefs(opts) {
|
|
127
|
+
const gh = opts?.gh ?? ghExec;
|
|
128
|
+
const slugs = opts?.repos ?? projectRepoSlugs(opts?.defs ?? listProjectDefs());
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
const refs = [];
|
|
131
|
+
for (const raw of slugs) {
|
|
132
|
+
const repo = await canonicalizeRepo(raw, gh);
|
|
133
|
+
if (seen.has(repo))
|
|
134
|
+
continue;
|
|
135
|
+
seen.add(repo);
|
|
136
|
+
let listed = [];
|
|
137
|
+
try {
|
|
138
|
+
const rawList = await gh([
|
|
139
|
+
'pr', 'list',
|
|
140
|
+
'--repo', repo,
|
|
141
|
+
'--author', '@me',
|
|
142
|
+
'--state', 'open',
|
|
143
|
+
'--limit', '50',
|
|
144
|
+
'--json', 'number,reviewDecision,statusCheckRollup',
|
|
145
|
+
]);
|
|
146
|
+
const parsed = JSON.parse(rawList);
|
|
147
|
+
listed = Array.isArray(parsed) ? parsed : [];
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const mergeable = await selectListedMergeable(repo, listed, gh);
|
|
153
|
+
for (const pr of mergeable)
|
|
154
|
+
refs.push(formatMergeableRef(pr));
|
|
155
|
+
}
|
|
156
|
+
return refs.join(' ');
|
|
157
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-author PR approval + CI-green checks for merge-on-green.
|
|
3
|
+
*
|
|
4
|
+
* Verdict rules are copied from merge-guard.sh (the PreToolUse hook in
|
|
5
|
+
* phnx-labs/.agents-system `rules/subrules/gh-merge-guard/merge-guard.sh`).
|
|
6
|
+
* That hook accepts EITHER a formal GitHub `APPROVED` review OR an APPROVE
|
|
7
|
+
* verdict comment on THIS PR, and rejects a comment that only cites another
|
|
8
|
+
* PR ("carried from #N" / "APPROVE on #N") — the #2736 laundering pattern.
|
|
9
|
+
*
|
|
10
|
+
* Do not invent a third rule. If the hook's regexes change, change these in
|
|
11
|
+
* the same delivery.
|
|
12
|
+
*/
|
|
13
|
+
/** A GitHub pull-request review, shaped after `GET .../pulls/{n}/reviews`. */
|
|
14
|
+
export interface PrReview {
|
|
15
|
+
state?: string;
|
|
16
|
+
}
|
|
17
|
+
/** An issue comment on a PR, shaped after `GET .../issues/{n}/comments`. */
|
|
18
|
+
export interface PrComment {
|
|
19
|
+
body?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One status-check rollup item from `gh pr list --json statusCheckRollup`.
|
|
23
|
+
* `gh` exposes conclusion/state/status under those names; empty fields are
|
|
24
|
+
* pending/in-progress.
|
|
25
|
+
*/
|
|
26
|
+
export interface StatusCheck {
|
|
27
|
+
conclusion?: string;
|
|
28
|
+
state?: string;
|
|
29
|
+
status?: string;
|
|
30
|
+
}
|
|
31
|
+
/** One open PR with the fields the mergeable selector reads. */
|
|
32
|
+
export interface MergeablePrInput {
|
|
33
|
+
number: number;
|
|
34
|
+
/** `owner/repo`. Required so a daemon with no git cwd can still merge. */
|
|
35
|
+
repo: string;
|
|
36
|
+
reviewDecision?: string | null;
|
|
37
|
+
statusCheckRollup?: StatusCheck[] | null;
|
|
38
|
+
reviews?: PrReview[] | null;
|
|
39
|
+
comments?: PrComment[] | null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True when a non-author verdict exists ON this PR.
|
|
43
|
+
*
|
|
44
|
+
* Mirrors merge-guard.sh: a GitHub review with `state == "APPROVED"`, or an
|
|
45
|
+
* issue comment whose body contains `\bAPPROVE\b` and is not a carried-from
|
|
46
|
+
* citation (`\bcarried\s+(?:over\s+)?from\b` or `\bAPPROVE\s+(?:on|from)\s+#\d+`).
|
|
47
|
+
*/
|
|
48
|
+
export declare function hasApproveVerdict(reviews: readonly PrReview[] | null | undefined, comments: readonly PrComment[] | null | undefined): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* True when every check is a terminal success-class conclusion.
|
|
51
|
+
*
|
|
52
|
+
* Matches the built-in monitor's original jq: SUCCESS / NEUTRAL / SKIPPED
|
|
53
|
+
* (ascii-uppercased conclusion, else state, else status). An empty rollup
|
|
54
|
+
* is green — jq `all([])` is true, and a PR with no checks is mergeable
|
|
55
|
+
* from this filter's point of view.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isCiGreen(rollup: readonly StatusCheck[] | null | undefined): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Keep PRs that are CI-green AND non-author-approved.
|
|
60
|
+
*
|
|
61
|
+
* `reviewDecision == "APPROVED"` is sufficient (a formal review). When that
|
|
62
|
+
* field is empty — the fleet convention: reviewers post an APPROVE *comment*
|
|
63
|
+
* rather than a GitHub review — fall through to {@link hasApproveVerdict}.
|
|
64
|
+
*/
|
|
65
|
+
export declare function selectMergeablePrs(prs: readonly MergeablePrInput[]): MergeablePrInput[];
|
|
66
|
+
/** `owner/repo#n` — the poll observation the merge action can parse without a cwd. */
|
|
67
|
+
export declare function formatMergeableRef(pr: Pick<MergeablePrInput, 'repo' | 'number'>): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-author PR approval + CI-green checks for merge-on-green.
|
|
3
|
+
*
|
|
4
|
+
* Verdict rules are copied from merge-guard.sh (the PreToolUse hook in
|
|
5
|
+
* phnx-labs/.agents-system `rules/subrules/gh-merge-guard/merge-guard.sh`).
|
|
6
|
+
* That hook accepts EITHER a formal GitHub `APPROVED` review OR an APPROVE
|
|
7
|
+
* verdict comment on THIS PR, and rejects a comment that only cites another
|
|
8
|
+
* PR ("carried from #N" / "APPROVE on #N") — the #2736 laundering pattern.
|
|
9
|
+
*
|
|
10
|
+
* Do not invent a third rule. If the hook's regexes change, change these in
|
|
11
|
+
* the same delivery.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* True when a non-author verdict exists ON this PR.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors merge-guard.sh: a GitHub review with `state == "APPROVED"`, or an
|
|
17
|
+
* issue comment whose body contains `\bAPPROVE\b` and is not a carried-from
|
|
18
|
+
* citation (`\bcarried\s+(?:over\s+)?from\b` or `\bAPPROVE\s+(?:on|from)\s+#\d+`).
|
|
19
|
+
*/
|
|
20
|
+
export function hasApproveVerdict(reviews, comments) {
|
|
21
|
+
if (Array.isArray(reviews) && reviews.some((r) => r?.state === 'APPROVED')) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (!Array.isArray(comments))
|
|
25
|
+
return false;
|
|
26
|
+
for (const c of comments) {
|
|
27
|
+
const body = c?.body ?? '';
|
|
28
|
+
if (!/\bAPPROVE\b/.test(body))
|
|
29
|
+
continue;
|
|
30
|
+
// A verdict that only points at another PR is laundering, not review.
|
|
31
|
+
if (/\bcarried\s+(?:over\s+)?from\b|\bAPPROVE\s+(?:on|from)\s+#\d+/.test(body)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* True when every check is a terminal success-class conclusion.
|
|
40
|
+
*
|
|
41
|
+
* Matches the built-in monitor's original jq: SUCCESS / NEUTRAL / SKIPPED
|
|
42
|
+
* (ascii-uppercased conclusion, else state, else status). An empty rollup
|
|
43
|
+
* is green — jq `all([])` is true, and a PR with no checks is mergeable
|
|
44
|
+
* from this filter's point of view.
|
|
45
|
+
*/
|
|
46
|
+
export function isCiGreen(rollup) {
|
|
47
|
+
const items = rollup ?? [];
|
|
48
|
+
return items.every((c) => {
|
|
49
|
+
const v = (c.conclusion || c.state || c.status || '').toUpperCase();
|
|
50
|
+
return v === 'SUCCESS' || v === 'NEUTRAL' || v === 'SKIPPED';
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Keep PRs that are CI-green AND non-author-approved.
|
|
55
|
+
*
|
|
56
|
+
* `reviewDecision == "APPROVED"` is sufficient (a formal review). When that
|
|
57
|
+
* field is empty — the fleet convention: reviewers post an APPROVE *comment*
|
|
58
|
+
* rather than a GitHub review — fall through to {@link hasApproveVerdict}.
|
|
59
|
+
*/
|
|
60
|
+
export function selectMergeablePrs(prs) {
|
|
61
|
+
return prs.filter((pr) => {
|
|
62
|
+
if (!isCiGreen(pr.statusCheckRollup))
|
|
63
|
+
return false;
|
|
64
|
+
if (pr.reviewDecision === 'APPROVED')
|
|
65
|
+
return true;
|
|
66
|
+
return hasApproveVerdict(pr.reviews, pr.comments);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** `owner/repo#n` — the poll observation the merge action can parse without a cwd. */
|
|
70
|
+
export function formatMergeableRef(pr) {
|
|
71
|
+
return `${pr.repo}#${pr.number}`;
|
|
72
|
+
}
|
|
@@ -73,9 +73,10 @@ export interface ExecLaunchArgsCtx {
|
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* Context for the routine (daemon-job) launch-arg quirks (mapping B, runner.ts
|
|
76
|
-
* side). This idiom mutates a
|
|
77
|
-
* distinct from buildExecCommand's declarative modeFlags —
|
|
78
|
-
* method on the same adapter, one source of truth per
|
|
76
|
+
* side). This idiom mutates a token array baked from AGENT_COMMANDS
|
|
77
|
+
* (bakeRoutineArgv), distinct from buildExecCommand's declarative modeFlags —
|
|
78
|
+
* so it is a separate method on the same adapter, one source of truth per
|
|
79
|
+
* harness across both.
|
|
79
80
|
*/
|
|
80
81
|
export interface RoutineLaunchCtx {
|
|
81
82
|
/** normalizeMode(config.mode) — the canonicalized mode. */
|
|
@@ -131,8 +132,8 @@ export interface HarnessAdapter {
|
|
|
131
132
|
execModeArgs?(ctx: ExecLaunchArgsCtx): string[] | undefined;
|
|
132
133
|
/**
|
|
133
134
|
* This harness's routine (daemon-job) launch-arg quirks for buildJobCommand
|
|
134
|
-
* (runner.ts). Mutates the
|
|
135
|
-
*
|
|
135
|
+
* (runner.ts). Mutates the bakeRoutineArgv token array in place, exactly as
|
|
136
|
+
* the old per-agent arm did; runner appends model/reasoning flags after.
|
|
136
137
|
* Omitted for a harness with no routine launch quirks.
|
|
137
138
|
*/
|
|
138
139
|
routineModeArgs?(cmd: string[], ctx: RoutineLaunchCtx): void;
|
|
@@ -48,6 +48,16 @@ export declare const REMOTE_PASSTHROUGH: Record<string, RemoteSpec>;
|
|
|
48
48
|
* these to {@link REMOTE_PASSTHROUGH}.
|
|
49
49
|
*/
|
|
50
50
|
export declare const OWN_HOST_COMMANDS: Set<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Argv forwarded over SSH for a `--device` passthrough.
|
|
53
|
+
*
|
|
54
|
+
* `sync`'s umbrella spec appends `--yes` when there is no TTY so a remote
|
|
55
|
+
* reconcile does not hang on a picker. `sync status` is inspect-only unless the
|
|
56
|
+
* caller typed `--yes` themselves: `--yes` on that command is reconcile, not
|
|
57
|
+
* "don't prompt". Nesting it under `sync` must not inherit the umbrella flag
|
|
58
|
+
* (RUSH-2864).
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildPassthroughForwardedArgs(command: string, allArgs: string[], interactive: boolean): string[];
|
|
51
61
|
/** Injectable dependencies for {@link runFleetPassthrough} — used by tests. */
|
|
52
62
|
export interface FleetPassthroughOptions {
|
|
53
63
|
/** Override the device registry loader (tests). */
|
|
@@ -52,11 +52,10 @@ export { flagValue, hasHostRoutingFlag } from './routing-flag.js';
|
|
|
52
52
|
* `add`/`use`/`list`, and none) and were removed.
|
|
53
53
|
*/
|
|
54
54
|
export const REMOTE_PASSTHROUGH = {
|
|
55
|
-
//
|
|
55
|
+
// inspect
|
|
56
56
|
view: {},
|
|
57
57
|
inspect: {},
|
|
58
58
|
doctor: {},
|
|
59
|
-
status: {},
|
|
60
59
|
check: {},
|
|
61
60
|
list: {},
|
|
62
61
|
usage: {},
|
|
@@ -81,7 +80,6 @@ export const REMOTE_PASSTHROUGH = {
|
|
|
81
80
|
workflows: {},
|
|
82
81
|
models: {},
|
|
83
82
|
defaults: {},
|
|
84
|
-
alias: {},
|
|
85
83
|
// Installations are per-machine, so updating one on a peer means running it
|
|
86
84
|
// there — the same local/remote shape `add` would need.
|
|
87
85
|
update: {},
|
|
@@ -137,6 +135,32 @@ const STRIP_SPECS = [
|
|
|
137
135
|
{ long: 'hosts', takesValue: true },
|
|
138
136
|
{ long: 'devices', takesValue: true },
|
|
139
137
|
];
|
|
138
|
+
/** First non-flag token after `group` in argv (the subcommand, robust to leading flags). */
|
|
139
|
+
function firstSubcommand(allArgs, group) {
|
|
140
|
+
const idx = allArgs.indexOf(group);
|
|
141
|
+
return idx >= 0 ? allArgs.slice(idx + 1).find((a) => !a.startsWith('-')) : undefined;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Argv forwarded over SSH for a `--device` passthrough.
|
|
145
|
+
*
|
|
146
|
+
* `sync`'s umbrella spec appends `--yes` when there is no TTY so a remote
|
|
147
|
+
* reconcile does not hang on a picker. `sync status` is inspect-only unless the
|
|
148
|
+
* caller typed `--yes` themselves: `--yes` on that command is reconcile, not
|
|
149
|
+
* "don't prompt". Nesting it under `sync` must not inherit the umbrella flag
|
|
150
|
+
* (RUSH-2864).
|
|
151
|
+
*/
|
|
152
|
+
export function buildPassthroughForwardedArgs(command, allArgs, interactive) {
|
|
153
|
+
const spec = REMOTE_PASSTHROUGH[command];
|
|
154
|
+
let forwarded = stripRoutingFlags(allArgs, STRIP_SPECS);
|
|
155
|
+
// Detect the subcommand on the *stripped* argv. On the raw argv,
|
|
156
|
+
// `sync --device peer status` would treat `peer` as the first non-flag
|
|
157
|
+
// token and inherit umbrella `--yes` (RUSH-2864 review).
|
|
158
|
+
const skipInheritedYes = command === 'sync' && firstSubcommand(forwarded, 'sync') === 'status';
|
|
159
|
+
if (!interactive && spec?.nonInteractive && !skipInheritedYes) {
|
|
160
|
+
forwarded = [...forwarded, ...spec.nonInteractive];
|
|
161
|
+
}
|
|
162
|
+
return forwarded;
|
|
163
|
+
}
|
|
140
164
|
/** Synthesize a `Host` for a raw `user@host` / bare-alias target (not enrolled). */
|
|
141
165
|
function syntheticHost(target) {
|
|
142
166
|
const at = target.indexOf('@');
|
|
@@ -531,9 +555,7 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
531
555
|
// out — otherwise force the command's non-interactive path so no half-drawn
|
|
532
556
|
// picker is piped into a file or another program.
|
|
533
557
|
const interactive = !!process.stdout.isTTY && !allArgs.includes('--no-tty');
|
|
534
|
-
|
|
535
|
-
if (!interactive && spec.nonInteractive)
|
|
536
|
-
forwarded = [...forwarded, ...spec.nonInteractive];
|
|
558
|
+
const forwarded = buildPassthroughForwardedArgs(command, allArgs, interactive);
|
|
537
559
|
// The one long-running case: keep the remote team supervisor alive past a
|
|
538
560
|
// disconnect by dispatching it detached (nohup), still streaming live.
|
|
539
561
|
const isWatchedTeamStart = command === 'teams' && forwarded[1] === 'start' && forwarded.includes('--watch');
|
|
@@ -21,7 +21,7 @@ import { upsertSession } from '../session/db.js';
|
|
|
21
21
|
import { isSessionTrackedAgent } from '../session/types.js';
|
|
22
22
|
import { localLogPath, updateTask } from './tasks.js';
|
|
23
23
|
import { parseSessionIdMarker } from './session-marker.js';
|
|
24
|
-
import { deriveShortId } from '../
|
|
24
|
+
import { deriveShortId } from '../text/short-id.js';
|
|
25
25
|
import { normalizeHost } from '../machine-id.js';
|
|
26
26
|
/**
|
|
27
27
|
* Build the SessionMeta for a host-dispatched run. Returns null when the run has
|
|
@@ -2015,7 +2015,7 @@ export function pruneOrphanedCommandShim(fileName) {
|
|
|
2015
2015
|
return false;
|
|
2016
2016
|
}
|
|
2017
2017
|
if (content.includes('# Alias shim:'))
|
|
2018
|
-
return false; // a user `agents alias` — leave it
|
|
2018
|
+
return false; // a user `agents setup alias` shim — leave it
|
|
2019
2019
|
const bin = readAgentsBinFromShim(shimPath);
|
|
2020
2020
|
if (!bin)
|
|
2021
2021
|
return false; // not an AGENTS_BIN-baked shim
|
|
@@ -230,6 +230,46 @@ export declare function printTrashFooter(moved: Array<{
|
|
|
230
230
|
* containing only `home/`) remain under the agent dir.
|
|
231
231
|
*/
|
|
232
232
|
export declare function removeAllVersions(agent: AgentId): number;
|
|
233
|
+
export interface HealedVersionPointers {
|
|
234
|
+
/** Global default reassigned off a not-installed version (`to: null` = cleared). */
|
|
235
|
+
globalDefault?: {
|
|
236
|
+
from: string;
|
|
237
|
+
to: string | null;
|
|
238
|
+
};
|
|
239
|
+
/** Isolated default (bare `agents run <agent>`) reassigned off a not-installed version. */
|
|
240
|
+
isolatedDefault?: {
|
|
241
|
+
from: string;
|
|
242
|
+
to: string | null;
|
|
243
|
+
};
|
|
244
|
+
/** `~/.<agent>` config symlink repointed off a not-installed version. */
|
|
245
|
+
configSymlink?: {
|
|
246
|
+
from: string;
|
|
247
|
+
to: string;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Repoint an agent's version pointers — the global/isolated default and the
|
|
252
|
+
* `~/.<agent>` config symlink — off any version that is no longer installed, so
|
|
253
|
+
* neither `agents sync`/`agents run` resolution nor the conventional-path home
|
|
254
|
+
* can land on a dead version (RUSH-2471).
|
|
255
|
+
*
|
|
256
|
+
* How a pointer goes dangling: {@link removeVersion} reassigns the defaults and
|
|
257
|
+
* clears the config symlink only when IT removes the pointed-at version. A
|
|
258
|
+
* version-home whose launch binary disappears by any other route — grok
|
|
259
|
+
* self-updated its per-version binary out from under the old dir, a manual
|
|
260
|
+
* delete, a half-finished install that seeded the home but never landed the
|
|
261
|
+
* binary — leaves the default and/or the symlink pointing at something
|
|
262
|
+
* {@link isVersionInstalled} reports as gone.
|
|
263
|
+
*
|
|
264
|
+
* Called from the sync resolve path so the invariant self-heals. Mirrors
|
|
265
|
+
* {@link removeVersion}'s reassignment: the global default prefers the newest
|
|
266
|
+
* NON-isolated survivor else clears; the isolated default prefers the newest
|
|
267
|
+
* remaining isolated copy else clears. The symlink is repointed at the now-healed
|
|
268
|
+
* resolved default, else the newest installed non-isolated version. A symlink
|
|
269
|
+
* already on an installed version, a real (non-symlink) config dir, and an
|
|
270
|
+
* isolated-only agent are left untouched.
|
|
271
|
+
*/
|
|
272
|
+
export declare function healDanglingVersionPointers(agent: AgentId, cwd: string): Promise<HealedVersionPointers>;
|
|
233
273
|
/**
|
|
234
274
|
* Normalize a user-supplied @version token across CLI subcommands.
|
|
235
275
|
*
|