@phnx-labs/agents-cli 1.22.11 → 1.22.12
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 +38 -0
- 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/view.d.ts +1 -1
- package/dist/commands/view.js +7 -7
- 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.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/package.json +1 -1
package/dist/lib/migrate.js
CHANGED
|
@@ -998,7 +998,6 @@ function migrateRuntimeToHistory() {
|
|
|
998
998
|
moveDirOnce(path.join(USER_DIR, '.backups'), path.join(HISTORY_DIR, 'backups'));
|
|
999
999
|
moveDirOnce(path.join(USER_DIR, 'routines', 'runs'), path.join(HISTORY_DIR, 'runs'));
|
|
1000
1000
|
moveDirOnce(path.join(USER_DIR, 'teams', 'agents'), path.join(HISTORY_DIR, 'teams', 'agents'));
|
|
1001
|
-
migrateEventLogsToHistory();
|
|
1002
1001
|
// Drop any empty leftover skeletons created mid-rename (e.g. `versions/<agent>/<v>/home/`
|
|
1003
1002
|
// recreated by a concurrent process). The real data is already under .history/.
|
|
1004
1003
|
rmEmptyDirTree(path.join(USER_DIR, 'versions'));
|
|
@@ -1014,66 +1013,6 @@ function migrateRuntimeToHistory() {
|
|
|
1014
1013
|
catch { /* best-effort */ }
|
|
1015
1014
|
}
|
|
1016
1015
|
}
|
|
1017
|
-
/** Move the operational event stream out of the git-backed user-repo root. */
|
|
1018
|
-
function migrateEventLogsToHistory() {
|
|
1019
|
-
const destination = path.join(HISTORY_DIR, 'events');
|
|
1020
|
-
let files = [];
|
|
1021
|
-
try {
|
|
1022
|
-
files = fs.readdirSync(USER_DIR).filter((file) => file === 'events.jsonl' || /^events\.\d+\.jsonl\.gz$/.test(file));
|
|
1023
|
-
}
|
|
1024
|
-
catch {
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
try {
|
|
1028
|
-
fs.mkdirSync(destination, { recursive: true, mode: 0o700 });
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
const active = files.find((file) => file === 'events.jsonl');
|
|
1034
|
-
if (active) {
|
|
1035
|
-
const src = path.join(USER_DIR, active);
|
|
1036
|
-
const dest = path.join(destination, active);
|
|
1037
|
-
if (!fs.existsSync(dest)) {
|
|
1038
|
-
moveFileOnce(src, dest);
|
|
1039
|
-
}
|
|
1040
|
-
else {
|
|
1041
|
-
try {
|
|
1042
|
-
const lines = [fs.readFileSync(src, 'utf-8'), fs.readFileSync(dest, 'utf-8')]
|
|
1043
|
-
.flatMap((content) => content.split('\n').filter(Boolean))
|
|
1044
|
-
.map((line, index) => {
|
|
1045
|
-
try {
|
|
1046
|
-
const timestamp = Date.parse(JSON.parse(line).ts ?? '');
|
|
1047
|
-
return { line, index, timestamp: Number.isNaN(timestamp) ? Number.MAX_SAFE_INTEGER : timestamp };
|
|
1048
|
-
}
|
|
1049
|
-
catch {
|
|
1050
|
-
return { line, index, timestamp: Number.MAX_SAFE_INTEGER };
|
|
1051
|
-
}
|
|
1052
|
-
})
|
|
1053
|
-
.sort((a, b) => a.timestamp - b.timestamp || a.index - b.index)
|
|
1054
|
-
.map(({ line }) => line);
|
|
1055
|
-
atomicWriteFileSync(dest, `${lines.join('\n')}\n`, { mode: 0o600 });
|
|
1056
|
-
fs.unlinkSync(src);
|
|
1057
|
-
}
|
|
1058
|
-
catch { /* preserve the source for a later retry */ }
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
const archives = files
|
|
1062
|
-
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
1063
|
-
.filter((entry) => entry.match !== null)
|
|
1064
|
-
.sort((a, b) => Number(a.match[1]) - Number(b.match[1]));
|
|
1065
|
-
let nextArchive = fs.readdirSync(destination).reduce((max, file) => {
|
|
1066
|
-
const match = file.match(/^events\.(\d+)\.jsonl\.gz$/);
|
|
1067
|
-
return match ? Math.max(max, Number(match[1])) : max;
|
|
1068
|
-
}, 0) + 1;
|
|
1069
|
-
for (const archive of archives) {
|
|
1070
|
-
const preferred = path.join(destination, archive.file);
|
|
1071
|
-
const dest = fs.existsSync(preferred)
|
|
1072
|
-
? path.join(destination, `events.${nextArchive++}.jsonl.gz`)
|
|
1073
|
-
: preferred;
|
|
1074
|
-
moveFileOnce(path.join(USER_DIR, archive.file), dest);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
1016
|
/**
|
|
1078
1017
|
* Restore plugins from the cache bucket back to the user-root.
|
|
1079
1018
|
*
|
|
@@ -1694,7 +1633,7 @@ function containsOnlyDsStore(dir) {
|
|
|
1694
1633
|
function warnSystemOrphans() {
|
|
1695
1634
|
const SHIPPED_ALLOWLIST = new Set([
|
|
1696
1635
|
// resource directories shipped by the npm package
|
|
1697
|
-
'commands', 'hooks', 'skills', 'rules', 'mcp', '
|
|
1636
|
+
'commands', 'hooks', 'skills', 'rules', 'mcp', 'clis', 'permissions', 'subagents', 'profiles', 'agents', 'routines',
|
|
1698
1637
|
// top-level metadata files
|
|
1699
1638
|
'agents.yaml', 'hooks.yaml', 'README.md', 'CHANGELOG.md',
|
|
1700
1639
|
// git + repo metadata
|
|
@@ -2074,10 +2013,35 @@ export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_
|
|
|
2074
2013
|
catch { /* already gone */ }
|
|
2075
2014
|
console.error('Migrated watchdog: legacy enable sentinel → watchdog routine (kept enabled)');
|
|
2076
2015
|
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Rename cli/ → clis/ in each of the given `.agents/` directories.
|
|
2018
|
+
*
|
|
2019
|
+
* One-way, idempotent: no-op when src is absent. Throws when both
|
|
2020
|
+
* `<dir>/cli` and `<dir>/clis` exist — the user must resolve the conflict
|
|
2021
|
+
* manually before proceeding. Exported for unit-testing with temp dirs.
|
|
2022
|
+
*/
|
|
2023
|
+
export function migrateCliDirToClis(agentsDirs) {
|
|
2024
|
+
for (const agentsDir of agentsDirs) {
|
|
2025
|
+
const src = path.join(agentsDir, 'cli');
|
|
2026
|
+
const dest = path.join(agentsDir, 'clis');
|
|
2027
|
+
if (!fs.existsSync(src))
|
|
2028
|
+
continue;
|
|
2029
|
+
if (fs.existsSync(dest)) {
|
|
2030
|
+
throw new Error(`Migration conflict: both ${src} and ${dest} exist. ` +
|
|
2031
|
+
`Remove or merge the old cli/ directory into clis/ manually.`);
|
|
2032
|
+
}
|
|
2033
|
+
fs.renameSync(src, dest);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2077
2036
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
2078
2037
|
export async function runMigration() {
|
|
2079
2038
|
// MUST run first: every other migrator reads SYSTEM_DIR (the new path).
|
|
2080
2039
|
foldLegacySystemRepo();
|
|
2040
|
+
const cliMigrateDirs = [USER_DIR, SYSTEM_DIR];
|
|
2041
|
+
const projectDotAgents = path.join(process.cwd(), '.agents');
|
|
2042
|
+
if (fs.existsSync(projectDotAgents))
|
|
2043
|
+
cliMigrateDirs.push(projectDotAgents);
|
|
2044
|
+
migrateCliDirToClis(cliMigrateDirs);
|
|
2081
2045
|
migrateAgentsYaml();
|
|
2082
2046
|
deleteSystemPromptsJson();
|
|
2083
2047
|
migrateSystemConfigJson();
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.12",
|
|
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",
|