@phnx-labs/agents-cli 1.22.11 → 1.22.13
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 +44 -0
- package/README.md +5 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/cli.js +12 -12
- package/dist/commands/events.d.ts +20 -1
- package/dist/commands/events.js +71 -55
- package/dist/commands/logs.js +14 -138
- package/dist/commands/repo.d.ts +1 -1
- package/dist/commands/repo.js +3 -3
- package/dist/commands/sessions.d.ts +15 -0
- package/dist/commands/sessions.js +79 -10
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.js +7 -7
- package/dist/commands/workflows.js +1 -0
- package/dist/lib/cli-resources.js +2 -2
- package/dist/lib/event-stream.d.ts +1 -1
- package/dist/lib/event-stream.js +1 -1
- package/dist/lib/events.d.ts +21 -12
- package/dist/lib/events.js +312 -65
- package/dist/lib/exec.js +1 -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/migrate.d.ts +8 -0
- package/dist/lib/migrate.js +26 -62
- package/dist/lib/resources/workflows.js +41 -21
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/runner.js +1 -2
- 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/audit.js +1 -1
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +60 -2
- package/dist/lib/watchdog/runner.d.ts +13 -0
- package/dist/lib/watchdog/runner.js +75 -6
- package/dist/lib/workflows.d.ts +22 -4
- package/dist/lib/workflows.js +79 -29
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { getProjectAgentsDir, getUserWorkflowsDir, getSystemWorkflowsDir, getEnabledExtraRepos, } from '../state.js';
|
|
11
|
-
import { parseWorkflowFrontmatter, countWorkflowSubagents, listPluginWorkflowDirs,
|
|
11
|
+
import { parseWorkflowFrontmatter, countWorkflowSubagents, listPluginWorkflowDirs, resolveWorkflowRef, parseWorkflowRef, } from '../workflows.js';
|
|
12
12
|
function getLayerDirs(cwd) {
|
|
13
13
|
const projectDir = getProjectAgentsDir(cwd);
|
|
14
14
|
const extraRepos = getEnabledExtraRepos();
|
|
@@ -47,6 +47,27 @@ function orderedWorkflowSearchDirs(cwd) {
|
|
|
47
47
|
out.push({ dir: dirs.system, layer: 'system' });
|
|
48
48
|
return out;
|
|
49
49
|
}
|
|
50
|
+
/** Map a resolved absolute workflow dir to its origin layer (best-effort). */
|
|
51
|
+
function layerForWorkflowPath(workflowPath, cwd) {
|
|
52
|
+
const abs = path.resolve(workflowPath);
|
|
53
|
+
const under = (root) => {
|
|
54
|
+
const r = path.resolve(root);
|
|
55
|
+
return abs === r || abs.startsWith(r + path.sep);
|
|
56
|
+
};
|
|
57
|
+
const projectDir = getProjectAgentsDir(cwd);
|
|
58
|
+
if (projectDir && under(path.join(projectDir, 'workflows')))
|
|
59
|
+
return 'project';
|
|
60
|
+
if (under(getUserWorkflowsDir()))
|
|
61
|
+
return 'user';
|
|
62
|
+
if (under(getSystemWorkflowsDir()))
|
|
63
|
+
return 'system';
|
|
64
|
+
for (const extra of getEnabledExtraRepos()) {
|
|
65
|
+
if (under(path.join(extra.dir, 'workflows')))
|
|
66
|
+
return 'system';
|
|
67
|
+
}
|
|
68
|
+
// Plugin marketplaces (and anything else plugin-shaped).
|
|
69
|
+
return 'plugin';
|
|
70
|
+
}
|
|
50
71
|
class WorkflowsHandlerImpl {
|
|
51
72
|
kind = 'workflow';
|
|
52
73
|
listAll(_agent, cwd) {
|
|
@@ -76,27 +97,26 @@ class WorkflowsHandlerImpl {
|
|
|
76
97
|
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
77
98
|
}
|
|
78
99
|
resolve(_agent, name, cwd) {
|
|
79
|
-
//
|
|
80
|
-
|
|
100
|
+
// Delegate to resolveWorkflowRef so bare, workflow:, and name@source share one path.
|
|
101
|
+
const workflowPath = resolveWorkflowRef(name, cwd ?? process.cwd());
|
|
102
|
+
if (!workflowPath)
|
|
81
103
|
return null;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
104
|
+
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
105
|
+
if (!fm)
|
|
106
|
+
return null;
|
|
107
|
+
const parsed = parseWorkflowRef(name);
|
|
108
|
+
const displayName = parsed?.name ?? path.basename(workflowPath);
|
|
109
|
+
return {
|
|
110
|
+
name: displayName,
|
|
111
|
+
item: {
|
|
112
|
+
name: fm.name || displayName,
|
|
113
|
+
description: fm.description,
|
|
114
|
+
model: fm.model,
|
|
115
|
+
subagentCount: countWorkflowSubagents(workflowPath),
|
|
116
|
+
},
|
|
117
|
+
layer: layerForWorkflowPath(workflowPath, cwd),
|
|
118
|
+
path: workflowPath,
|
|
119
|
+
};
|
|
100
120
|
}
|
|
101
121
|
sync(_agent, _versionHome, _cwd) {
|
|
102
122
|
// Version-home copies are written by syncResourcesToVersion in versions.ts.
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { AgentId } from './types.js';
|
|
6
6
|
import { type SkillParseError } from './skills.js';
|
|
7
7
|
/** Resource kind — matches the subdirectory name under each repo root. */
|
|
8
|
-
export type ResourceKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | '
|
|
8
|
+
export type ResourceKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'clis' | 'permissions' | 'subagents' | 'workflows' | 'profiles' | 'secrets';
|
|
9
9
|
/** A resource resolved with its origin. */
|
|
10
10
|
export interface ResolvedResource {
|
|
11
11
|
name: string;
|
package/dist/lib/runner.js
CHANGED
|
@@ -21,7 +21,7 @@ import { resolveJobPrompt, parseTimeout, writeRunMeta, listRuns, getJobRunsDir,
|
|
|
21
21
|
import { getRunsDir } from './state.js';
|
|
22
22
|
import { prepareJobHome, buildSpawnEnv, getJobHomePath } from './sandbox.js';
|
|
23
23
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
24
|
-
import { createTimer,
|
|
24
|
+
import { createTimer, redactPrompt } from './events.js';
|
|
25
25
|
import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, detectAuthFailure, isAuthFailureFromLog, authFailureReason, } from './exec.js';
|
|
26
26
|
import { resolveActor } from './actor.js';
|
|
27
27
|
import { loadTask as loadHostTask } from './hosts/tasks.js';
|
|
@@ -691,7 +691,6 @@ export async function executeJob(config, deps) {
|
|
|
691
691
|
if (config.command) {
|
|
692
692
|
return executeCommandJobForeground(config);
|
|
693
693
|
}
|
|
694
|
-
maybeRotate();
|
|
695
694
|
const launch = await resolveRoutineLaunch(config);
|
|
696
695
|
const primaryVersion = launch.chain[0]?.version ?? config.version;
|
|
697
696
|
const timer = createTimer('agent.run', {
|
|
Binary file
|
|
Binary file
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This is the ONE write path for secret events. Every path that creates,
|
|
5
5
|
* imports, exports, views, reads a VALUE from, or unlocks a bundle funnels its
|
|
6
6
|
* audit through here, so the operational event stream — `agents events`, backed
|
|
7
|
-
* by the append-only `~/.agents/.history/events
|
|
7
|
+
* by the dated append-only `~/.agents/.history/events/` audit log — carries a uniform,
|
|
8
8
|
* value-free provenance record: bundle, key NAMES, the resolving agent/harness
|
|
9
9
|
* identity, operation, source, status. The ts / host / session / caller fields
|
|
10
10
|
* are filled in by `emit()` itself. The secret VALUE is never part of the
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -113,7 +113,7 @@ export declare function getUserSecretsDir(): string;
|
|
|
113
113
|
* value-free usage telemetry (which bundle was created/imported/exported/viewed/
|
|
114
114
|
* accessed/unlocked, when, by whom), never a secret value. It is a derived index
|
|
115
115
|
* fed FROM the emitSecretAudit chokepoint alongside the append-only
|
|
116
|
-
* ~/.agents/.history/events/
|
|
116
|
+
* ~/.agents/.history/events/YYYY-MM-DD audit log — the same way sessions.db indexes session
|
|
117
117
|
* metadata off the real session flow — not a second write path.
|
|
118
118
|
*/
|
|
119
119
|
export declare function getSecretsDbPath(): string;
|
package/dist/lib/state.js
CHANGED
|
@@ -372,7 +372,7 @@ export function getUserSecretsDir() { return USER_SECRETS_DIR; }
|
|
|
372
372
|
* value-free usage telemetry (which bundle was created/imported/exported/viewed/
|
|
373
373
|
* accessed/unlocked, when, by whom), never a secret value. It is a derived index
|
|
374
374
|
* fed FROM the emitSecretAudit chokepoint alongside the append-only
|
|
375
|
-
* ~/.agents/.history/events/
|
|
375
|
+
* ~/.agents/.history/events/YYYY-MM-DD audit log — the same way sessions.db indexes session
|
|
376
376
|
* metadata off the real session flow — not a second write path.
|
|
377
377
|
*/
|
|
378
378
|
export function getSecretsDbPath() {
|
|
@@ -794,6 +794,59 @@ function writeIfChanged(filePath, content) {
|
|
|
794
794
|
* All callers funnel through writeMeta → here, so nothing else changes. Empty
|
|
795
795
|
* `agents:` / `versions:` are not written (no empty committed files).
|
|
796
796
|
*/
|
|
797
|
+
/**
|
|
798
|
+
* Every `Meta` key, classified as `central` (synced via agents.yaml) or `device`
|
|
799
|
+
* (routed to the per-machine device file by {@link writeMetaUnlocked}). The
|
|
800
|
+
* `Record<keyof Meta, …>` type makes this EXHAUSTIVE: adding a field to `Meta`
|
|
801
|
+
* without classifying it here is a compile error. That guarantee is what lets
|
|
802
|
+
* `serializeCentral` delete only keys THIS version models — a key a newer CLI
|
|
803
|
+
* version added (absent from this map) is preserved verbatim instead of being
|
|
804
|
+
* dropped and synced fleet-wide as data loss. This is the fix for the recurring
|
|
805
|
+
* agents.yaml key-loss (beta flags, `notify.owner`, and `feed:` all vanished
|
|
806
|
+
* this way when an older-versioned CLI on the fleet rewrote the file).
|
|
807
|
+
*/
|
|
808
|
+
const META_KEY_SCOPE = {
|
|
809
|
+
// Device-local — writeMetaUnlocked destructures these out; never in central.
|
|
810
|
+
agents: 'device',
|
|
811
|
+
isolatedAgents: 'device',
|
|
812
|
+
versions: 'device',
|
|
813
|
+
defaultBrowserProfile: 'device',
|
|
814
|
+
deviceConfig: 'device',
|
|
815
|
+
// Central — synced via agents.yaml.
|
|
816
|
+
run: 'central',
|
|
817
|
+
model: 'central',
|
|
818
|
+
watchdog: 'central',
|
|
819
|
+
lease: 'central',
|
|
820
|
+
secrets: 'central',
|
|
821
|
+
budget: 'central',
|
|
822
|
+
feed: 'central',
|
|
823
|
+
beta: 'central',
|
|
824
|
+
registries: 'central',
|
|
825
|
+
profiles: 'central',
|
|
826
|
+
source: 'central',
|
|
827
|
+
projectRoot: 'central',
|
|
828
|
+
extraRepos: 'central',
|
|
829
|
+
brands: 'central',
|
|
830
|
+
actors: 'central',
|
|
831
|
+
seededPresets: 'central',
|
|
832
|
+
hooks: 'central',
|
|
833
|
+
browser: 'central',
|
|
834
|
+
config: 'central',
|
|
835
|
+
hosts: 'central',
|
|
836
|
+
fleet: 'central',
|
|
837
|
+
share: 'central',
|
|
838
|
+
notify: 'central',
|
|
839
|
+
routines: 'central',
|
|
840
|
+
};
|
|
841
|
+
/**
|
|
842
|
+
* Every key this version models (central + device). serializeCentral deletes an
|
|
843
|
+
* on-disk key only when it is KNOWN and absent from the write's in-memory object:
|
|
844
|
+
* a central key the caller cleared, OR a device key that is legacy cruft in the
|
|
845
|
+
* synced file (device keys are routed to the per-machine file, so one lingering
|
|
846
|
+
* in central is stale and must be migrated out). A key NOT listed here — e.g. one
|
|
847
|
+
* a newer CLI version added — is preserved verbatim, never dropped + synced away.
|
|
848
|
+
*/
|
|
849
|
+
const KNOWN_META_KEYS = new Set(Object.keys(META_KEY_SCOPE));
|
|
797
850
|
/**
|
|
798
851
|
* Serialize the central (synced) meta to `agents.yaml` WITHOUT destroying the
|
|
799
852
|
* hand-written comments in the committed file.
|
|
@@ -834,7 +887,12 @@ function serializeCentral(central) {
|
|
|
834
887
|
}
|
|
835
888
|
}
|
|
836
889
|
for (const k of Object.keys(current)) {
|
|
837
|
-
|
|
890
|
+
// Only delete a key THIS version knows about. A key not in KNOWN_META_KEYS
|
|
891
|
+
// (e.g. one a newer CLI version added) is preserved verbatim — deleting it
|
|
892
|
+
// here would drop it and sync the deletion fleet-wide (the agents.yaml
|
|
893
|
+
// config data-loss bug). A known device key lingering in the synced file is
|
|
894
|
+
// still removed — it belongs in the per-machine file, not here.
|
|
895
|
+
if (!(k in central) && KNOWN_META_KEYS.has(k)) {
|
|
838
896
|
doc.delete(k);
|
|
839
897
|
changed = true;
|
|
840
898
|
}
|
|
@@ -97,6 +97,11 @@ export interface WatchdogTickOptions {
|
|
|
97
97
|
dryRun?: boolean;
|
|
98
98
|
enter?: boolean;
|
|
99
99
|
}) => Promise<InjectResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Publish a declared block on the owner's feed. Default publishBlock() — tests
|
|
102
|
+
* inject a collector so no real feed dir is touched.
|
|
103
|
+
*/
|
|
104
|
+
publishBlockFn?: (block: OpenBlock) => void;
|
|
100
105
|
/** Override the canonical watchdog.log path (tests point at a tmp file). */
|
|
101
106
|
logPath?: string;
|
|
102
107
|
/**
|
|
@@ -195,6 +200,14 @@ export interface NudgeDecision {
|
|
|
195
200
|
nudge: boolean;
|
|
196
201
|
reason: string;
|
|
197
202
|
text?: string;
|
|
203
|
+
/**
|
|
204
|
+
* Set to `true` when the brain (smart decider) explicitly concluded the session
|
|
205
|
+
* needs the human — as opposed to a cheap deterministic skip (session is done,
|
|
206
|
+
* not stalled, etc.). Only `true` when the brain escalation path ran and returned
|
|
207
|
+
* `nudge: false`. The watchdog uses this to gate the self-file reminder: a
|
|
208
|
+
* "session is done" skip does NOT trigger a reminder.
|
|
209
|
+
*/
|
|
210
|
+
needsHuman?: boolean;
|
|
198
211
|
}
|
|
199
212
|
/**
|
|
200
213
|
* The smart brain. Resolves a `watchdog` workflow for the session's cwd
|
|
@@ -44,7 +44,7 @@ import { withFileLock, atomicWriteFileSync, ensureLockTarget } from '../fs-atomi
|
|
|
44
44
|
import { resolveAnswerRoute, isOpenQuestionBlock } from '../answer-router.js';
|
|
45
45
|
import { enqueue, mailboxDir } from '../mailbox.js';
|
|
46
46
|
import { mailboxIdForActiveSession } from '../mailbox-target.js';
|
|
47
|
-
import { readBlock, blockIdForSession } from '../feed.js';
|
|
47
|
+
import { readBlock, blockIdForSession, buildDeclaredBlock, publishBlock } from '../feed.js';
|
|
48
48
|
import { summarizeWatchdogTail } from './watchdogTail.js';
|
|
49
49
|
import { appendWatchdogEvents } from './log.js';
|
|
50
50
|
import { buildRotateLaunchCommand, buildRotateReplayText, classifyTailForRotate, defaultRotateGate, defaultTuiLiveFor, exitSequenceFor, isInflightPhase, isWatchdogRotateEnabled, listInflightRotates, readRotateState, recordRotateSkip, shouldLogRotateSkip, writeRotateState, DEFAULT_ROTATE_READINESS_MS, DEFAULT_ROTATE_SKIP_COOLDOWN_MS, DEFAULT_ROTATE_FAILED_COOLDOWN_MS, } from './rotate.js';
|
|
@@ -353,6 +353,7 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
353
353
|
const smartDecider = opts.smartDecider ?? makeDefaultSmartDecider(opts.smartAgent ?? 'claude');
|
|
354
354
|
const openBlockFor = opts.openBlockFor ?? defaultOpenBlockFor;
|
|
355
355
|
const injectFn = opts.injectFn ?? injectIntoTerminal;
|
|
356
|
+
const publishBlockFn = opts.publishBlockFn ?? publishBlock;
|
|
356
357
|
// Rotate seams (watchdog/rotate.ts). The config is read fresh per tick
|
|
357
358
|
// (readMeta is mtime-cached), so a `watchdog.rotate` flip is honored on the
|
|
358
359
|
// next pass. The default readiness probe (defaultTuiLiveFor) treats the
|
|
@@ -645,13 +646,21 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
645
646
|
// forces every stalled candidate through the brain.
|
|
646
647
|
let decision;
|
|
647
648
|
if (opts.smart) {
|
|
648
|
-
|
|
649
|
+
const d = await smartDecider(session, candidate);
|
|
650
|
+
// Mark needsHuman when the brain explicitly concluded "leave for human".
|
|
651
|
+
decision = d.nudge ? d : { ...d, needsHuman: true };
|
|
649
652
|
}
|
|
650
653
|
else {
|
|
651
654
|
const det = deterministicDecision(session, candidate);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
+
if (det.kind === 'escalate') {
|
|
656
|
+
const d = await smartDecider(session, candidate);
|
|
657
|
+
// Mark needsHuman when the brain escalation path concluded "leave for human".
|
|
658
|
+
decision = d.nudge ? d : { ...d, needsHuman: true };
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// Cheap deterministic path — completion or clear stall. Never "needs human".
|
|
662
|
+
decision = { nudge: det.kind === 'nudge', reason: det.reason };
|
|
663
|
+
}
|
|
655
664
|
}
|
|
656
665
|
const chosenText = decision.text ?? nudgeText;
|
|
657
666
|
// Log the decision for the Factory watchdog card.
|
|
@@ -670,6 +679,53 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
670
679
|
lastAssistantMessage: summary.lastAssistantMessage,
|
|
671
680
|
});
|
|
672
681
|
if (!decision.nudge) {
|
|
682
|
+
// Brain explicitly concluded "leave for human" (needsHuman === true). Inject a
|
|
683
|
+
// self-file reminder into the agent's terminal so it knows to post a feed block.
|
|
684
|
+
// Cheap deterministic skips (session done, no stall) are NOT reminder-worthy —
|
|
685
|
+
// guard on needsHuman so a finished session is never poked.
|
|
686
|
+
// Gated by the same cooldown as a nudge to prevent re-firing every 2-minute tick.
|
|
687
|
+
if (decision.needsHuman) {
|
|
688
|
+
const lastNudgeMs = ledger[session.sessionId ?? ''] ?? 0;
|
|
689
|
+
const cooldownMs = thresholds.cooldownMs;
|
|
690
|
+
const withinCooldown = nowMs - lastNudgeMs < cooldownMs;
|
|
691
|
+
if (opts.nudge && session.sessionId && !withinCooldown) {
|
|
692
|
+
const existingBlock = openBlockFor(session);
|
|
693
|
+
if (existingBlock === null) {
|
|
694
|
+
// Determine addressability without planning the full nudge (no text needed).
|
|
695
|
+
const resolution = resolveInjectTargetForSession(session, { allowGhosttyFocus: opts.allowGhosttyFocus });
|
|
696
|
+
if (resolution.addressable) {
|
|
697
|
+
// Addressable → inject a reminder asking the agent to self-file a feed block.
|
|
698
|
+
const reminderText = 'You appear stuck. If you genuinely need Muqsit, file it: ' +
|
|
699
|
+
'agents feed post "<one-line ask>" --blocked --default "<safe default>". ' +
|
|
700
|
+
'Otherwise keep going.';
|
|
701
|
+
try {
|
|
702
|
+
await injectFn(resolution.target, reminderText, { dryRun: opts.injectDryRun });
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// Swallow inject errors — reminder is best-effort; flag set below.
|
|
706
|
+
}
|
|
707
|
+
ledgerUpdates[session.sessionId] = nowMs;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
// Un-addressable → we can't reach the terminal to remind it, so the ONLY
|
|
711
|
+
// way to reach Muqsit is to file a declared block on the agent's behalf.
|
|
712
|
+
// This is the most important case: the session genuinely needs the human
|
|
713
|
+
// AND the watchdog can't even nudge it. Never let it silently vanish.
|
|
714
|
+
const mailboxId = mailboxIdForActiveSession(session) ?? session.sessionId;
|
|
715
|
+
const machineHost = session.provenance?.host ?? 'unknown';
|
|
716
|
+
const runtime = session.kind;
|
|
717
|
+
try {
|
|
718
|
+
const declaredBlock = buildDeclaredBlock({ sessionId: session.sessionId, mailboxId, host: machineHost, runtime, cwd: session.cwd }, { text: `Session genuinely needs Muqsit and is un-addressable — ${decision.reason}. Needs attention.` });
|
|
719
|
+
publishBlockFn(declaredBlock);
|
|
720
|
+
ledgerUpdates[session.sessionId] = nowMs;
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
// publishBlock failure is non-fatal — best-effort owner page.
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
673
729
|
outcomes.push({ ...base, decision: 'skip', reason: decision.reason });
|
|
674
730
|
continue;
|
|
675
731
|
}
|
|
@@ -682,6 +738,12 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
682
738
|
const addressable = plan.via === 'inject' ? true : undefined;
|
|
683
739
|
if (plan.via === 'refuse') {
|
|
684
740
|
// No addressable rail and not headless-resumable — flag, NEVER guess.
|
|
741
|
+
// This branch is reached ONLY for a nudge-worthy (decision.nudge === true)
|
|
742
|
+
// drive-forward poke, which is NEVER needsHuman (needsHuman is set only when
|
|
743
|
+
// decision.nudge === false). So we do NOT page the owner here — a short "just
|
|
744
|
+
// needs a poke" stall must not text Muqsit's phone. Owner-paging for an
|
|
745
|
+
// un-addressable session happens only on the confirmed needsHuman skip path
|
|
746
|
+
// above. Here we only flag it for the tray.
|
|
685
747
|
flags[session.sessionId] = { reason: plan.reason, host: session.host, atMs: nowMs };
|
|
686
748
|
outcomes.push({
|
|
687
749
|
...base, decision: 'skip', addressable: false,
|
|
@@ -690,7 +752,14 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
690
752
|
});
|
|
691
753
|
continue;
|
|
692
754
|
}
|
|
693
|
-
// handsoff = detect + flag, but never deliver.
|
|
755
|
+
// handsoff = detect + flag, but never deliver via inject/mailbox.
|
|
756
|
+
// This branch is reached ONLY for a nudge-worthy (decision.nudge === true)
|
|
757
|
+
// drive-forward poke, which is NEVER needsHuman. A hands-off policy means "don't
|
|
758
|
+
// nudge it forward" — it must NOT translate into paging Muqsit for a poke. So we
|
|
759
|
+
// only flag it for the tray, no owner page. A genuinely needs-human session (even
|
|
760
|
+
// under hands-off) is paged by the confirmed-needsHuman skip path above, which
|
|
761
|
+
// does not consult policy — hands-off silences the forward nudge, not the
|
|
762
|
+
// "it's actually stuck" signal.
|
|
694
763
|
if (policy === 'handsoff') {
|
|
695
764
|
flags[session.sessionId] = {
|
|
696
765
|
reason: `handsoff: would nudge via ${viaLabel(plan)} but policy is hands-off`,
|
package/dist/lib/workflows.d.ts
CHANGED
|
@@ -318,19 +318,37 @@ export declare function grokWorkflowMarker(filePath: string): string | null;
|
|
|
318
318
|
* runnable via `agents run <name>` without a separate install into
|
|
319
319
|
* ~/.agents/workflows/ (Phase 5 packaging). Within the plugin band, project
|
|
320
320
|
* plugins beat user/system plugins (same first-hit-wins as other layers).
|
|
321
|
+
*
|
|
322
|
+
* Pass `pluginName` to restrict to one plugin (for `name@plugin` resolution).
|
|
321
323
|
*/
|
|
322
|
-
export declare function listPluginWorkflowDirs(cwd?: string): string[];
|
|
324
|
+
export declare function listPluginWorkflowDirs(cwd?: string, pluginName?: string): string[];
|
|
323
325
|
/**
|
|
324
|
-
* True when `ref` is a single bare workflow
|
|
325
|
-
* Name lookup must not path-join multi-segment
|
|
326
|
+
* True when `ref` is a single bare workflow / source identifier (no path
|
|
327
|
+
* separators, no `..`, no `@`). Name lookup must not path-join multi-segment
|
|
328
|
+
* or traversal refs into search roots. `name@source` is parsed separately.
|
|
326
329
|
*/
|
|
327
330
|
export declare function isBareWorkflowName(ref: string): boolean;
|
|
331
|
+
/** Parsed `agents run` workflow reference (docs/07-entrypoints). */
|
|
332
|
+
export interface ParsedWorkflowRef {
|
|
333
|
+
/** Workflow directory name (WORKFLOW.md parent). */
|
|
334
|
+
name: string;
|
|
335
|
+
/**
|
|
336
|
+
* When set (`name@source`), pin resolution to that source only:
|
|
337
|
+
* a plugin name, or an enabled extra-repo alias.
|
|
338
|
+
*/
|
|
339
|
+
source?: string;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Parse a workflow run target: optional `workflow:` type prefix and optional
|
|
343
|
+
* `@source` pin. Returns null when the form is not a valid name lookup.
|
|
344
|
+
*/
|
|
345
|
+
export declare function parseWorkflowRef(ref: string): ParsedWorkflowRef | null;
|
|
328
346
|
/**
|
|
329
347
|
* Resolve an `agents run <workflow>` reference.
|
|
330
348
|
*
|
|
331
349
|
* Directories are accepted anywhere on disk when they contain WORKFLOW.md.
|
|
332
350
|
* Name lookup precedence (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
333
|
-
*
|
|
351
|
+
* Pin a source with `name@plugin` or `name@extra-alias` (optional `workflow:` prefix).
|
|
334
352
|
*/
|
|
335
353
|
export declare function resolveWorkflowRef(ref: string, cwd?: string): string | null;
|
|
336
354
|
/**
|
package/dist/lib/workflows.js
CHANGED
|
@@ -522,15 +522,8 @@ function resolveWorkflowPath(ref, cwd) {
|
|
|
522
522
|
const candidate = path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
|
|
523
523
|
return isWorkflowDir(candidate) ? candidate : null;
|
|
524
524
|
}
|
|
525
|
-
/**
|
|
526
|
-
|
|
527
|
-
* extra). Used by name resolution and listing so a plugin-packaged workflow is
|
|
528
|
-
* runnable via `agents run <name>` without a separate install into
|
|
529
|
-
* ~/.agents/workflows/ (Phase 5 packaging). Within the plugin band, project
|
|
530
|
-
* plugins beat user/system plugins (same first-hit-wins as other layers).
|
|
531
|
-
*/
|
|
532
|
-
export function listPluginWorkflowDirs(cwd = process.cwd()) {
|
|
533
|
-
const pluginRoots = [];
|
|
525
|
+
/** Plugin-root marketplace dirs in project → user → system → extra order. */
|
|
526
|
+
function pluginMarketplaceDirs(cwd) {
|
|
534
527
|
const pluginsDirs = [];
|
|
535
528
|
const projectPlugins = getProjectPluginsDir(cwd);
|
|
536
529
|
if (projectPlugins)
|
|
@@ -539,7 +532,34 @@ export function listPluginWorkflowDirs(cwd = process.cwd()) {
|
|
|
539
532
|
for (const extra of getEnabledExtraRepos()) {
|
|
540
533
|
pluginsDirs.push(path.join(extra.dir, 'plugins'));
|
|
541
534
|
}
|
|
542
|
-
|
|
535
|
+
return pluginsDirs;
|
|
536
|
+
}
|
|
537
|
+
function isPluginDirectory(pluginRoot, entry) {
|
|
538
|
+
if (entry.name.startsWith('.'))
|
|
539
|
+
return false;
|
|
540
|
+
let isDir = entry.isDirectory();
|
|
541
|
+
if (!isDir && entry.isSymbolicLink()) {
|
|
542
|
+
try {
|
|
543
|
+
isDir = fs.statSync(pluginRoot).isDirectory();
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
isDir = false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return isDir;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Plugin `workflows/` directories in discovery order (project → user → system →
|
|
553
|
+
* extra). Used by name resolution and listing so a plugin-packaged workflow is
|
|
554
|
+
* runnable via `agents run <name>` without a separate install into
|
|
555
|
+
* ~/.agents/workflows/ (Phase 5 packaging). Within the plugin band, project
|
|
556
|
+
* plugins beat user/system plugins (same first-hit-wins as other layers).
|
|
557
|
+
*
|
|
558
|
+
* Pass `pluginName` to restrict to one plugin (for `name@plugin` resolution).
|
|
559
|
+
*/
|
|
560
|
+
export function listPluginWorkflowDirs(cwd = process.cwd(), pluginName) {
|
|
561
|
+
const pluginRoots = [];
|
|
562
|
+
for (const pluginsDir of pluginMarketplaceDirs(cwd)) {
|
|
543
563
|
if (!fs.existsSync(pluginsDir))
|
|
544
564
|
continue;
|
|
545
565
|
let entries;
|
|
@@ -550,20 +570,10 @@ export function listPluginWorkflowDirs(cwd = process.cwd()) {
|
|
|
550
570
|
continue;
|
|
551
571
|
}
|
|
552
572
|
for (const entry of entries) {
|
|
553
|
-
if (entry.name
|
|
573
|
+
if (pluginName !== undefined && entry.name !== pluginName)
|
|
554
574
|
continue;
|
|
555
|
-
// Directories and symlinks-to-directories (plugin marketplaces often symlink).
|
|
556
575
|
const pluginRoot = path.join(pluginsDir, entry.name);
|
|
557
|
-
|
|
558
|
-
if (!isDir && entry.isSymbolicLink()) {
|
|
559
|
-
try {
|
|
560
|
-
isDir = fs.statSync(pluginRoot).isDirectory();
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
isDir = false;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (!isDir)
|
|
576
|
+
if (!isPluginDirectory(pluginRoot, entry))
|
|
567
577
|
continue;
|
|
568
578
|
const workflowsDir = path.join(pluginRoot, 'workflows');
|
|
569
579
|
if (fs.existsSync(workflowsDir))
|
|
@@ -573,8 +583,9 @@ export function listPluginWorkflowDirs(cwd = process.cwd()) {
|
|
|
573
583
|
return pluginRoots;
|
|
574
584
|
}
|
|
575
585
|
/**
|
|
576
|
-
* True when `ref` is a single bare workflow
|
|
577
|
-
* Name lookup must not path-join multi-segment
|
|
586
|
+
* True when `ref` is a single bare workflow / source identifier (no path
|
|
587
|
+
* separators, no `..`, no `@`). Name lookup must not path-join multi-segment
|
|
588
|
+
* or traversal refs into search roots. `name@source` is parsed separately.
|
|
578
589
|
*/
|
|
579
590
|
export function isBareWorkflowName(ref) {
|
|
580
591
|
if (!ref || ref === '.' || ref === '..')
|
|
@@ -583,26 +594,65 @@ export function isBareWorkflowName(ref) {
|
|
|
583
594
|
return false;
|
|
584
595
|
if (ref.includes('..'))
|
|
585
596
|
return false;
|
|
597
|
+
if (ref.includes('@'))
|
|
598
|
+
return false;
|
|
586
599
|
// Reject absolute paths (posix or Windows).
|
|
587
600
|
if (path.isAbsolute(ref))
|
|
588
601
|
return false;
|
|
589
602
|
return path.basename(ref) === ref;
|
|
590
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Parse a workflow run target: optional `workflow:` type prefix and optional
|
|
606
|
+
* `@source` pin. Returns null when the form is not a valid name lookup.
|
|
607
|
+
*/
|
|
608
|
+
export function parseWorkflowRef(ref) {
|
|
609
|
+
let r = ref.trim();
|
|
610
|
+
if (r.startsWith('workflow:'))
|
|
611
|
+
r = r.slice('workflow:'.length);
|
|
612
|
+
if (!r)
|
|
613
|
+
return null;
|
|
614
|
+
const at = r.lastIndexOf('@');
|
|
615
|
+
if (at > 0) {
|
|
616
|
+
const name = r.slice(0, at);
|
|
617
|
+
const source = r.slice(at + 1);
|
|
618
|
+
if (!isBareWorkflowName(name) || !isBareWorkflowName(source))
|
|
619
|
+
return null;
|
|
620
|
+
return { name, source };
|
|
621
|
+
}
|
|
622
|
+
if (!isBareWorkflowName(r))
|
|
623
|
+
return null;
|
|
624
|
+
return { name: r };
|
|
625
|
+
}
|
|
591
626
|
/**
|
|
592
627
|
* Resolve an `agents run <workflow>` reference.
|
|
593
628
|
*
|
|
594
629
|
* Directories are accepted anywhere on disk when they contain WORKFLOW.md.
|
|
595
630
|
* Name lookup precedence (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
596
|
-
*
|
|
631
|
+
* Pin a source with `name@plugin` or `name@extra-alias` (optional `workflow:` prefix).
|
|
597
632
|
*/
|
|
598
633
|
export function resolveWorkflowRef(ref, cwd = process.cwd()) {
|
|
599
634
|
const direct = resolveWorkflowPath(ref, cwd);
|
|
600
635
|
if (direct)
|
|
601
636
|
return direct;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (!isBareWorkflowName(ref))
|
|
637
|
+
const parsed = parseWorkflowRef(ref);
|
|
638
|
+
if (!parsed)
|
|
605
639
|
return null;
|
|
640
|
+
// Source-qualified: only that plugin or extra-repo workflows/ (no layered fallback).
|
|
641
|
+
if (parsed.source) {
|
|
642
|
+
for (const dir of listPluginWorkflowDirs(cwd, parsed.source)) {
|
|
643
|
+
const workflowPath = path.join(dir, parsed.name);
|
|
644
|
+
if (isWorkflowDir(workflowPath))
|
|
645
|
+
return workflowPath;
|
|
646
|
+
}
|
|
647
|
+
for (const extra of getEnabledExtraRepos()) {
|
|
648
|
+
if (extra.alias !== parsed.source)
|
|
649
|
+
continue;
|
|
650
|
+
const workflowPath = path.join(extra.dir, 'workflows', parsed.name);
|
|
651
|
+
if (isWorkflowDir(workflowPath))
|
|
652
|
+
return workflowPath;
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
606
656
|
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
607
657
|
const searchDirs = [
|
|
608
658
|
...(projectAgentsDir ? [path.join(projectAgentsDir, 'workflows')] : []),
|
|
@@ -612,7 +662,7 @@ export function resolveWorkflowRef(ref, cwd = process.cwd()) {
|
|
|
612
662
|
getSystemWorkflowsDir(),
|
|
613
663
|
];
|
|
614
664
|
for (const dir of searchDirs) {
|
|
615
|
-
const workflowPath = path.join(dir,
|
|
665
|
+
const workflowPath = path.join(dir, parsed.name);
|
|
616
666
|
if (isWorkflowDir(workflowPath))
|
|
617
667
|
return workflowPath;
|
|
618
668
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.13",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|