@phnx-labs/agents-cli 1.22.13 → 1.22.15
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 +30 -0
- package/README.md +4 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +4 -6
- package/dist/commands/packages.js +90 -22
- package/dist/commands/projects.d.ts +2 -5
- package/dist/commands/projects.js +92 -65
- package/dist/commands/routines.js +53 -71
- package/dist/commands/secrets.js +10 -6
- package/dist/commands/sessions-optimize.d.ts +14 -0
- package/dist/commands/sessions-optimize.js +41 -0
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/setup-watchdog.d.ts +3 -0
- package/dist/commands/setup-watchdog.js +54 -0
- package/dist/commands/setup.js +8 -1
- package/dist/commands/ssh.d.ts +11 -0
- package/dist/commands/ssh.js +18 -4
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +40 -28
- package/dist/lib/auto-dispatch.d.ts +8 -10
- package/dist/lib/auto-dispatch.js +20 -42
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +4 -3
- package/dist/lib/project-import.d.ts +7 -46
- package/dist/lib/project-import.js +9 -108
- package/dist/lib/projects.d.ts +17 -0
- package/dist/lib/projects.js +17 -1
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +11 -0
- package/dist/lib/routine-activation.d.ts +14 -0
- package/dist/lib/routine-activation.js +78 -0
- package/dist/lib/routines-project.d.ts +1 -1
- package/dist/lib/routines-project.js +3 -9
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/routines.js +32 -19
- 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/session/db.d.ts +19 -0
- package/dist/lib/session/db.js +24 -0
- package/dist/lib/state.js +17 -4
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
- package/dist/lib/watchdog/routine.d.ts +0 -44
- package/dist/lib/watchdog/routine.js +0 -69
|
@@ -10,26 +10,26 @@
|
|
|
10
10
|
* agents watchdog --nudge one tick, actually injects (explicit opt-in)
|
|
11
11
|
* agents watchdog --watch manual poll loop (dry unless --nudge)
|
|
12
12
|
* agents watchdog --json machine-readable tick output (for the menu-bar)
|
|
13
|
-
* agents watchdog
|
|
13
|
+
* agents watchdog on|off turn the always-on watchdog routine on/off
|
|
14
14
|
* agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
|
|
15
15
|
*
|
|
16
16
|
* The always-on watchdog is a daemon-fired ROUTINE, not a private sentinel + a
|
|
17
|
-
* hand-rolled loop
|
|
18
|
-
*
|
|
19
|
-
* `disable` pauses it. See ../lib/watchdog/routine.ts.
|
|
17
|
+
* hand-rolled loop. Its immutable definition comes from the system DotAgents
|
|
18
|
+
* repo; on/off only changes this device's routine membership.
|
|
20
19
|
*/
|
|
21
20
|
import chalk from 'chalk';
|
|
22
21
|
import * as path from 'path';
|
|
23
22
|
import { setHelpSections } from '../lib/help.js';
|
|
24
23
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
25
24
|
import { getRuntimeStateDir } from '../lib/state.js';
|
|
26
|
-
import { setJobEnabled } from '../lib/routines.js';
|
|
25
|
+
import { readJob, setJobEnabled } from '../lib/routines.js';
|
|
27
26
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
28
27
|
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
29
28
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
30
|
-
import {
|
|
29
|
+
import { devicesWithRoutineEnabled, routineEnabledOnThisDevice } from '../lib/routine-activation.js';
|
|
31
30
|
import { runWatchdogTick, writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
|
|
32
31
|
import { isWatchdogRotateEnabled, listRotateStates, setWatchdogRotateEnabled } from '../lib/watchdog/rotate.js';
|
|
32
|
+
const WATCHDOG_ROUTINE_NAME = 'watchdog';
|
|
33
33
|
/** Default state dir the runner and these subcommands share. */
|
|
34
34
|
function stateDir() {
|
|
35
35
|
return path.join(getRuntimeStateDir(), 'watchdog');
|
|
@@ -120,7 +120,7 @@ export function registerWatchdogCommand(program) {
|
|
|
120
120
|
.command('watchdog')
|
|
121
121
|
.description('Auto-nudge stalled agent terminals: detect stalls, resolve the exact split, inject "Continue." — no menu-bar needed.')
|
|
122
122
|
.option('--nudge', 'Actually inject (default is a dry run that only reports what it would do)')
|
|
123
|
-
.option('--watch', 'Manual poll loop: run a tick every --interval (dry unless --nudge; the always-on path is `watchdog
|
|
123
|
+
.option('--watch', 'Manual poll loop: run a tick every --interval (dry unless --nudge; the always-on path is `watchdog on`)')
|
|
124
124
|
.option('--interval <dur>', 'Poll interval in --watch mode (e.g. 30s, 1m)', '30s')
|
|
125
125
|
.option('--stall <dur>', 'Idle time before a session counts as stalled', humanMs(DEFAULT_THRESHOLDS.stallMs))
|
|
126
126
|
.option('--cooldown <dur>', 'Minimum time between nudges to the same session', humanMs(DEFAULT_THRESHOLDS.cooldownMs))
|
|
@@ -166,7 +166,7 @@ export function registerWatchdogCommand(program) {
|
|
|
166
166
|
const intervalMs = durationMsOr(opts.interval, 30_000);
|
|
167
167
|
if (!computeWillInject() && !opts.json) {
|
|
168
168
|
console.log(chalk.yellow(`watchdog --watch is DETECT-ONLY. Pass --nudge to inject, ` +
|
|
169
|
-
`or run 'agents watchdog
|
|
169
|
+
`or run 'agents watchdog on' for the always-on daemon routine.`));
|
|
170
170
|
}
|
|
171
171
|
// eslint-disable-next-line no-constant-condition
|
|
172
172
|
while (true) {
|
|
@@ -197,7 +197,7 @@ export function registerWatchdogCommand(program) {
|
|
|
197
197
|
agents watchdog --json
|
|
198
198
|
|
|
199
199
|
# Turn on the ALWAYS-ON watchdog (a daemon-fired routine)
|
|
200
|
-
agents watchdog
|
|
200
|
+
agents watchdog on
|
|
201
201
|
|
|
202
202
|
# Show the routine, rotate config, and any in-flight in-place rotates
|
|
203
203
|
agents watchdog status
|
|
@@ -242,36 +242,45 @@ export function registerWatchdogCommand(program) {
|
|
|
242
242
|
'watchdog.rotate: off' to ~/.agents/agents.yaml; nudging stays on).
|
|
243
243
|
State machine: ~/.agents/.cache/state/watchdog/rotate/<sessionId>.json.
|
|
244
244
|
|
|
245
|
-
Always-on: 'agents watchdog
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
Always-on: 'agents watchdog on' enables the system-defined 'watchdog' routine
|
|
246
|
+
on this device and reloads the daemon; 'off' disables it here. Inspect it with
|
|
247
|
+
'agents routines list'. Defaults
|
|
248
248
|
OFF. Per-session policy: off (ignore), keep (default), handsoff (detect + flag).
|
|
249
249
|
|
|
250
250
|
State (tray-readable): ${path.join('~/.agents/.cache/state/watchdog', '{nudges,flags,last-tick}.json')}
|
|
251
251
|
`,
|
|
252
252
|
});
|
|
253
253
|
// --- always-on enable/disable/status (backed by the daemon routine) --------
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
254
|
+
const turnOn = async () => {
|
|
255
|
+
const routine = readJob(WATCHDOG_ROUTINE_NAME);
|
|
256
|
+
if (!routine)
|
|
257
|
+
throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
|
|
258
|
+
setJobEnabled(WATCHDOG_ROUTINE_NAME, true);
|
|
258
259
|
await reloadDaemonForRoutine(true);
|
|
259
|
-
console.log(chalk.green(`watchdog:
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (!watchdogRoutineExists()) {
|
|
265
|
-
console.log(chalk.dim('watchdog: already off (no routine)'));
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
260
|
+
console.log(chalk.green(`watchdog: ON on this device (${routine.schedule ?? 'event-triggered'})`));
|
|
261
|
+
};
|
|
262
|
+
const turnOff = async () => {
|
|
263
|
+
if (!readJob(WATCHDOG_ROUTINE_NAME))
|
|
264
|
+
throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
|
|
268
265
|
setJobEnabled(WATCHDOG_ROUTINE_NAME, false);
|
|
269
266
|
await reloadDaemonForRoutine(false);
|
|
270
|
-
console.log(chalk.yellow('watchdog:
|
|
267
|
+
console.log(chalk.yellow('watchdog: OFF on this device'));
|
|
268
|
+
};
|
|
269
|
+
cmd.command('on')
|
|
270
|
+
.description('Enable the built-in watchdog routine on this device.')
|
|
271
|
+
.action(async () => {
|
|
272
|
+
await turnOn();
|
|
273
|
+
});
|
|
274
|
+
cmd.command('off')
|
|
275
|
+
.description('Disable the built-in watchdog routine on this device.')
|
|
276
|
+
.action(async () => {
|
|
277
|
+
await turnOff();
|
|
271
278
|
});
|
|
279
|
+
cmd.command('enable', { hidden: true }).action(turnOn);
|
|
280
|
+
cmd.command('disable', { hidden: true }).action(turnOff);
|
|
272
281
|
cmd.command('rotate <state>')
|
|
273
282
|
.description('Turn in-place rotate of rate-limited sessions on|off (watchdog.rotate in agents.yaml). ' +
|
|
274
|
-
'Rotate-only: nudging stays on — unlike `watchdog
|
|
283
|
+
'Rotate-only: nudging stays on — unlike `watchdog off`, which disables the whole watchdog on this device.')
|
|
275
284
|
.action((state) => {
|
|
276
285
|
const s = state.toLowerCase();
|
|
277
286
|
if (s !== 'on' && s !== 'off') {
|
|
@@ -292,13 +301,15 @@ export function registerWatchdogCommand(program) {
|
|
|
292
301
|
// parent, not this subcommand. optsWithGlobals() merges both levels, so we
|
|
293
302
|
// read it correctly regardless of which command commander bound it to.
|
|
294
303
|
const json = command.optsWithGlobals().json === true;
|
|
295
|
-
const on =
|
|
304
|
+
const on = routineEnabledOnThisDevice(WATCHDOG_ROUTINE_NAME) === true;
|
|
305
|
+
const enabledDevices = devicesWithRoutineEnabled(WATCHDOG_ROUTINE_NAME);
|
|
296
306
|
const rotate = isWatchdogRotateEnabled() ? 'on' : 'off';
|
|
297
307
|
const rotates = listRotateStates(stateDir());
|
|
298
308
|
const inflight = rotates.filter((r) => r.phase !== 'done' && r.phase !== 'failed');
|
|
299
309
|
if (json) {
|
|
300
310
|
console.log(JSON.stringify({
|
|
301
311
|
enabled: on,
|
|
312
|
+
enabledDevices,
|
|
302
313
|
routine: WATCHDOG_ROUTINE_NAME,
|
|
303
314
|
stateDir: stateDir(),
|
|
304
315
|
rotate,
|
|
@@ -314,6 +325,7 @@ export function registerWatchdogCommand(program) {
|
|
|
314
325
|
return;
|
|
315
326
|
}
|
|
316
327
|
console.log(`always-on watchdog: ${on ? chalk.green('ON') : chalk.dim('off')} (routine '${WATCHDOG_ROUTINE_NAME}')`);
|
|
328
|
+
console.log(`enabled devices: ${enabledDevices.length > 0 ? enabledDevices.join(', ') : chalk.dim('none')}`);
|
|
317
329
|
console.log(`rotate: ${rotate === 'on' ? chalk.green('on') : chalk.yellow('off')} (watchdog.rotate in agents.yaml) · ${inflight.length} in-flight`);
|
|
318
330
|
for (const r of inflight) {
|
|
319
331
|
console.log(` ${chalk.magenta(r.phase.padEnd(12))} ${chalk.bold(r.sessionId.slice(0, 8))} → ${r.newSessionId.slice(0, 8)}${r.error ? chalk.red(` ${r.error}`) : ''}`);
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The dispatch goes through the same provider layer as `agents cloud run`, so Rush
|
|
10
10
|
* (Prix) is just one provider among rush/codex/factory — NOT a hidden requirement.
|
|
11
|
-
* A project
|
|
11
|
+
* A project pins its provider via `dispatch.provider` in its
|
|
12
|
+
* ~/.agents/projects/<name>.yaml definition.
|
|
12
13
|
*
|
|
13
|
-
* OPT-IN: a project auto-dispatches ONLY when `
|
|
14
|
-
* Nothing
|
|
14
|
+
* OPT-IN: a project auto-dispatches ONLY when `dispatch.enabled: true` AND
|
|
15
|
+
* `dispatch.maxAgents > 0` AND it has a `linear.projectId`. Nothing is on by default.
|
|
15
16
|
*/
|
|
16
|
-
/** The subset of a
|
|
17
|
+
/** The subset of a project definition this module needs. */
|
|
17
18
|
export interface AutoDispatchProject {
|
|
18
19
|
id: string;
|
|
19
20
|
name: string;
|
|
@@ -44,13 +45,10 @@ export interface PlannedDispatch {
|
|
|
44
45
|
delegateName: string;
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* tested end-to-end against a real fixture file instead of a mock.
|
|
48
|
+
* Read auto-dispatch candidates from canonical project YAML definitions
|
|
49
|
+
* (~/.agents/projects/<name>.yaml). Only projects with `dispatch.enabled: true`
|
|
50
|
+
* are returned; the caller's `isEligible` gate then checks the cap and Linear id.
|
|
51
51
|
*/
|
|
52
|
-
export declare function factoryProjectsPath(): string;
|
|
53
|
-
/** Read the project registry rows this module cares about. */
|
|
54
52
|
export declare function readAutoDispatchProjects(): AutoDispatchProject[];
|
|
55
53
|
/** A project is eligible only when it has explicitly opted in with a positive cap. */
|
|
56
54
|
export declare function isEligible(p: AutoDispatchProject): boolean;
|
|
@@ -8,53 +8,31 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The dispatch goes through the same provider layer as `agents cloud run`, so Rush
|
|
10
10
|
* (Prix) is just one provider among rush/codex/factory — NOT a hidden requirement.
|
|
11
|
-
* A project
|
|
11
|
+
* A project pins its provider via `dispatch.provider` in its
|
|
12
|
+
* ~/.agents/projects/<name>.yaml definition.
|
|
12
13
|
*
|
|
13
|
-
* OPT-IN: a project auto-dispatches ONLY when `
|
|
14
|
-
* Nothing
|
|
14
|
+
* OPT-IN: a project auto-dispatches ONLY when `dispatch.enabled: true` AND
|
|
15
|
+
* `dispatch.maxAgents > 0` AND it has a `linear.projectId`. Nothing is on by default.
|
|
15
16
|
*/
|
|
16
|
-
import
|
|
17
|
-
import * as path from 'path';
|
|
18
|
-
import { homedir } from 'os';
|
|
17
|
+
import { listProjectDefs } from './projects.js';
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* tested end-to-end against a real fixture file instead of a mock.
|
|
19
|
+
* Read auto-dispatch candidates from canonical project YAML definitions
|
|
20
|
+
* (~/.agents/projects/<name>.yaml). Only projects with `dispatch.enabled: true`
|
|
21
|
+
* are returned; the caller's `isEligible` gate then checks the cap and Linear id.
|
|
24
22
|
*/
|
|
25
|
-
export function factoryProjectsPath() {
|
|
26
|
-
return process.env.AGENTS_FACTORY_PROJECTS_PATH ?? path.join(homedir(), '.agents', 'factory', 'projects.json');
|
|
27
|
-
}
|
|
28
|
-
/** Read the project registry rows this module cares about. */
|
|
29
23
|
export function readAutoDispatchProjects() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
continue;
|
|
43
|
-
const o = r;
|
|
44
|
-
if (typeof o.id !== 'string' || typeof o.name !== 'string')
|
|
45
|
-
continue;
|
|
46
|
-
out.push({
|
|
47
|
-
id: o.id,
|
|
48
|
-
name: o.name,
|
|
49
|
-
linearProjectId: typeof o.linearProjectId === 'string' ? o.linearProjectId : undefined,
|
|
50
|
-
repoSlug: typeof o.repoSlug === 'string' ? o.repoSlug : undefined,
|
|
51
|
-
autoDispatch: o.autoDispatch === true,
|
|
52
|
-
maxAgents: typeof o.maxAgents === 'number' ? o.maxAgents : undefined,
|
|
53
|
-
provider: typeof o.provider === 'string' ? o.provider : undefined,
|
|
54
|
-
host: typeof o.host === 'string' ? o.host : undefined,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
24
|
+
return listProjectDefs()
|
|
25
|
+
.filter((d) => d.dispatch?.enabled === true)
|
|
26
|
+
.map((d) => ({
|
|
27
|
+
id: d.name,
|
|
28
|
+
name: d.name,
|
|
29
|
+
linearProjectId: d.linear?.projectId,
|
|
30
|
+
repoSlug: d.repo ?? d.repos?.[0]?.slug,
|
|
31
|
+
autoDispatch: d.dispatch?.enabled ?? false,
|
|
32
|
+
maxAgents: d.dispatch?.maxAgents,
|
|
33
|
+
provider: d.dispatch?.provider,
|
|
34
|
+
host: d.dispatch?.host,
|
|
35
|
+
}));
|
|
58
36
|
}
|
|
59
37
|
/** A project is eligible only when it has explicitly opted in with a positive cap. */
|
|
60
38
|
export function isEligible(p) {
|
|
Binary file
|
|
Binary file
|
package/dist/lib/migrate.d.ts
CHANGED
|
@@ -132,7 +132,7 @@ export declare function migrateRoutineDeviceToDevices(routinesDir?: string): voi
|
|
|
132
132
|
* Both seams are injectable so the migration is unit-testable without touching the
|
|
133
133
|
* real routines dir (and without racing the routine module's own tests).
|
|
134
134
|
*/
|
|
135
|
-
export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string,
|
|
135
|
+
export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string, enable?: (name: string, enabled: boolean) => void): void;
|
|
136
136
|
/**
|
|
137
137
|
* Rename cli/ → clis/ in each of the given `.agents/` directories.
|
|
138
138
|
*
|
package/dist/lib/migrate.js
CHANGED
|
@@ -13,7 +13,7 @@ import { atomicWriteFileSync } from './fs-atomic.js';
|
|
|
13
13
|
import { machineId } from './machine-id.js';
|
|
14
14
|
import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
|
|
15
15
|
import { createLink } from './platform/index.js';
|
|
16
|
-
import {
|
|
16
|
+
import { migrateLegacyRoutineActivation, setJobEnabled } from './routines.js';
|
|
17
17
|
const HOME = process.env.HOME ?? os.homedir();
|
|
18
18
|
const USER_DIR = path.join(HOME, '.agents');
|
|
19
19
|
/** Canonical system-repo location (post-fold). */
|
|
@@ -1997,11 +1997,11 @@ export function migrateRoutineDeviceToDevices(routinesDir) {
|
|
|
1997
1997
|
* Both seams are injectable so the migration is unit-testable without touching the
|
|
1998
1998
|
* real routines dir (and without racing the routine module's own tests).
|
|
1999
1999
|
*/
|
|
2000
|
-
export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_DIR, 'state', 'watchdog', 'enabled'),
|
|
2000
|
+
export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_DIR, 'state', 'watchdog', 'enabled'), enable = setJobEnabled) {
|
|
2001
2001
|
if (!fs.existsSync(sentinelPath))
|
|
2002
2002
|
return;
|
|
2003
2003
|
try {
|
|
2004
|
-
|
|
2004
|
+
enable('watchdog', true);
|
|
2005
2005
|
}
|
|
2006
2006
|
catch (err) {
|
|
2007
2007
|
console.error(`watchdog sentinel migration: could not create the routine (${err.message}); leaving the sentinel for a later retry`);
|
|
@@ -2096,6 +2096,7 @@ export async function runMigration() {
|
|
|
2096
2096
|
migrateExtrasExtrasToAgentsExtras();
|
|
2097
2097
|
// Rewrite routine YAML files: singular `device:` -> plural `devices: []`.
|
|
2098
2098
|
migrateRoutineDeviceToDevices();
|
|
2099
|
+
migrateLegacyRoutineActivation();
|
|
2099
2100
|
// Fold the legacy watchdog enable sentinel into the watchdog routine so a user
|
|
2100
2101
|
// who opted in under the old build stays opted in after upgrading. After the
|
|
2101
2102
|
// routine rewrites above so the routines dir is in its canonical shape.
|
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure builders behind `agents projects import`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* registry** (`~/.agents/factory/projects.json`, an auto-detection that guesses
|
|
7
|
-
* from checkouts on disk). Both funnel through `writeProjectDef`.
|
|
8
|
-
*
|
|
9
|
-
* The Factory registry stamps each row with a `confidence` — and importing every
|
|
10
|
-
* row regardless is what buried the real projects under a dozen guesses
|
|
11
|
-
* (`agents-cleaned-stale2`, a repo cloned from someone else's org, …). So the
|
|
12
|
-
* import is gated on that field, `high` by default.
|
|
4
|
+
* Currently supports one source: **Linear** (projects someone deliberately
|
|
5
|
+
* created on the board). Results funnel through `writeProjectDef`.
|
|
13
6
|
*
|
|
14
7
|
* Everything here is a pure function of its arguments — no fs, no shell, no
|
|
15
|
-
* process env. The command layer
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* against `$HOME`.
|
|
8
|
+
* process env. The command layer shells out to `linear` and hands the rows in;
|
|
9
|
+
* that's what makes these testable against plain fixtures with no mocking.
|
|
10
|
+
* The one exception is `toHomeRelative`, a string rewrite against `$HOME`.
|
|
19
11
|
*/
|
|
20
12
|
import { type ProjectDef } from './projects.js';
|
|
21
13
|
import { type LinearProjectLite } from './linear-projects.js';
|
|
22
|
-
/** Factory's per-row detection confidence, weakest first. */
|
|
23
|
-
export type ImportConfidence = 'low' | 'medium' | 'high';
|
|
24
|
-
/**
|
|
25
|
-
* The floor an import runs at. `any` is what `--all` means — it takes rows that
|
|
26
|
-
* state no confidence at all, which rank below even `low`; without it "import
|
|
27
|
-
* every row regardless of confidence" would quietly drop the unranked ones.
|
|
28
|
-
*/
|
|
29
|
-
export type ImportFloor = ImportConfidence | 'any';
|
|
30
14
|
/** A row the import declined to write, with the reason to print. */
|
|
31
15
|
export interface ImportSkip {
|
|
32
16
|
name: string;
|
|
@@ -39,42 +23,19 @@ export interface ImportPlan {
|
|
|
39
23
|
}
|
|
40
24
|
/** The validated shape of the `import` flags. */
|
|
41
25
|
export interface ImportOptions {
|
|
42
|
-
source: '
|
|
43
|
-
/** Factory only: the lowest confidence that still imports. */
|
|
44
|
-
minConfidence: ImportFloor;
|
|
26
|
+
source: 'linear';
|
|
45
27
|
force: boolean;
|
|
46
28
|
}
|
|
47
29
|
/** The raw commander flags, before validation. */
|
|
48
30
|
export interface RawImportFlags {
|
|
49
|
-
fromFactory?: boolean;
|
|
50
31
|
fromLinear?: boolean;
|
|
51
|
-
minConfidence?: string;
|
|
52
|
-
all?: boolean;
|
|
53
32
|
force?: boolean;
|
|
54
33
|
}
|
|
55
34
|
/**
|
|
56
35
|
* Validate the flag combination, throwing a user-facing message on the first
|
|
57
|
-
* problem.
|
|
58
|
-
* error, never a silent fall back to the default floor.
|
|
36
|
+
* problem.
|
|
59
37
|
*/
|
|
60
38
|
export declare function validateImportOpts(flags: RawImportFlags): ImportOptions;
|
|
61
|
-
/**
|
|
62
|
-
* Plan the Factory import: same field mapping as before (`path`→`root`,
|
|
63
|
-
* `repoSlug`→`repo`, `linearProjectId`→`linear.projectId`), now gated on the
|
|
64
|
-
* row's `confidence`. A row with no confidence field is a guess with no stated
|
|
65
|
-
* strength, so it ranks below every floor and only `--all` takes it.
|
|
66
|
-
*
|
|
67
|
-
* `resolveRemote` reads the checkout's actual `origin` and OVERRIDES the row's
|
|
68
|
-
* `repoSlug`. Factory derives that slug from the checkout path's last two
|
|
69
|
-
* segments (`apps/factory/src/core/projectIndex.ts:19-26`), which is only right
|
|
70
|
-
* when the directory tree happens to mirror the GitHub org. It commonly does
|
|
71
|
-
* not: a checkout at `~/src/github.com/<you>/agents-cli` whose remote is
|
|
72
|
-
* `phnx-labs/agents-cli` imported as `<you>/agents-cli` — a real, different
|
|
73
|
-
* repo — so the card's merged-PR and release lines silently reported a
|
|
74
|
-
* stranger's repository instead of failing. The remote is the only authority on
|
|
75
|
-
* which repo a checkout is; the path is a guess about it.
|
|
76
|
-
*/
|
|
77
|
-
export declare function buildFactoryImportCandidates(rows: unknown[], existing: Map<string, ProjectDef>, opts: Pick<ImportOptions, 'minConfidence' | 'force'>, resolveRemote?: (root: string) => string | undefined): ImportPlan;
|
|
78
39
|
/**
|
|
79
40
|
* Turn a Linear project name into a definition slug: lowercase, every run of
|
|
80
41
|
* unusable characters collapsed to one `-`, trimmed of leading/trailing
|
|
@@ -1,123 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure builders behind `agents projects import`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* registry** (`~/.agents/factory/projects.json`, an auto-detection that guesses
|
|
7
|
-
* from checkouts on disk). Both funnel through `writeProjectDef`.
|
|
8
|
-
*
|
|
9
|
-
* The Factory registry stamps each row with a `confidence` — and importing every
|
|
10
|
-
* row regardless is what buried the real projects under a dozen guesses
|
|
11
|
-
* (`agents-cleaned-stale2`, a repo cloned from someone else's org, …). So the
|
|
12
|
-
* import is gated on that field, `high` by default.
|
|
4
|
+
* Currently supports one source: **Linear** (projects someone deliberately
|
|
5
|
+
* created on the board). Results funnel through `writeProjectDef`.
|
|
13
6
|
*
|
|
14
7
|
* Everything here is a pure function of its arguments — no fs, no shell, no
|
|
15
|
-
* process env. The command layer
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* against `$HOME`.
|
|
8
|
+
* process env. The command layer shells out to `linear` and hands the rows in;
|
|
9
|
+
* that's what makes these testable against plain fixtures with no mocking.
|
|
10
|
+
* The one exception is `toHomeRelative`, a string rewrite against `$HOME`.
|
|
19
11
|
*/
|
|
20
|
-
import { toHomeRelative } from './project-root.js';
|
|
21
12
|
import { isSafeProjectName } from './projects.js';
|
|
22
13
|
import { matchLocalCheckoutExact } from './linear-projects.js';
|
|
23
|
-
const CONFIDENCE_RANK = { any: 0, low: 1, medium: 2, high: 3 };
|
|
24
|
-
/** Rank a raw `confidence` value; anything absent or unrecognized ranks 0 (below every floor). */
|
|
25
|
-
function confidenceRank(raw) {
|
|
26
|
-
return typeof raw === 'string' ? (CONFIDENCE_RANK[raw] ?? 0) : 0;
|
|
27
|
-
}
|
|
28
14
|
/**
|
|
29
15
|
* Validate the flag combination, throwing a user-facing message on the first
|
|
30
|
-
* problem.
|
|
31
|
-
* error, never a silent fall back to the default floor.
|
|
16
|
+
* problem.
|
|
32
17
|
*/
|
|
33
18
|
export function validateImportOpts(flags) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (sources.length > 1)
|
|
38
|
-
throw new Error('--from-linear and --from-factory are mutually exclusive — pick one.');
|
|
39
|
-
const source = sources[0];
|
|
40
|
-
if (source === 'linear' && (flags.all || flags.minConfidence !== undefined)) {
|
|
41
|
-
throw new Error('--all and --min-confidence apply to --from-factory only (Linear rows carry no confidence).');
|
|
42
|
-
}
|
|
43
|
-
if (flags.all && flags.minConfidence !== undefined) {
|
|
44
|
-
throw new Error('--all and --min-confidence are mutually exclusive (--all means --min-confidence low).');
|
|
45
|
-
}
|
|
46
|
-
let minConfidence = 'high';
|
|
47
|
-
if (flags.all)
|
|
48
|
-
minConfidence = 'any';
|
|
49
|
-
else if (flags.minConfidence !== undefined) {
|
|
50
|
-
const v = flags.minConfidence.trim().toLowerCase();
|
|
51
|
-
if (v !== 'low' && v !== 'medium' && v !== 'high') {
|
|
52
|
-
throw new Error(`Invalid --min-confidence "${flags.minConfidence}" (expected low, medium, or high).`);
|
|
53
|
-
}
|
|
54
|
-
minConfidence = v;
|
|
55
|
-
}
|
|
56
|
-
return { source, minConfidence, force: flags.force === true };
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Plan the Factory import: same field mapping as before (`path`→`root`,
|
|
60
|
-
* `repoSlug`→`repo`, `linearProjectId`→`linear.projectId`), now gated on the
|
|
61
|
-
* row's `confidence`. A row with no confidence field is a guess with no stated
|
|
62
|
-
* strength, so it ranks below every floor and only `--all` takes it.
|
|
63
|
-
*
|
|
64
|
-
* `resolveRemote` reads the checkout's actual `origin` and OVERRIDES the row's
|
|
65
|
-
* `repoSlug`. Factory derives that slug from the checkout path's last two
|
|
66
|
-
* segments (`apps/factory/src/core/projectIndex.ts:19-26`), which is only right
|
|
67
|
-
* when the directory tree happens to mirror the GitHub org. It commonly does
|
|
68
|
-
* not: a checkout at `~/src/github.com/<you>/agents-cli` whose remote is
|
|
69
|
-
* `phnx-labs/agents-cli` imported as `<you>/agents-cli` — a real, different
|
|
70
|
-
* repo — so the card's merged-PR and release lines silently reported a
|
|
71
|
-
* stranger's repository instead of failing. The remote is the only authority on
|
|
72
|
-
* which repo a checkout is; the path is a guess about it.
|
|
73
|
-
*/
|
|
74
|
-
export function buildFactoryImportCandidates(rows, existing, opts, resolveRemote = () => undefined) {
|
|
75
|
-
const floor = CONFIDENCE_RANK[opts.minConfidence];
|
|
76
|
-
const defs = [];
|
|
77
|
-
const skipped = [];
|
|
78
|
-
// The registry keys rows by `owner/repo` but names them by basename, so two
|
|
79
|
-
// repos with the same basename in different orgs (grinich/inflow, me/inflow)
|
|
80
|
-
// arrive as two rows called `inflow`. Without this the second silently
|
|
81
|
-
// overwrote the first on disk and the run still reported both as imported.
|
|
82
|
-
const seen = new Set();
|
|
83
|
-
for (const raw of rows) {
|
|
84
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
85
|
-
continue;
|
|
86
|
-
const o = raw;
|
|
87
|
-
const name = typeof o.name === 'string' ? o.name : undefined;
|
|
88
|
-
if (!name || !isSafeProjectName(name)) {
|
|
89
|
-
skipped.push({ name: name || '(unnamed)', reason: 'not a usable project name' });
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const rank = confidenceRank(o.confidence);
|
|
93
|
-
if (rank < floor) {
|
|
94
|
-
const stated = typeof o.confidence === 'string' && o.confidence ? `confidence "${o.confidence}"` : 'no confidence field';
|
|
95
|
-
skipped.push({ name, reason: `${stated} is below the "${opts.minConfidence}" floor` });
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (seen.has(name)) {
|
|
99
|
-
skipped.push({ name, reason: 'another row in this registry already claimed the name' });
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (existing.has(name) && !opts.force) {
|
|
103
|
-
skipped.push({ name, reason: 'already defined — pass --force to overwrite' });
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
seen.add(name);
|
|
107
|
-
const def = { name };
|
|
108
|
-
if (typeof o.path === 'string')
|
|
109
|
-
def.root = toHomeRelative(o.path);
|
|
110
|
-
// The checkout's own remote wins over the registry's path-derived guess;
|
|
111
|
-
// the guess is the fallback for a path with no git remote to ask.
|
|
112
|
-
const fromRemote = typeof o.path === 'string' ? resolveRemote(o.path) : undefined;
|
|
113
|
-
const repo = fromRemote ?? (typeof o.repoSlug === 'string' ? o.repoSlug : undefined);
|
|
114
|
-
if (repo)
|
|
115
|
-
def.repo = repo;
|
|
116
|
-
if (typeof o.linearProjectId === 'string')
|
|
117
|
-
def.linear = { projectId: o.linearProjectId };
|
|
118
|
-
defs.push(def);
|
|
119
|
-
}
|
|
120
|
-
return { defs, skipped };
|
|
19
|
+
if (!flags.fromLinear)
|
|
20
|
+
throw new Error('Pick an import source: --from-linear.');
|
|
21
|
+
return { source: 'linear', force: flags.force === true };
|
|
121
22
|
}
|
|
122
23
|
/**
|
|
123
24
|
* Turn a Linear project name into a definition slug: lowercase, every run of
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -94,9 +94,25 @@ export interface ProjectDef {
|
|
|
94
94
|
linear?: {
|
|
95
95
|
projectId?: string;
|
|
96
96
|
url?: string;
|
|
97
|
+
name?: string;
|
|
97
98
|
};
|
|
98
99
|
/** Free-form doc links surfaced in `projects show`. */
|
|
99
100
|
docs?: string[];
|
|
101
|
+
/**
|
|
102
|
+
* Auto-dispatch settings. When `enabled` is true and `maxAgents > 0` and the
|
|
103
|
+
* project has a `linear.projectId`, the daemon polls Linear for delegated-Todo
|
|
104
|
+
* tickets and dispatches them up to the concurrency cap.
|
|
105
|
+
*/
|
|
106
|
+
dispatch?: {
|
|
107
|
+
/** Opt-in: enable auto-dispatch for this project (default: off). */
|
|
108
|
+
enabled?: boolean;
|
|
109
|
+
/** Per-project concurrency cap for auto-dispatched agents. */
|
|
110
|
+
maxAgents?: number;
|
|
111
|
+
/** Optional provider pin: 'rush' | 'codex' | 'factory' | 'host' | … */
|
|
112
|
+
provider?: string;
|
|
113
|
+
/** For provider='host': which machine to dispatch onto (name/device/cap tag). */
|
|
114
|
+
host?: string;
|
|
115
|
+
};
|
|
100
116
|
}
|
|
101
117
|
/** A project name safe to use as a filename: no separators, `..`, or leading dot. */
|
|
102
118
|
export declare function isSafeProjectName(name: string): boolean;
|
|
@@ -124,6 +140,7 @@ export declare function listProjectDefs(): ProjectDef[];
|
|
|
124
140
|
/**
|
|
125
141
|
* Persist a project definition, normalizing `root`/`defaultPath` to home-relative
|
|
126
142
|
* so it stays portable across machines. Creates the projects dir on first write.
|
|
143
|
+
* Writes via temp+rename so readers never see a partial file.
|
|
127
144
|
*/
|
|
128
145
|
export declare function writeProjectDef(def: ProjectDef): string;
|
|
129
146
|
/** Delete a project definition. Returns true if a file was removed. Never touches the repo. */
|
package/dist/lib/projects.js
CHANGED
|
@@ -34,6 +34,7 @@ import { getProjectsDir } from './state.js';
|
|
|
34
34
|
import { safeJoin } from './paths.js';
|
|
35
35
|
import { toHomeRelative, expandLocalHome } from './project-root.js';
|
|
36
36
|
import { resolveProjectKey } from './project-key.js';
|
|
37
|
+
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
37
38
|
/** A project name safe to use as a filename: no separators, `..`, or leading dot. */
|
|
38
39
|
export function isSafeProjectName(name) {
|
|
39
40
|
return (typeof name === 'string' &&
|
|
@@ -155,9 +156,23 @@ export function validateProjectDef(raw, sourceName) {
|
|
|
155
156
|
def.linear.projectId = l.projectId;
|
|
156
157
|
if (typeof l.url === 'string')
|
|
157
158
|
def.linear.url = l.url;
|
|
159
|
+
if (typeof l.name === 'string')
|
|
160
|
+
def.linear.name = l.name;
|
|
158
161
|
}
|
|
159
162
|
if (Array.isArray(o.docs))
|
|
160
163
|
def.docs = o.docs.filter((d) => typeof d === 'string');
|
|
164
|
+
if (o.dispatch && typeof o.dispatch === 'object' && !Array.isArray(o.dispatch)) {
|
|
165
|
+
const d = o.dispatch;
|
|
166
|
+
def.dispatch = {};
|
|
167
|
+
if (d.enabled === true || d.enabled === false)
|
|
168
|
+
def.dispatch.enabled = d.enabled;
|
|
169
|
+
if (typeof d.maxAgents === 'number' && Number.isFinite(d.maxAgents))
|
|
170
|
+
def.dispatch.maxAgents = d.maxAgents;
|
|
171
|
+
if (typeof d.provider === 'string')
|
|
172
|
+
def.dispatch.provider = d.provider;
|
|
173
|
+
if (typeof d.host === 'string')
|
|
174
|
+
def.dispatch.host = d.host;
|
|
175
|
+
}
|
|
161
176
|
return def;
|
|
162
177
|
}
|
|
163
178
|
/**
|
|
@@ -210,6 +225,7 @@ export function listProjectDefs() {
|
|
|
210
225
|
/**
|
|
211
226
|
* Persist a project definition, normalizing `root`/`defaultPath` to home-relative
|
|
212
227
|
* so it stays portable across machines. Creates the projects dir on first write.
|
|
228
|
+
* Writes via temp+rename so readers never see a partial file.
|
|
213
229
|
*/
|
|
214
230
|
export function writeProjectDef(def) {
|
|
215
231
|
const validated = validateProjectDef(def, def.name);
|
|
@@ -230,7 +246,7 @@ export function writeProjectDef(def) {
|
|
|
230
246
|
const clean = Object.fromEntries(Object.entries(normalized).filter(([, v]) => v !== undefined));
|
|
231
247
|
const target = projectDefPath(def.name);
|
|
232
248
|
fs.mkdirSync(getProjectsDir(), { recursive: true });
|
|
233
|
-
|
|
249
|
+
atomicWriteFileSync(target, yaml.stringify(clean), 'utf8');
|
|
234
250
|
return target;
|
|
235
251
|
}
|
|
236
252
|
/** Delete a project definition. Returns true if a file was removed. Never touches the repo. */
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -85,7 +85,7 @@ export declare function search(query: string, options?: {
|
|
|
85
85
|
}): Promise<RegistrySearchResult[]>;
|
|
86
86
|
/** Parse a package identifier into its type (mcp, skill, git) and name. */
|
|
87
87
|
export declare function parsePackageIdentifier(identifier: string): {
|
|
88
|
-
type: RegistryType | 'git' | 'unknown';
|
|
88
|
+
type: RegistryType | 'git' | 'plugin' | 'unknown';
|
|
89
89
|
name: string;
|
|
90
90
|
};
|
|
91
91
|
/** Resolve a package identifier to an installable package with source metadata. */
|
package/dist/lib/registry.js
CHANGED
|
@@ -387,6 +387,11 @@ export function parsePackageIdentifier(identifier) {
|
|
|
387
387
|
if (identifier.startsWith('skill:')) {
|
|
388
388
|
return { type: 'skill', name: identifier.slice(6) };
|
|
389
389
|
}
|
|
390
|
+
// plugin:name@source | plugin:/path | plugin:gh:user/repo — Phase 5 packaging
|
|
391
|
+
// umbrella. Spec after the prefix is the same grammar as `agents plugins install`.
|
|
392
|
+
if (identifier.startsWith('plugin:')) {
|
|
393
|
+
return { type: 'plugin', name: identifier.slice('plugin:'.length) };
|
|
394
|
+
}
|
|
390
395
|
// gh:user/repo -> git source
|
|
391
396
|
if (identifier.startsWith('gh:')) {
|
|
392
397
|
return { type: 'git', name: identifier };
|
|
@@ -412,6 +417,12 @@ export function parsePackageIdentifier(identifier) {
|
|
|
412
417
|
/** Resolve a package identifier to an installable package with source metadata. */
|
|
413
418
|
export async function resolvePackage(identifier) {
|
|
414
419
|
const parsed = parsePackageIdentifier(identifier);
|
|
420
|
+
if (parsed.type === 'plugin') {
|
|
421
|
+
const spec = parsed.name.trim();
|
|
422
|
+
if (!spec)
|
|
423
|
+
return null;
|
|
424
|
+
return { type: 'plugin', source: spec, pluginSpec: spec };
|
|
425
|
+
}
|
|
415
426
|
if (parsed.type === 'git') {
|
|
416
427
|
return { type: 'git', source: parsed.name };
|
|
417
428
|
}
|