@phnx-labs/agents-cli 1.22.57 → 1.22.58
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 +56 -0
- package/dist/bootstrap.js +8 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +11 -0
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -1
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/doctor-diff.js +77 -7
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +26 -133
- package/dist/lib/installations/versions.js +41 -204
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +23 -0
- package/dist/lib/self-update.js +50 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/db.d.ts +20 -1
- package/dist/lib/session/db.js +139 -9
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +15 -0
- package/dist/lib/traces/sync.js +104 -19
- package/dist/lib/traces/worker-template.js +154 -1
- package/package.json +1 -1
|
@@ -245,12 +245,17 @@ function renderRoutineRows({ jobs, scheduler, overdueSet, link, now, local = tru
|
|
|
245
245
|
: lastStatus === 'missed' ? chalk.magenta
|
|
246
246
|
: chalk.gray;
|
|
247
247
|
const overdueTag = overdueSet.has(job.name) ? chalk.yellow(' (overdue)') : '';
|
|
248
|
+
// Append the concrete reason a routine did not complete (auth_failed, wedged,
|
|
249
|
+
// blocked, missed) right in the Last Status cell, so the list answers "why"
|
|
250
|
+
// without a drill-in. Only for non-completed local runs; peer rows stay blank.
|
|
251
|
+
const reason = latestRun ? runFailureReason(latestRun) : null;
|
|
252
|
+
const reasonTag = reason ? chalk.gray(` — ${reason}`) : '';
|
|
248
253
|
const agentLabelPadded = job.command
|
|
249
254
|
? chalk.magenta('command'.padEnd(10))
|
|
250
255
|
: job.workflow
|
|
251
256
|
? chalk.magenta(`wf:${job.workflow}`.padEnd(10))
|
|
252
257
|
: (job.agent || '').padEnd(10);
|
|
253
|
-
console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${deviceCell}${' '.repeat(devicePad)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}`);
|
|
258
|
+
console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${deviceCell}${' '.repeat(devicePad)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}${reasonTag}`);
|
|
254
259
|
}
|
|
255
260
|
}
|
|
256
261
|
function parseRoutineTrigger(options) {
|
|
@@ -622,6 +627,43 @@ function routineMatchesQuery(job, q) {
|
|
|
622
627
|
.toLowerCase()
|
|
623
628
|
.includes(q);
|
|
624
629
|
}
|
|
630
|
+
/** Friendly one-liners for the claim a `skipped` run lost. */
|
|
631
|
+
const SKIP_REASON_LABEL = {
|
|
632
|
+
active_run: 'wedged: a prior run is still active',
|
|
633
|
+
duplicate_slot: 'duplicate slot (already fired)',
|
|
634
|
+
wrong_owner: 'pinned to another device',
|
|
635
|
+
};
|
|
636
|
+
/**
|
|
637
|
+
* The short, human reason a run did not simply complete — for inline display in
|
|
638
|
+
* the list/detail so "why did it fail" needs no dig into the run dir. Prefers the
|
|
639
|
+
* concrete `errorMessage` (which carries `auth_failed: …`, OAuth-revoked, timeouts),
|
|
640
|
+
* then the readiness block for a `blocked` run, then the mapped skip reason. Returns
|
|
641
|
+
* null for a healthy (`completed`/`running`) run, which needs no annotation.
|
|
642
|
+
*/
|
|
643
|
+
export function runFailureReason(run) {
|
|
644
|
+
if (run.status === 'completed' || run.status === 'running')
|
|
645
|
+
return null;
|
|
646
|
+
const compact = (s) => {
|
|
647
|
+
const one = s.replace(/\s+/g, ' ').trim();
|
|
648
|
+
return one.length > 80 ? one.slice(0, 79) + '…' : one;
|
|
649
|
+
};
|
|
650
|
+
if (run.errorMessage)
|
|
651
|
+
return compact(run.errorMessage);
|
|
652
|
+
if (run.status === 'blocked' && run.readiness) {
|
|
653
|
+
return compact(run.readiness.message || run.readiness.code);
|
|
654
|
+
}
|
|
655
|
+
if (run.skipReason)
|
|
656
|
+
return SKIP_REASON_LABEL[run.skipReason];
|
|
657
|
+
if (run.status === 'missed')
|
|
658
|
+
return 'scheduler was not running when it came due';
|
|
659
|
+
// The most common failure shape: a command/agent body that exited nonzero with
|
|
660
|
+
// no structured errorMessage (the cause is in stdout). Naming the exit code is
|
|
661
|
+
// still more than the bare status word, and tells the reader it ran and threw.
|
|
662
|
+
if ((run.status === 'failed' || run.status === 'timeout') && run.exitCode !== null && run.exitCode !== undefined) {
|
|
663
|
+
return `exit ${run.exitCode}`;
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
625
667
|
/** One compact routine row for the browser list: name · kind · schedule · next · last. */
|
|
626
668
|
function routineBrowserRow(job, scheduler, overdueSet, now) {
|
|
627
669
|
const kind = job.command ? 'command' : job.workflow ? `wf:${job.workflow}` : job.agent ?? '?';
|
|
@@ -685,7 +727,12 @@ function buildRoutineDetail(job, scheduler, now) {
|
|
|
685
727
|
? chalk.red(run.status)
|
|
686
728
|
: chalk.yellow(run.status);
|
|
687
729
|
const dur = run.completedAt ? ` ${formatRunDuration(run.startedAt, run.completedAt)}` : '';
|
|
688
|
-
|
|
730
|
+
// Surface WHY a run did not complete, inline, so "looking at status" does not
|
|
731
|
+
// require digging into the run dir. auth_failed / OAuth-revoked, a wedged
|
|
732
|
+
// active-run skip, or a readiness block all live on the RunMeta already.
|
|
733
|
+
const reason = runFailureReason(run);
|
|
734
|
+
const why = reason ? chalk.gray(` — ${reason}`) : '';
|
|
735
|
+
lines.push(` ${run.startedAt} ${status}${dur}${why}`);
|
|
689
736
|
}
|
|
690
737
|
}
|
|
691
738
|
// 4. Stats
|
|
@@ -1533,7 +1580,9 @@ export function registerRoutinesCommands(program) {
|
|
|
1533
1580
|
: run.status === 'failed'
|
|
1534
1581
|
? chalk.red(run.status)
|
|
1535
1582
|
: chalk.yellow(run.status);
|
|
1536
|
-
|
|
1583
|
+
const reason = runFailureReason(run);
|
|
1584
|
+
const why = reason ? chalk.gray(` — ${reason}`) : '';
|
|
1585
|
+
console.log(` ${run.runId} ${status} ${run.startedAt}${why}`);
|
|
1537
1586
|
}
|
|
1538
1587
|
});
|
|
1539
1588
|
routinesCmd
|
|
@@ -1856,6 +1905,11 @@ export function registerRoutinesCommands(program) {
|
|
|
1856
1905
|
chalk.gray(` ${run.startedAt}`) +
|
|
1857
1906
|
chalk.gray(formatRunDuration(run.startedAt, run.completedAt)) +
|
|
1858
1907
|
(run.exitCode !== null && run.exitCode !== undefined ? chalk.gray(` exit ${run.exitCode}`) : ''));
|
|
1908
|
+
// The structured reason (auth_failed, blocked readiness, wedged skip) — the
|
|
1909
|
+
// report/stdout tail below often buries or omits it, so name it up front.
|
|
1910
|
+
const logsReason = runFailureReason(run);
|
|
1911
|
+
if (logsReason)
|
|
1912
|
+
console.log(chalk.red('reason: ') + chalk.gray(logsReason));
|
|
1859
1913
|
console.log(chalk.gray('─'.repeat(60)));
|
|
1860
1914
|
const reportPath = path.join(getRunDir(name, runId), 'report.md');
|
|
1861
1915
|
if (fs.existsSync(reportPath)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SessionEvent, SessionMeta, TodoProgress } from '../lib/session/types.js';
|
|
2
2
|
import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
|
|
3
3
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
|
|
4
|
+
import type { FileChange } from '../lib/session/digest.js';
|
|
4
5
|
import { extractArtifacts, extractHooks, extractLinks, extractSkills } from '../lib/session/highlights.js';
|
|
5
6
|
/**
|
|
6
7
|
* Compact checklist tally for list rows and previews (RUSH-2045).
|
|
@@ -118,6 +119,16 @@ export interface SessionPreviewDigest {
|
|
|
118
119
|
backgroundShellCount?: number;
|
|
119
120
|
toolTags: string[];
|
|
120
121
|
changes: ReturnType<typeof changeCounts>;
|
|
122
|
+
/**
|
|
123
|
+
* The per-file source paths behind `changes`. `changes` keeps the roll-up
|
|
124
|
+
* counts every existing consumer already reads; `changedFiles` carries the
|
|
125
|
+
* real path + op the CLI computed at scan time (via classifyFileChanges) and
|
|
126
|
+
* used to discard — kept so a consumer that renders a per-file diff list (the
|
|
127
|
+
* AGI EXT Fleet detail panel, PHNX-2973) has the paths, not just the totals.
|
|
128
|
+
* Capped so a session that rewrote thousands of files can't bloat the cached
|
|
129
|
+
* digest / JSON payload; the `changes` counts stay the true totals.
|
|
130
|
+
*/
|
|
131
|
+
changedFiles: FileChange[];
|
|
121
132
|
dirs: string[];
|
|
122
133
|
repos: string[];
|
|
123
134
|
artifacts: ReturnType<typeof extractArtifacts>;
|
|
@@ -229,6 +229,14 @@ export function sanitizeRemoteDigest(raw) {
|
|
|
229
229
|
backgroundShellCount: optNum(d.backgroundShellCount),
|
|
230
230
|
toolTags: strList(d.toolTags),
|
|
231
231
|
changes,
|
|
232
|
+
// Per-file paths from an untrusted peer: keep only well-formed {path, op}
|
|
233
|
+
// entries (path scrubbed of terminal escapes, op a known FileOp), bounded
|
|
234
|
+
// like the local build so a hostile peer can't flood the pane.
|
|
235
|
+
changedFiles: objList(d.changedFiles, (f) => {
|
|
236
|
+
const path = str(f.path);
|
|
237
|
+
const op = f.op === 'created' || f.op === 'modified' || f.op === 'deleted' ? f.op : undefined;
|
|
238
|
+
return path && op ? { path, op } : undefined;
|
|
239
|
+
}).slice(0, CHANGED_FILES_MAX),
|
|
232
240
|
dirs: strList(d.dirs),
|
|
233
241
|
repos: strList(d.repos),
|
|
234
242
|
artifacts: objList(d.artifacts, (a) => {
|
|
@@ -655,6 +663,10 @@ const LAST_RESPONSE_MAX_LINES = 15;
|
|
|
655
663
|
const LAST_RESPONSE_MAX_LINES_WITH_TODOS = 8;
|
|
656
664
|
const TODOS_MAX_ITEMS = 5;
|
|
657
665
|
const DIRS_TOUCHED_MAX = 5;
|
|
666
|
+
// Upper bound on the per-file `changedFiles` list carried on the digest. High
|
|
667
|
+
// enough to cover any real session's edits, low enough that a runaway rewrite
|
|
668
|
+
// can't bloat the cached JSON. The `changes` counts stay the true totals.
|
|
669
|
+
const CHANGED_FILES_MAX = 200;
|
|
658
670
|
/** Fold a harness-normalized event stream into the stable preview data model. */
|
|
659
671
|
export function buildSessionPreviewDigest(events, session) {
|
|
660
672
|
let firstUser = '';
|
|
@@ -747,6 +759,10 @@ export function buildSessionPreviewDigest(events, session) {
|
|
|
747
759
|
backgroundShellCount,
|
|
748
760
|
toolTags: [...toolTags],
|
|
749
761
|
changes: chg,
|
|
762
|
+
// Full per-file list the picker used to collapse to `chg` and throw away.
|
|
763
|
+
// Bounded so a mass-rewrite session can't blow up the cached digest; the
|
|
764
|
+
// counts above remain the true totals.
|
|
765
|
+
changedFiles: changes.slice(0, CHANGED_FILES_MAX),
|
|
750
766
|
dirs: directoriesTouched(session, events, changes),
|
|
751
767
|
repos: extractRepos(events, session.cwd),
|
|
752
768
|
artifacts: extractArtifacts(changes),
|
|
@@ -2061,6 +2061,7 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2061
2061
|
tokenCount: session.tokenCount,
|
|
2062
2062
|
costUsd: session.costUsd,
|
|
2063
2063
|
label: session.label,
|
|
2064
|
+
topic: session.topic,
|
|
2064
2065
|
ticketId: session.ticketId,
|
|
2065
2066
|
prUrl: session.prUrl,
|
|
2066
2067
|
},
|
package/dist/commands/share.d.ts
CHANGED
|
@@ -250,6 +250,17 @@ export interface ShareUpdateResult {
|
|
|
250
250
|
templateHash: string;
|
|
251
251
|
baseUrl: string;
|
|
252
252
|
workerName: string;
|
|
253
|
+
/** True when this was a `--check` run: nothing was deployed, the fields below
|
|
254
|
+
* just report whether a deploy WOULD be needed. Used by the release train to
|
|
255
|
+
* decide during preflight — before it verifies deploy creds — whether this
|
|
256
|
+
* release even changes the Worker. */
|
|
257
|
+
checked?: boolean;
|
|
258
|
+
/** Whether the current `worker-template.ts` render differs from what the
|
|
259
|
+
* endpoint last deployed (or `--force`/an endpoint with no recorded hash). */
|
|
260
|
+
deployNeeded?: boolean;
|
|
261
|
+
/** The template hash the endpoint last deployed, per local config. Undefined
|
|
262
|
+
* on an endpoint provisioned before the hash field existed. */
|
|
263
|
+
deployedHash?: string;
|
|
253
264
|
}
|
|
254
265
|
/** Re-deploy the Worker script on an ALREADY-provisioned endpoint to match the
|
|
255
266
|
* current `worker-template.ts`. Reuses the existing account/worker/bucket and
|
|
@@ -264,6 +275,9 @@ export declare function runShareUpdate(opts?: {
|
|
|
264
275
|
account?: string;
|
|
265
276
|
token?: string;
|
|
266
277
|
force?: boolean;
|
|
278
|
+
/** Report whether a deploy is needed and return without deploying. Needs no
|
|
279
|
+
* Cloudflare credentials — change detection is a pure local render+hash. */
|
|
280
|
+
check?: boolean;
|
|
267
281
|
request?: CloudflareRequester;
|
|
268
282
|
/** Bind PHOENIX_ID_BASE even when the configured hostname is not the managed domain. */
|
|
269
283
|
managed?: boolean;
|
package/dist/commands/share.js
CHANGED
|
@@ -820,6 +820,7 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
820
820
|
.option('--account <id>', 'Cloudflare account id override (default: the configured endpoint\'s account)')
|
|
821
821
|
.option('--token <t>', 'Cloudflare API token (else read from --bundle)')
|
|
822
822
|
.option('--force', 're-deploy even if the deployed template already matches')
|
|
823
|
+
.option('--check', 'report whether a deploy is needed and exit without deploying (needs no Cloudflare credentials)')
|
|
823
824
|
// Named --update-json, not --json: `share <file>` (the parent) already
|
|
824
825
|
// owns --json for its own publish-time result. Commander resolves an
|
|
825
826
|
// option's long name against the WHOLE ancestor chain, so a same-named
|
|
@@ -832,6 +833,12 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
832
833
|
console.log(JSON.stringify(result, null, 2));
|
|
833
834
|
return;
|
|
834
835
|
}
|
|
836
|
+
if (result.checked) {
|
|
837
|
+
console.log(result.deployNeeded
|
|
838
|
+
? chalk.yellow(`Worker '${result.workerName}' template is outdated — deploy needed (current render ${result.templateHash.slice(0, 12)}…).`)
|
|
839
|
+
: chalk.dim(`Worker '${result.workerName}' already matches the current template — no deploy needed.`));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
835
842
|
if (result.updated) {
|
|
836
843
|
console.log(chalk.green(`Worker '${result.workerName}' updated → template ${result.templateHash.slice(0, 12)}…`));
|
|
837
844
|
}
|
|
@@ -851,11 +858,18 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
851
858
|
|
|
852
859
|
# Force a re-deploy even though the template hash already matches
|
|
853
860
|
agents artifacts share update --force
|
|
861
|
+
|
|
862
|
+
# Report whether a deploy is due without deploying (no Cloudflare creds needed)
|
|
863
|
+
agents artifacts share update --check --update-json
|
|
854
864
|
`,
|
|
855
865
|
notes: `
|
|
856
866
|
Reuses the existing account/worker/bucket from 'agents artifacts share status' and the
|
|
857
867
|
existing write token — it never re-provisions a bucket, touches routes, or
|
|
858
868
|
regenerates the token. See 'agents artifacts share status' for whether an update is due.
|
|
869
|
+
|
|
870
|
+
--check renders the current template and compares its hash to what the endpoint last
|
|
871
|
+
deployed, then exits — deploying nothing and reading no Cloudflare credentials. The
|
|
872
|
+
release train uses it to decide, before publish, whether a release changes the Worker.
|
|
859
873
|
`,
|
|
860
874
|
});
|
|
861
875
|
shareCmd
|
|
@@ -1222,6 +1236,27 @@ export async function runShareUpdate(opts = {}) {
|
|
|
1222
1236
|
if (!cfg) {
|
|
1223
1237
|
throw new Error("Not configured. Run 'agents artifacts setup' (to provision) or 'agents artifacts share join' first.");
|
|
1224
1238
|
}
|
|
1239
|
+
// Change detection is a pure local computation (render + sha256), so it needs
|
|
1240
|
+
// no Cloudflare credentials. The release train calls this in `--check` mode
|
|
1241
|
+
// during preflight — BEFORE it has verified the deploy creds — to decide
|
|
1242
|
+
// whether this release changes the Worker at all. `shareTemplateStatus` is the
|
|
1243
|
+
// same comparator `agents artifacts share status` uses; 'unknown' (an endpoint
|
|
1244
|
+
// with no recorded hash) deploys, mirroring `updateWorker`'s undefined-previous
|
|
1245
|
+
// behavior.
|
|
1246
|
+
const worker = renderWorkerBundle();
|
|
1247
|
+
const templateHash = hashWorkerScript(worker.script);
|
|
1248
|
+
const deployNeeded = opts.force === true || shareTemplateStatus(cfg) !== 'current';
|
|
1249
|
+
if (opts.check) {
|
|
1250
|
+
return {
|
|
1251
|
+
updated: false,
|
|
1252
|
+
checked: true,
|
|
1253
|
+
deployNeeded,
|
|
1254
|
+
templateHash,
|
|
1255
|
+
deployedHash: cfg.templateHash,
|
|
1256
|
+
baseUrl: cfg.baseUrl,
|
|
1257
|
+
workerName: cfg.workerName,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1225
1260
|
const { apiToken, accountId: acctFromBundle } = readCloudflareCreds(opts.bundle ?? DEFAULT_CF_BUNDLE, {
|
|
1226
1261
|
apiToken: opts.token,
|
|
1227
1262
|
accountId: opts.account,
|
|
@@ -1231,7 +1266,6 @@ export async function runShareUpdate(opts = {}) {
|
|
|
1231
1266
|
throw new Error("Share endpoint has no Cloudflare account id — `agents artifacts share update` cannot call the API. Pass --account <id>, or re-run 'agents artifacts share join'.");
|
|
1232
1267
|
}
|
|
1233
1268
|
const writeToken = readWriteToken();
|
|
1234
|
-
const worker = renderWorkerBundle();
|
|
1235
1269
|
const phoenixIdBase = phoenixIdBaseForDeploy({ managed: opts.managed }, cfg);
|
|
1236
1270
|
const provisionOpts = {
|
|
1237
1271
|
...(opts.request ? { request: opts.request } : {}),
|
|
@@ -1242,7 +1276,14 @@ export async function runShareUpdate(opts = {}) {
|
|
|
1242
1276
|
if (!result.skipped) {
|
|
1243
1277
|
writeShareConfig({ ...cfg, accountId, templateHash: result.templateHash });
|
|
1244
1278
|
}
|
|
1245
|
-
return {
|
|
1279
|
+
return {
|
|
1280
|
+
updated: !result.skipped,
|
|
1281
|
+
templateHash: result.templateHash,
|
|
1282
|
+
deployNeeded,
|
|
1283
|
+
deployedHash: cfg.templateHash,
|
|
1284
|
+
baseUrl: cfg.baseUrl,
|
|
1285
|
+
workerName: cfg.workerName,
|
|
1286
|
+
};
|
|
1246
1287
|
}
|
|
1247
1288
|
function cleanHostname(domain) {
|
|
1248
1289
|
const raw = domain?.trim().replace(/\/+$/, '');
|
package/dist/commands/status.js
CHANGED
|
@@ -78,7 +78,7 @@ export function registerStatusCommand(syncCmd) {
|
|
|
78
78
|
// A non-git / partial ~/.agents is its own drift state, not a per-agent
|
|
79
79
|
// "N missing" — surface it distinctly so the real problem isn't buried (PHNX-3301).
|
|
80
80
|
if (status.user.notGitRepo) {
|
|
81
|
-
console.log(` ${'~/.agents (user repo)'.padEnd(28)} ${chalk.yellow('not a git repo — will adopt on next `agents repo sync user`')}`);
|
|
81
|
+
console.log(` ${'~/.agents (user repo)'.padEnd(28)} ${chalk.yellow('not a git repo — will adopt on next `agents sync` (or `agents repo sync user`)')}`);
|
|
82
82
|
}
|
|
83
83
|
if (status.agents.length === 0) {
|
|
84
84
|
console.log(chalk.gray(' (no installed agent versions)'));
|
package/dist/commands/sync.js
CHANGED
|
@@ -45,6 +45,7 @@ import { runLaunchSync } from '../lib/project-launch.js';
|
|
|
45
45
|
import { formatKeptProjectResources } from '../lib/project-resources.js';
|
|
46
46
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
47
47
|
import { runUmbrellaSync } from '../lib/sync-umbrella.js';
|
|
48
|
+
import { verifyVersionConverged, formatResidualDrift } from '../lib/sync-status.js';
|
|
48
49
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
49
50
|
import { syncRepoGit, adoptUserRepoIfNeeded, recordUserRepoRemote, resolveUserRepoRemoteUrl } from '../lib/git.js';
|
|
50
51
|
import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos } from '../lib/state.js';
|
|
@@ -53,6 +54,46 @@ import { registerStatusCommand } from './status.js';
|
|
|
53
54
|
function emitJson(payload) {
|
|
54
55
|
console.log(JSON.stringify(payload));
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Post-reconcile verification (PHNX-3186): after a sync writes into a set of
|
|
59
|
+
* version homes, re-read each home and confirm it now matches its resolved
|
|
60
|
+
* sources. The `agents sync` success line MUST NOT read "reconciled" while the
|
|
61
|
+
* drift it was asked to fix stays put. Any residual drifted/missing resource
|
|
62
|
+
* sets a non-zero exit code and — outside `--json` — names the exact unfixed
|
|
63
|
+
* drift so the operator sees what did not converge instead of a false ✓. Orphans
|
|
64
|
+
* are excluded (sync never removes them). Returns the residual for `--json`.
|
|
65
|
+
*/
|
|
66
|
+
function verifyReconciled(pairs, cwd) {
|
|
67
|
+
const residual = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const { agent, version } of pairs) {
|
|
70
|
+
const key = `${agent}@${version}`;
|
|
71
|
+
if (seen.has(key))
|
|
72
|
+
continue;
|
|
73
|
+
seen.add(key);
|
|
74
|
+
const r = verifyVersionConverged(agent, version, cwd);
|
|
75
|
+
if (r)
|
|
76
|
+
residual.push(r);
|
|
77
|
+
}
|
|
78
|
+
// Residual drift is reported loudly via `ok:false` + the printed ⚠ block, but
|
|
79
|
+
// does NOT change the exit code — matching the declined-write precedent
|
|
80
|
+
// (RUSH-2700). The fleet fan-out (`agents sync --device all`) THROWS on a
|
|
81
|
+
// non-zero peer exit (`hosts/passthrough.ts`), discarding that box's JSON, so a
|
|
82
|
+
// non-zero exit here would hide the very `residualDrift` payload it emitted.
|
|
83
|
+
// Callers that must treat an incomplete sync as failure read `ok`/`residualDrift`
|
|
84
|
+
// from `--json`, exactly as they already do for declines.
|
|
85
|
+
return residual;
|
|
86
|
+
}
|
|
87
|
+
/** Print the residual-drift block naming exactly what did not converge. */
|
|
88
|
+
function printResidual(residual, errLog) {
|
|
89
|
+
if (residual.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
const lines = formatResidualDrift(residual);
|
|
92
|
+
errLog(chalk.yellow(`⚠ sync did not fully reconcile — ${lines.length} resource(s) still drift after writing:`));
|
|
93
|
+
for (const line of lines)
|
|
94
|
+
errLog(chalk.yellow(` ${line}`));
|
|
95
|
+
errLog(chalk.gray(' Re-run the sync; a gap that survives a re-run is a real unreconcilable drift — report it.'));
|
|
96
|
+
}
|
|
56
97
|
/**
|
|
57
98
|
* Translate per-kind CLI flags into a `ResourceSelection` for `buildSelection`.
|
|
58
99
|
* Returns `undefined` when no kind flag was given (caller should use full sync).
|
|
@@ -429,6 +470,7 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
|
429
470
|
await runInteractiveReconcile(opts, outLog, errLog);
|
|
430
471
|
return;
|
|
431
472
|
}
|
|
473
|
+
const cwd = opts.cwd || process.cwd();
|
|
432
474
|
const flags = {
|
|
433
475
|
repos: opts.repos,
|
|
434
476
|
secrets: opts.secrets,
|
|
@@ -459,10 +501,18 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
|
459
501
|
// --cloud (fetch-only, no local reconcile).
|
|
460
502
|
if (!opts.cloud)
|
|
461
503
|
evictCentralBrowserProfilesForSync(quiet, json, outLog, errLog);
|
|
504
|
+
// Post-reconcile verification (PHNX-3186): re-read every version the reconcile
|
|
505
|
+
// wrote into and confirm it now matches source. Without this the umbrella
|
|
506
|
+
// printed `✓ sync: reconciled` unconditionally, the exact false-success the
|
|
507
|
+
// ticket reports. Residual drift downgrades the line and sets a non-zero exit.
|
|
508
|
+
const residual = result.reconciled
|
|
509
|
+
? verifyReconciled(result.reconciledVersions.map((r) => ({ agent: r.agent, version: r.version })), cwd)
|
|
510
|
+
: [];
|
|
462
511
|
if (json) {
|
|
463
512
|
emitJson({
|
|
464
|
-
// A refused resource is not a clean sync (RUSH-2700
|
|
465
|
-
|
|
513
|
+
// A refused resource OR residual drift is not a clean sync (RUSH-2700 +
|
|
514
|
+
// PHNX-3186).
|
|
515
|
+
ok: result.declined.length === 0 && residual.length === 0,
|
|
466
516
|
mode: 'umbrella',
|
|
467
517
|
plan: result.plan,
|
|
468
518
|
repos: result.repos,
|
|
@@ -470,6 +520,7 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
|
470
520
|
devices: result.devices,
|
|
471
521
|
reconciled: result.reconciled,
|
|
472
522
|
declined: result.declined,
|
|
523
|
+
residualDrift: residual,
|
|
473
524
|
});
|
|
474
525
|
return;
|
|
475
526
|
}
|
|
@@ -482,12 +533,18 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
|
482
533
|
if (result.secrets) {
|
|
483
534
|
parts.push(result.secrets.skipped ? 'secrets skipped' : `secrets ${result.secrets.pulled} pulled`);
|
|
484
535
|
}
|
|
536
|
+
// Only claim "reconciled" when the reconcile actually converged. Residual
|
|
537
|
+
// drift is already printed loudly by verifyReconciled above; reflect it in
|
|
538
|
+
// the one-line summary rather than a bare ✓.
|
|
485
539
|
if (result.reconciled)
|
|
486
|
-
parts.push('reconciled');
|
|
487
|
-
|
|
540
|
+
parts.push(residual.length === 0 ? 'reconciled' : 'reconcile INCOMPLETE');
|
|
541
|
+
const symbol = residual.length === 0 ? chalk.green('✓') : chalk.yellow('⚠');
|
|
542
|
+
const line = `${symbol} sync: ${parts.join(' · ') || 'nothing to do'}`;
|
|
543
|
+
outLog(residual.length === 0 ? chalk.green(line) : chalk.yellow(line));
|
|
488
544
|
const errs = [...(result.repos?.errors ?? []), ...(result.secrets?.errors ?? [])];
|
|
489
545
|
for (const e of errs)
|
|
490
546
|
errLog(chalk.yellow(` ! ${e}`));
|
|
547
|
+
printResidual(residual, errLog);
|
|
491
548
|
}
|
|
492
549
|
}
|
|
493
550
|
catch (err) {
|
|
@@ -700,13 +757,23 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
700
757
|
if (!quiet && !json)
|
|
701
758
|
printSyncDetail(result, agentId, v, cwd);
|
|
702
759
|
}
|
|
760
|
+
// Verify each version actually converged; a repo-scoped sync only touched
|
|
761
|
+
// that repo's kinds, so skip verification there (the other layers legitimately
|
|
762
|
+
// still differ and are not this run's responsibility).
|
|
763
|
+
const residual = repoScope
|
|
764
|
+
? []
|
|
765
|
+
: verifyReconciled(versions.map(({ version: v }) => ({ agent: agentId, version: v })), cwd);
|
|
766
|
+
if (!quiet && !json)
|
|
767
|
+
printResidual(residual, errLog);
|
|
703
768
|
if (json) {
|
|
704
769
|
emitJson({
|
|
705
|
-
// Any version that refused a write
|
|
706
|
-
|
|
770
|
+
// Any version that refused a write, or any residual drift after the
|
|
771
|
+
// reconcile, makes the whole run not-ok (RUSH-2700 + PHNX-3186).
|
|
772
|
+
ok: versions.every(({ result }) => result.declined.length === 0) && residual.length === 0,
|
|
707
773
|
mode: 'agent-all',
|
|
708
774
|
agent: agentId,
|
|
709
775
|
repo: repoScope,
|
|
776
|
+
residualDrift: residual,
|
|
710
777
|
...healedPointers,
|
|
711
778
|
versions: versions.map(({ version: v, result }) => ({
|
|
712
779
|
version: v,
|
|
@@ -901,6 +968,11 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
901
968
|
return;
|
|
902
969
|
}
|
|
903
970
|
const result = syncResourcesToVersion(agentId, version, selection, { projectDir, cwd, force, allowExecSurfaces: !!opts.allowExecSurfaces });
|
|
971
|
+
// Post-reconcile verification (PHNX-3186). Only for a FULL reconcile
|
|
972
|
+
// (`!selection`): an interactive subset-selection deliberately touched only the
|
|
973
|
+
// picked kinds, so the rest legitimately still differs and is not this run's
|
|
974
|
+
// failure. This is the exact path `agents sync <agent>[@version]` takes.
|
|
975
|
+
const residual = selection ? [] : verifyReconciled([{ agent: agentId, version }], cwd);
|
|
904
976
|
// Compile project-scope rules into the workspace itself so each agent's
|
|
905
977
|
// native loader picks up cwd/<INSTRUCTIONS_FILE>. projectDir is the
|
|
906
978
|
// .agents/ directory; the workspace root is its parent.
|
|
@@ -910,8 +982,11 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
910
982
|
projectCompile = compileRulesForProject(projectRoot);
|
|
911
983
|
}
|
|
912
984
|
if (json) {
|
|
985
|
+
const base = agentSyncJson(agentId, version, result);
|
|
913
986
|
emitJson({
|
|
914
|
-
...
|
|
987
|
+
...base,
|
|
988
|
+
ok: base.ok === true && residual.length === 0,
|
|
989
|
+
residualDrift: residual,
|
|
915
990
|
...healedPointers,
|
|
916
991
|
projectCompile: projectCompile
|
|
917
992
|
? {
|
|
@@ -928,6 +1003,7 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
928
1003
|
return;
|
|
929
1004
|
// ---------- 6. Detailed output ----------
|
|
930
1005
|
printSyncDetail(result, agentId, version, cwd);
|
|
1006
|
+
printResidual(residual, errLog);
|
|
931
1007
|
if (projectCompile?.compiled) {
|
|
932
1008
|
const linkInfo = projectCompile.symlinks.length > 0
|
|
933
1009
|
? ` (+ ${projectCompile.symlinks.join(', ')})`
|
package/dist/commands/traces.js
CHANGED
|
@@ -109,6 +109,13 @@ async function handleSync(opts) {
|
|
|
109
109
|
parts.push(chalk.dim('nothing new'));
|
|
110
110
|
}
|
|
111
111
|
console.log(parts.join(chalk.dim(' · ')));
|
|
112
|
+
// The per-session data can upload cleanly while the aggregated console shard
|
|
113
|
+
// fails to refresh (e.g. the sessions DB is locked by a running app). That used
|
|
114
|
+
// to be silent, so the console sat stale with no signal here (PHNX-3401).
|
|
115
|
+
if (result.indexError) {
|
|
116
|
+
console.log(chalk.yellow(' ⚠ console index not refreshed') +
|
|
117
|
+
chalk.dim(` (${result.indexError}) — dashboard may be stale; it will retry next sync`));
|
|
118
|
+
}
|
|
112
119
|
}
|
|
113
120
|
async function handleStatus() {
|
|
114
121
|
const ledger = readSyncLedger();
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - `__secrets-get` / `__secrets-ping` / `__secrets-lock` (SYNC_* tokens)
|
|
16
16
|
* - `__shim`
|
|
17
17
|
* - `__claude-statusline`
|
|
18
|
-
* - `__usage-ingest`
|
|
18
|
+
* - `__usage-ingest` / `__usage-export`
|
|
19
19
|
* - `__daemon-run`
|
|
20
20
|
*
|
|
21
21
|
* The tokens are imported from the leaf module sync-commands.ts — the SAME
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - `__secrets-get` / `__secrets-ping` / `__secrets-lock` (SYNC_* tokens)
|
|
16
16
|
* - `__shim`
|
|
17
17
|
* - `__claude-statusline`
|
|
18
|
-
* - `__usage-ingest`
|
|
18
|
+
* - `__usage-ingest` / `__usage-export`
|
|
19
19
|
* - `__daemon-run`
|
|
20
20
|
*
|
|
21
21
|
* The tokens are imported from the leaf module sync-commands.ts — the SAME
|
|
@@ -84,6 +84,11 @@ if (process.argv[2] === '__usage-ingest') {
|
|
|
84
84
|
const { runUsageIngest } = await import('./lib/accounting/usage-ingest.js');
|
|
85
85
|
process.exit(await runUsageIngest());
|
|
86
86
|
}
|
|
87
|
+
if (process.argv[2] === '__usage-export') {
|
|
88
|
+
const { exportClaudeUsageCacheRows } = await import('./lib/accounting/usage.js');
|
|
89
|
+
process.stdout.write(JSON.stringify({ v: 1, rows: exportClaudeUsageCacheRows() }));
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
87
92
|
if (process.argv[2] === '__daemon-run') {
|
|
88
93
|
const { runDaemon, log: daemonLog } = await import('./lib/daemon/daemon.js');
|
|
89
94
|
// RUSH-2418: the daemon is the one always-on process here, and it ran with no
|
|
@@ -63,11 +63,15 @@ export declare function unbindAccount(nameOrId: string, target: string): void;
|
|
|
63
63
|
/** Every target bound to `accountId`: this box's device-doc bindings merged over
|
|
64
64
|
* the fleet-shared central bindings (PHNX-3315). */
|
|
65
65
|
export declare function accountBindings(accountId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): string[];
|
|
66
|
+
export interface AccountSelection {
|
|
67
|
+
id: string;
|
|
68
|
+
source: 'explicit' | 'binding' | 'default';
|
|
69
|
+
}
|
|
66
70
|
/** Explicit selection wins over a configured per-harness default. */
|
|
67
71
|
export declare function resolveAccountSelection(explicit: string | undefined, agent: AgentId, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, opts?: {
|
|
68
72
|
useDefault?: boolean;
|
|
69
73
|
target?: string;
|
|
70
|
-
}):
|
|
74
|
+
}): AccountSelection | undefined;
|
|
71
75
|
export interface AddAccountOptions {
|
|
72
76
|
baseUrl?: string;
|
|
73
77
|
}
|