@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.
Files changed (122) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +1 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +25 -7
  5. package/dist/cli/command-registry.d.ts +0 -3
  6. package/dist/cli/command-registry.js +0 -8
  7. package/dist/commands/alias.d.ts +7 -4
  8. package/dist/commands/alias.js +33 -8
  9. package/dist/commands/auth.js +7 -3
  10. package/dist/commands/computer.js +20 -6
  11. package/dist/commands/daemon.js +1 -1
  12. package/dist/commands/doctor.d.ts +4 -2
  13. package/dist/commands/doctor.js +20 -5
  14. package/dist/commands/exec.js +3 -2
  15. package/dist/commands/feed.js +1 -30
  16. package/dist/commands/harness.js +3 -3
  17. package/dist/commands/monitors.js +1 -1
  18. package/dist/commands/org.js +9 -8
  19. package/dist/commands/profiles.js +2 -2
  20. package/dist/commands/routines.js +43 -25
  21. package/dist/commands/sessions-picker.js +124 -122
  22. package/dist/commands/setup.js +5 -1
  23. package/dist/commands/snapshot.d.ts +1 -1
  24. package/dist/commands/snapshot.js +4 -4
  25. package/dist/commands/status.d.ts +4 -2
  26. package/dist/commands/status.js +10 -7
  27. package/dist/commands/sync.d.ts +1 -0
  28. package/dist/commands/sync.js +29 -2
  29. package/dist/commands/view.js +6 -0
  30. package/dist/commands/webhook.js +1 -1
  31. package/dist/lib/accounting/rotate.d.ts +5 -3
  32. package/dist/lib/accounting/rotate.js +36 -7
  33. package/dist/lib/accounting/usage.d.ts +18 -4
  34. package/dist/lib/accounting/usage.js +138 -27
  35. package/dist/lib/agent-spec/agents.d.ts +7 -7
  36. package/dist/lib/agent-spec/agents.js +15 -15
  37. package/dist/lib/cloud/rush.d.ts +0 -15
  38. package/dist/lib/cloud/rush.js +0 -35
  39. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  40. package/dist/lib/crabbox/runtimes.js +21 -5
  41. package/dist/lib/daemon/daemon.js +86 -58
  42. package/dist/lib/daemon/runner.d.ts +18 -1
  43. package/dist/lib/daemon/runner.js +105 -11
  44. package/dist/lib/devices/doctor-findings.js +1 -1
  45. package/dist/lib/devices/health-report.js +1 -1
  46. package/dist/lib/drift-sync.d.ts +1 -1
  47. package/dist/lib/drift-sync.js +2 -2
  48. package/dist/lib/drift.d.ts +1 -1
  49. package/dist/lib/exec.js +13 -3
  50. package/dist/lib/format.d.ts +9 -0
  51. package/dist/lib/format.js +16 -0
  52. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  53. package/dist/lib/github/pr-mergeable.js +157 -0
  54. package/dist/lib/github/pr-verdict.d.ts +67 -0
  55. package/dist/lib/github/pr-verdict.js +72 -0
  56. package/dist/lib/harness/adapter.d.ts +6 -5
  57. package/dist/lib/hosts/passthrough.d.ts +10 -0
  58. package/dist/lib/hosts/passthrough.js +28 -6
  59. package/dist/lib/hosts/session-index.js +1 -1
  60. package/dist/lib/installations/shims.js +1 -1
  61. package/dist/lib/installations/versions.d.ts +40 -0
  62. package/dist/lib/installations/versions.js +57 -2
  63. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  64. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  65. package/dist/lib/menubar/install-menubar.js +19 -8
  66. package/dist/lib/monitors/config.d.ts +2 -2
  67. package/dist/lib/monitors/config.js +3 -2
  68. package/dist/lib/origin-machine.d.ts +18 -0
  69. package/dist/lib/origin-machine.js +34 -0
  70. package/dist/lib/perf/db.js +1 -1
  71. package/dist/lib/perf/spool.js +1 -1
  72. package/dist/lib/profiles.d.ts +7 -0
  73. package/dist/lib/profiles.js +26 -1
  74. package/dist/lib/sandbox.d.ts +24 -0
  75. package/dist/lib/sandbox.js +101 -0
  76. package/dist/lib/scheduling/routines.d.ts +18 -9
  77. package/dist/lib/scheduling/routines.js +16 -8
  78. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  79. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  80. package/dist/lib/secrets/agent.js +9 -1
  81. package/dist/lib/secrets/index.js +1 -1
  82. package/dist/lib/secrets/list-filter.js +1 -1
  83. package/dist/lib/service-manifest.d.ts +20 -0
  84. package/dist/lib/service-manifest.js +34 -0
  85. package/dist/lib/session/cloud.js +1 -1
  86. package/dist/lib/session/db.js +1 -1
  87. package/dist/lib/session/fork.js +1 -1
  88. package/dist/lib/session/live-metadata.js +1 -1
  89. package/dist/lib/session/origin-machine.d.ts +1 -18
  90. package/dist/lib/session/origin-machine.js +4 -34
  91. package/dist/lib/session/relative-time.d.ts +1 -44
  92. package/dist/lib/session/relative-time.js +4 -120
  93. package/dist/lib/session/remote-list.js +1 -1
  94. package/dist/lib/session/remote.js +2 -2
  95. package/dist/lib/session/short-id.d.ts +1 -17
  96. package/dist/lib/session/short-id.js +4 -20
  97. package/dist/lib/session/width.d.ts +1 -29
  98. package/dist/lib/session/width.js +4 -101
  99. package/dist/lib/snapshot.d.ts +2 -2
  100. package/dist/lib/snapshot.js +1 -1
  101. package/dist/lib/startup/command-registry.d.ts +4 -1
  102. package/dist/lib/startup/command-registry.js +11 -4
  103. package/dist/lib/teams/placement-probe.js +1 -1
  104. package/dist/lib/teams/scheduler.js +1 -1
  105. package/dist/lib/text/relative-time.d.ts +44 -0
  106. package/dist/lib/text/relative-time.js +120 -0
  107. package/dist/lib/text/short-id.d.ts +17 -0
  108. package/dist/lib/text/short-id.js +20 -0
  109. package/dist/lib/text/width.d.ts +29 -0
  110. package/dist/lib/text/width.js +101 -0
  111. package/dist/lib/triggers/handlers.d.ts +7 -0
  112. package/dist/lib/triggers/handlers.js +8 -0
  113. package/dist/lib/triggers/webhook.d.ts +1 -0
  114. package/dist/lib/triggers/webhook.js +2 -1
  115. package/dist/lib/view-types.d.ts +4 -0
  116. package/package.json +1 -1
  117. package/dist/commands/tickets.d.ts +0 -2
  118. package/dist/commands/tickets.js +0 -43
  119. package/dist/lib/observe-aliases.d.ts +0 -30
  120. package/dist/lib/observe-aliases.js +0 -38
  121. package/dist/lib/tickets/list.d.ts +0 -53
  122. 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
- if (result.exitCode === 0)
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];
@@ -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
@@ -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 pre-baked token array from ROUTINE_AGENT_COMMANDS,
77
- * distinct from buildExecCommand's declarative modeFlags — so it is a separate
78
- * method on the same adapter, one source of truth per harness across both.
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 ROUTINE_AGENT_COMMANDS token array in place, exactly
135
- * as the old per-agent arm did; runner appends model/reasoning flags after.
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
- // status / inspect
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
- let forwarded = stripRoutingFlags(allArgs, STRIP_SPECS);
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 '../session/short-id.js';
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
  *