@ours.network/fleet 1.1.0-nightly.20 → 1.1.0-nightly.22

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/README.md CHANGED
@@ -365,8 +365,41 @@ Agent Template document (\`~/fleet/agent_templates/Worker.yaml\`) is inert and r
365
365
  role: { ref: developer }
366
366
  brain: { ref: claude-default }
367
367
  permissions: { approval: ask, filesystem: workspace, unattended: deny }
368
+ loops: # optional; temporary launches only
369
+ progress:
370
+ interval: 10m # required; 1m..30d
371
+ initial_delay: 10m # default: interval; 0s..30d
372
+ jitter: 30s # default: 0s; < interval and <=1h
373
+ enabled: true # default true
374
+ prompt: Continue the assigned work and report only material progress.
368
375
  ```
369
376
 
377
+ Agent Template-local `loops` are limited to 64 entries, implicitly scoped to the temporary agent, and
378
+ reuse Fleet's scheduled-turn semantics: a busy occurrence is skipped, ordinary
379
+ missed occurrences are not backlogged or replayed, and restart recovery may
380
+ preserve at most one recent late occurrence. The trusted authoring file and
381
+ private sealed role snapshot retain exact prompt text; resolved launch, task,
382
+ room, provenance, and audit presentation surfaces show only bytes and SHA-256.
383
+
384
+ Direct temporary spawn can set the same whole block from an owner-only regular
385
+ file containing exactly a top-level `loops:` mapping, or explicitly disable it:
386
+
387
+ ```sh
388
+ ours-fleet spawn --temp Scout --role 'inline:{mission: inspect}' --brain 'inline:{harness: codex}' --loops-file ./scout-loops.yaml
389
+ ours-fleet spawn --temp Scout --role 'inline:{mission: inspect}' --brain 'inline:{harness: codex}' --no-loops
390
+ ours-fleet task start TASK --member developer --loops-file ./developer-loops.yaml
391
+ ours-fleet task start TASK --member critic --no-loops
392
+ ```
393
+
394
+ `--loops-file` and `--no-loops` are mutually exclusive and temporary-only.
395
+ For room members they override the selected Agent Template as a whole; the
396
+ Agent Template overrides legacy omission. Omission preserves the historical
397
+ no-loop temporary behavior and never inherits manifest `roles: ["*"]` loops.
398
+ The file must be owned by the current user, mode 0600, non-symlink, no larger
399
+ than 1 MB, and contain a non-empty map. Validation completes before identity,
400
+ Cowork, snapshot, or supervisor side effects. Recovery and replacement reuse
401
+ the private sealed normalized definition even if mutable templates later change.
402
+
370
403
  Persistent Agent instance (\`~/fleet/agents/Name.yaml\`) either remains a canonical
371
404
  Role/Brain composition or explicitly reuses a template:
372
405
 
@@ -484,6 +517,15 @@ ours-fleet task create --title "Reviewed change" --template pair
484
517
  ours-fleet task create --title "Phased delivery" --template team
485
518
  ```
486
519
 
520
+ The default `team` gives only its LocalCoordinator Agent Template a 15-minute
521
+ continuity loop. Each idle-only pass uses authenticated assigned-room evidence,
522
+ posts at most one missing interval update or status request, and reports `STALLED`
523
+ only after concrete stopped/blocked evidence or the role's documented request/ETA
524
+ window. Stable history/checkpoint fingerprints suppress duplicates; `single` and
525
+ `pair` remain loop-free. This preset does not widen permissions: task planning and
526
+ launch still fail closed when the selected harness cannot supply the configured
527
+ unattended messaging and checkpoint capabilities.
528
+
487
529
  `ours-fleet init -c /path/custom.yaml` reviews the literal resolved manifest and
488
530
  `/path/custom/` split directory before doing anything. It requires a TTY and two
489
531
  default-No confirmations. You select Codex, Claude, or both; then either one explicit
@@ -514,11 +556,12 @@ task-local `LocalCoordinator`, `Developer`, and `Critic`. The persistent
514
556
  `FleetCoordinator` uses the separate packaged `Coordinator` contract and coordination
515
557
  model.
516
558
 
517
- Revision-3 role defaults have an explicit fail-closed adoption command: first run
559
+ Revision-3 role defaults and exact revision-4 LocalCoordinator/team defaults have an
560
+ explicit fail-closed adoption command: first run
518
561
  `ours-fleet migrate-role-defaults -c FILE` for a zero-write plan, review every removal,
519
562
  replacement, addition, preserved custom file, staging path, and recovery path, then rerun
520
563
  with `--write`. Only exact semantic matches for packaged-bootstrap and generated
521
- revision-3 forms are changed; same-named customized files remain byte-identical, and a
564
+ revision-3/4 forms are changed; same-named customized files remain byte-identical, and a
522
565
  dangling custom reference refuses publication. Rerunning the migration is a no-op.
523
566
 
524
567
  For manual adoption of a single newer packaged file, copy the reported source beside the
@@ -582,8 +625,9 @@ optional `rooms.owner.provider` setting is separate and defaults to
582
625
  ### Scheduled agent loops
583
626
 
584
627
  Top-level `loops` schedule literal prompts from trusted local YAML. Enabled targets
585
- must use a managed session (`acp` or `codex-app-server`); temporary roles never inherit loops, including `roles:
586
- ["*"]`. Fleet rejects an enabled loop when its explicitly selected base config is
628
+ must use a managed session (`acp` or `codex-app-server`); temporary roles never
629
+ inherit these manifest loops, including `roles: ["*"]`. Only their sealed Agent
630
+ Template/CLI-local `loops` apply. Fleet rejects an enabled loop when its explicitly selected base config is
587
631
  a symlink, is owned by another user, or is group/world writable. Prompts are
588
632
  bounded and normalized at validation time, but only their size and SHA-256 appear
589
633
  in `config`, `list`, logs, or durable state.
@@ -625,15 +625,24 @@ export class TaskRoomApplicationService {
625
625
  throw new TaskRoomApplicationError('template_mismatch', 'anonymous override does not match the existing Room launch policy', { room: task.room_id });
626
626
  if (TASK_TERMINAL_STATES.includes(task.state))
627
627
  throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: input.taskId, state: task.state });
628
+ const durableTemplate = task.room_id
629
+ ? getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot
630
+ : task.execution_plan?.snapshot;
631
+ const templateRequestMatches = !input.template || (durableTemplate !== undefined
632
+ && (input.template === durableTemplate.name
633
+ || input.template === `${durableTemplate.name}@${durableTemplate.version}`));
634
+ const memberRequestMatches = input.members === undefined || (task.execution_plan !== undefined
635
+ && task.execution_plan.overrides_hash === hashMemberOverrides(input.members));
636
+ const durableRequestMatches = durableTemplate !== undefined
637
+ && templateRequestMatches && memberRequestMatches;
628
638
  if (task.state === 'active' && task.room_id) {
629
639
  if (input.template || input.members) {
630
- const pinned = getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot;
640
+ const pinned = durableTemplate;
631
641
  if (!pinned)
632
642
  throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
633
- const templateName = input.template ?? pinned.name;
634
- if ((!input.template || input.template === pinned.name) && input.members
635
- && task.execution_plan?.overrides_hash === hashMemberOverrides(input.members))
643
+ if (durableRequestMatches)
636
644
  return { task, status: 'already_active' };
645
+ const templateName = input.template ?? pinned.name;
637
646
  const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
638
647
  if (!definition)
639
648
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
@@ -654,11 +663,10 @@ export class TaskRoomApplicationService {
654
663
  throw new Error(`task ${task.task_id} template reference does not match its durable execution snapshot`);
655
664
  const templateName = input.template ?? durable?.name ?? task.template?.name
656
665
  ?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
657
- const storedOverridesMatch = !!(durable && input.members && task.execution_plan?.overrides_hash
658
- === hashMemberOverrides(input.members));
659
- const definition = durable && !input.template && (!input.members || storedOverridesMatch) ? undefined
666
+ const storedOverridesMatch = !!(durable && memberRequestMatches);
667
+ const definition = durable && durableRequestMatches ? undefined
660
668
  : resolveTemplate(templateName, cfg.roomTemplates ?? {});
661
- if (input.template && !definition)
669
+ if (input.template && !definition && !durableRequestMatches)
662
670
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
663
671
  if (!durable && !definition)
664
672
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.20",
3
- "buildId": "1e57994b7fe2",
4
- "commit": "4921e26d6cae40636146ace372eb9aa73dffb799",
2
+ "version": "1.1.0-nightly.22",
3
+ "buildId": "bfb51e6ced00",
4
+ "commit": "5447c29d81481c7880fa9cf5bfbbcc2ff294950f",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-01T14:06:50.386Z",
6
+ "builtAt": "2026-09-01T19:41:56.761Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { analyzeInstalls, buildInfo, buildLabel, discoverInstalls, runningLabel,
14
14
  import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir, defaultConfigPath } from './paths.js';
15
15
  import { findRole, loadConfig, ROLE_NAME_RE } from './config.js';
16
16
  import { formatDuration } from './duration.js';
17
- import { redactSensitive, resolvedPlan } from './resolved-plan.js';
17
+ import { redactSensitive, resolvedPlan, resolvedRolePlan } from './resolved-plan.js';
18
18
  import { pickBackend } from './supervisor/index.js';
19
19
  import { up, down } from './ops.js';
20
20
  import { readRestartLedger, runSupervised, runTemp } from './runner.js';
@@ -26,11 +26,11 @@ import { acquireRunLock, latestReport, listRuns, readReport, releaseRunLock, rep
26
26
  import { watchdogAddressable } from './watchdog/query.js';
27
27
  import { lastProvenance } from './spawn.js';
28
28
  import { stringify } from 'yaml';
29
- import { resolvedRolePlan } from './resolved-plan.js';
30
29
  import { creationBuildNote, formatProvenance, readProvenance } from './creation.js';
31
30
  import { doctor } from './doctor.js';
32
31
  import { allWarnings, analyzeFleetPermissions, effectivePermissionMode, formatNative, } from './permissions.js';
33
32
  import { AI_DOCS } from './docs.js';
33
+ import { renderAgentConfiguration, selectionOrigin, summarizeResolvedLaunch } from './lifecycle-summary.js';
34
34
  import { classifyActivity, describeSessionState } from './session/activity.js';
35
35
  import { controlRequest, controlSocketPath, followControl, livenessNote, } from './session/control.js';
36
36
  import { SessionControlError } from './session/types.js';
@@ -1147,6 +1147,8 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1147
1147
  .option('--filesystem <mode>', 'common filesystem intent: read-only|workspace|unrestricted')
1148
1148
  .option('--unattended <mode>', 'permission behavior without a console: deny|wait')
1149
1149
  .option('--isolation-file <path>', 'file holding an isolation: mapping (same schema as fleet.yaml)')
1150
+ .option('--loops-file <path>', 'owner-only YAML containing exactly loops: for this temporary agent')
1151
+ .option('--no-loops', 'explicitly disable temporary-agent loops')
1150
1152
  .option('--dry-run', 'validate and print without reserving or creating anything')
1151
1153
  .option('--json', 'with --dry-run, emit a stable secret-safe JSON result')
1152
1154
  .action(async (name, opts) => {
@@ -1163,6 +1165,8 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1163
1165
  approval: opts.approval,
1164
1166
  filesystem: opts.filesystem, unattended: opts.unattended,
1165
1167
  isolationFile: opts.isolationFile, configPath: opts.configuration,
1168
+ loopsFile: opts.loopsFile, noLoops: opts.loops === false,
1169
+ loopSource: opts.loopsFile || opts.loops === false ? 'cli' : 'omitted',
1166
1170
  dryRun: opts.dryRun, json: opts.json,
1167
1171
  };
1168
1172
  if (o.json && !o.dryRun)
@@ -1176,7 +1180,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1176
1180
  process.stdout.write(`${JSON.stringify({
1177
1181
  schemaVersion: result.schemaVersion,
1178
1182
  warning: result.warning,
1179
- roleDocument: result.roleDocument,
1183
+ roleDocument: redactSensitive(result.roleDocument),
1180
1184
  resolvedRole: resolvedRolePlan(result.resolvedRole),
1181
1185
  }, null, 2)}\n`);
1182
1186
  }
@@ -1190,7 +1194,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1190
1194
  if (proxyStateDir) {
1191
1195
  // Paths entered in the agent shell belong to that shell's cwd, not the
1192
1196
  // supervisor process. Normalize before crossing the control boundary.
1193
- for (const key of ['isolationFile']) {
1197
+ for (const key of ['isolationFile', 'loopsFile']) {
1194
1198
  if (o[key])
1195
1199
  o[key] = resolvePath(o[key]);
1196
1200
  }
@@ -1211,6 +1215,8 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1211
1215
  + `monitor=${result.monitor.mode} interrupt=${result.monitor.interrupt}`);
1212
1216
  if (result.inherited.length)
1213
1217
  console.log(` inherited omitted defaults from ${result.caller}: ${result.inherited.join(', ')}`);
1218
+ if (result.configuration)
1219
+ console.log(` ${renderAgentConfiguration(result.configuration)}`);
1214
1220
  console.log(`→ watch it: ours-fleet peek ${result.role} | attach: ours-fleet attach ${result.role}`);
1215
1221
  return;
1216
1222
  }
@@ -1235,6 +1241,11 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1235
1241
  }
1236
1242
  console.log(` permission=${plannedPermissionMode.fleetMode} `
1237
1243
  + `native=${plannedPermissionMode.nativeMode}`);
1244
+ console.log(` ${renderAgentConfiguration(summarizeResolvedLaunch(created.plan.preview.resolvedRole, {
1245
+ role: selectionOrigin(o.role ?? o.agentDefinition?.role),
1246
+ brain: selectionOrigin(o.brain ?? o.agentDefinition?.brain),
1247
+ permissionMode: plannedPermissionMode,
1248
+ }))}`);
1238
1249
  console.log(`→ watch it: ours-fleet peek ${roleName} | attach: ours-fleet attach ${roleName}`);
1239
1250
  }
1240
1251
  catch (e) {
package/dist/config.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type ConfigDiagnostic, type YamlMode } from './config-yaml.js';
2
2
  import type { IsolationConfig, WrapContext } from './isolation/types.js';
3
3
  import type { ResolvedWatchdog } from './watchdog/config.js';
4
- import type { ResolvedLoop, ResolvedRoleLoop } from './loops/config.js';
4
+ import type { AgentLoopsConfig, ResolvedLoop, ResolvedRoleLoop } from './loops/config.js';
5
5
  export interface OverseeEntry {
6
6
  agent: string;
7
7
  interval: string;
@@ -162,6 +162,8 @@ export interface AgentDefinition {
162
162
  owner_channel?: OwnerChannelConfigInput;
163
163
  worklog?: WorklogPolicyInput;
164
164
  auth_proxy?: Partial<AuthProxyConfig>;
165
+ /** Scheduled turns scoped only to temporary launches; persistent Agents reject this field. */
166
+ loops?: AgentLoopsConfig;
165
167
  }
166
168
  /** Inert, reusable launch definition. It never owns an identity or runtime state. */
167
169
  export type AgentTemplateDefinition = Omit<AgentDefinition, 'identity'>;
@@ -202,6 +204,10 @@ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
202
204
  worklog?: WorklogPolicy;
203
205
  auth_proxy?: AuthProxyConfig;
204
206
  loops?: ResolvedRoleLoop[];
207
+ /** Agent Template-local loops, kept separate from manifest loops until a temp launch. */
208
+ temporaryLoops?: ResolvedRoleLoop[];
209
+ /** Durable source of the temporary loop policy; internal and never user-authored. */
210
+ temporaryLoopSource?: 'agent-template' | 'cli' | 'omitted';
205
211
  provenance?: Record<string, FieldProvenance>;
206
212
  /** Internal/transient: never accepted as a user-authored RoleConfig key. */
207
213
  roomMemberStartup?: RoomMemberStartup;
@@ -294,6 +300,7 @@ export declare function loadConfig(configPath?: string, options?: {
294
300
  additionalAgent?: {
295
301
  id: string;
296
302
  definition: AgentDefinition;
303
+ temporary?: boolean;
297
304
  };
298
305
  skipWatchdogs?: boolean;
299
306
  }): FleetConfig;
package/dist/config.js CHANGED
@@ -6,7 +6,7 @@ import { harnessRuntimeDir, resolveIsolation, validateIsolationConfig, } from '.
6
6
  import { getAdapter } from './harness/registry.js';
7
7
  import { resolveRoleModelEnv } from './model-env.js';
8
8
  import { resolveWatchdogs } from './watchdog/config.js';
9
- import { resolveLoops } from './loops/config.js';
9
+ import { resolveAgentLoops, resolveLoops } from './loops/config.js';
10
10
  import { validateRoomsConfig, validateTasksConfig, validateRoomTemplatesConfig, } from './rooms-tasks/config.js';
11
11
  import { CAPABILITIES, CAP_MONITOR_INTERRUPT_AFTER_TOOL } from './capabilities.js';
12
12
  import { runningLabel } from './provenance.js';
@@ -142,12 +142,12 @@ export const BRAIN_PRESET_KEYS = [
142
142
  ];
143
143
  export const AGENT_KEYS = [
144
144
  'role', 'brain', 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee',
145
- 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
145
+ 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops',
146
146
  ];
147
147
  const AGENT_INSTANCE_KEYS = ['template', 'overrides'];
148
148
  const TEMPLATE_FORBIDDEN_KEYS = ['identity'];
149
149
  const MERGED_MAP_FIELDS = new Set([
150
- 'permissions', 'env', 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
150
+ 'permissions', 'env', 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops',
151
151
  ]);
152
152
  const V2_API_VERSION = 'ours.network/fleet/v2';
153
153
  const utf8Bytes = (value) => Buffer.byteLength(value, 'utf8');
@@ -221,6 +221,10 @@ export function validateEffectiveAgentTemplate(definition, id = 'room-member') {
221
221
  return value;
222
222
  }
223
223
  function validateAgentScalars(value, file, id) {
224
+ if (value.loops !== undefined)
225
+ resolveAgentLoops(value.loops, {
226
+ name: id, harness: 'codex', session: 'acp', permissions: {},
227
+ }, file);
224
228
  for (const key of ['identity', 'cwd']) {
225
229
  const current = value[key];
226
230
  if (current !== undefined && (typeof current !== 'string' || !current.trim()))
@@ -471,6 +475,8 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
471
475
  }
472
476
  if (!('role' in a) || !('brain' in a))
473
477
  throw new ConfigError(`${agent.file}: agent '${id}' requires role and brain`);
478
+ if (Object.hasOwn(a, 'loops') && !(additionalAgent?.temporary && additionalAgent.id === id))
479
+ schemaError(agent.file, `/agents/${id}/loops`, 'is supported only for temporary Agent launches and inert Agent Templates');
474
480
  const role = selection(a.role, 'role', agent.file, roles, ROLE_PRESET_KEYS);
475
481
  const brain = selection(a.brain, 'brain', agent.file, brains, BRAIN_PRESET_KEYS);
476
482
  const operational = deepSubStrict(Object.fromEntries(Object.entries(a).filter(([key]) => key !== 'role' && key !== 'brain')), vars, agent.file, `/agents/${id}`);
@@ -545,8 +551,9 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
545
551
  brain: deepSubStrict(a.brain, vars, agent.file, `/agents/${id}/brain`),
546
552
  ...operational,
547
553
  };
554
+ const { loops: _temporaryLoops, ...runtimeOperational } = operational;
548
555
  documents.push({ file: agent.file,
549
- doc: { roles: { [id]: { ...brainValue, ...roleValue, ...operational } } }, provenance,
556
+ doc: { roles: { [id]: { ...brainValue, ...roleValue, ...runtimeOperational } } }, provenance,
550
557
  agentSelections: structuredClone({ role: canonicalDefinition.role, brain: canonicalDefinition.brain }),
551
558
  agentDefinition: canonicalDefinition,
552
559
  ...(agentTemplate ? { agentTemplate } : {}) });
@@ -677,7 +684,7 @@ export function loadConfig(configPath, options = {}) {
677
684
  defaulted('identity', r.identity !== undefined, r.identity === undefined);
678
685
  defaulted('permissions', defaults.permissions !== undefined, defaults.permissions === undefined);
679
686
  defaulted('monitor', defaults.monitor !== undefined, defaults.monitor === undefined);
680
- roles.push({
687
+ const resolvedRole = {
681
688
  ...r,
682
689
  name,
683
690
  sourceFile: file,
@@ -701,7 +708,10 @@ export function loadConfig(configPath, options = {}) {
701
708
  loops: [],
702
709
  ...(agentSelections ? { agentSelections } : {}),
703
710
  ...(agentTemplate ? { agentTemplate } : {}),
704
- });
711
+ };
712
+ if (agentDefinition?.loops !== undefined)
713
+ resolvedRole.temporaryLoops = resolveAgentLoops(agentDefinition.loops, resolvedRole, file);
714
+ roles.push(resolvedRole);
705
715
  // Forbidden-path enforcement: a mount that would breach the policy
706
716
  // is a configuration error, caught by `config` rather than at launch.
707
717
  if (isolation !== undefined) {
@@ -806,7 +816,7 @@ export function loadConfig(configPath, options = {}) {
806
816
  /** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
807
817
  export function resolveAgentDefinitionDryRun(configPath, id, definition) {
808
818
  return findRole(loadConfig(configPath, {
809
- additionalAgent: { id, definition }, skipWatchdogs: true,
819
+ additionalAgent: { id, definition, temporary: true }, skipWatchdogs: true,
810
820
  }), id);
811
821
  }
812
822
  /**
@@ -156,7 +156,7 @@ export declare function daemonIdentityInventoryProvisioner(env?: NodeJS.ProcessE
156
156
  /** Atomically write a bare Agent file, journalling it for rollback. */
157
157
  export declare function writeRoleFile(tx: CreationTransaction, file: string, contents: string): void;
158
158
  /** Where a setting's effective value came from. */
159
- export type ProvenanceSource = 'cli' | 'fleet-default' | 'caller-role' | 'built-in';
159
+ export type ProvenanceSource = 'cli' | 'agent-template' | 'fleet-default' | 'caller-role' | 'built-in';
160
160
  export interface ProvenanceEntry {
161
161
  value: unknown;
162
162
  source: ProvenanceSource;
package/dist/creation.js CHANGED
@@ -374,11 +374,12 @@ export function creationBuildNote(p) {
374
374
  export function formatProvenance(p) {
375
375
  const mark = {
376
376
  cli: 'explicit', 'fleet-default': 'fleet default', 'caller-role': 'caller role',
377
- 'built-in': 'built-in',
377
+ 'agent-template': 'Agent Template', 'built-in': 'built-in',
378
378
  };
379
379
  return Object.entries(p.settings)
380
380
  .filter(([, e]) => e.value !== undefined)
381
- .map(([k, e]) => ` ${k.padEnd(12)} ${String(e.value)} (${mark[e.source]})`);
381
+ .map(([k, e]) => ` ${k.padEnd(12)} ${e.value && typeof e.value === 'object'
382
+ ? JSON.stringify(e.value) : String(e.value)} (${mark[e.source]})`);
382
383
  }
383
384
  /** Classify one setting: an explicit CLI value, a fleet default, or built-in. */
384
385
  export function provenanceOf(cliValue, fleetDefault, builtIn) {