@lumenflow/cli 5.0.3 → 5.2.0

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.
Files changed (80) hide show
  1. package/dist/capacity-snapshot-emitter.js +10 -12
  2. package/dist/capacity-snapshot-emitter.js.map +1 -1
  3. package/dist/commands/integrate.js +9 -0
  4. package/dist/commands/integrate.js.map +1 -1
  5. package/dist/delegation-role-resolver.js +1 -1
  6. package/dist/delegation-role-resolver.js.map +1 -1
  7. package/dist/docs-generate-pack-reference.js +57 -15
  8. package/dist/docs-generate-pack-reference.js.map +1 -1
  9. package/dist/docs-sync.js +7 -3
  10. package/dist/docs-sync.js.map +1 -1
  11. package/dist/init-docs-scaffolder.js +18 -3
  12. package/dist/init-docs-scaffolder.js.map +1 -1
  13. package/dist/init-templates.js +307 -0
  14. package/dist/init-templates.js.map +1 -1
  15. package/dist/init.js +26 -6
  16. package/dist/init.js.map +1 -1
  17. package/dist/kernel-event-sync/lifecycle-emitters.js +13 -14
  18. package/dist/kernel-event-sync/lifecycle-emitters.js.map +1 -1
  19. package/dist/kernel-event-sync/narrow-emissions.js +7 -4
  20. package/dist/kernel-event-sync/narrow-emissions.js.map +1 -1
  21. package/dist/kernel-event-sync/software-delivery-emitters.js +91 -0
  22. package/dist/kernel-event-sync/software-delivery-emitters.js.map +1 -1
  23. package/dist/lumenflow-upgrade.js +86 -23
  24. package/dist/lumenflow-upgrade.js.map +1 -1
  25. package/dist/orchestrate-init-status.js +24 -11
  26. package/dist/orchestrate-init-status.js.map +1 -1
  27. package/dist/pack-install.js +27 -20
  28. package/dist/pack-install.js.map +1 -1
  29. package/dist/pack-publish.js +34 -26
  30. package/dist/pack-publish.js.map +1 -1
  31. package/dist/release.js +293 -146
  32. package/dist/release.js.map +1 -1
  33. package/dist/skills-projection.js +84 -0
  34. package/dist/skills-projection.js.map +1 -0
  35. package/dist/sync-templates.js +4 -4
  36. package/dist/sync-templates.js.map +1 -1
  37. package/dist/temp-dir-cleanup.js +112 -0
  38. package/dist/temp-dir-cleanup.js.map +1 -0
  39. package/dist/validate-agent-skills.js +4 -4
  40. package/dist/validate-agent-skills.js.map +1 -1
  41. package/dist/validate-agent-sync.js +109 -45
  42. package/dist/validate-agent-sync.js.map +1 -1
  43. package/dist/validate-skills-spec.js +2 -2
  44. package/dist/wu-done.js +1 -1
  45. package/dist/wu-done.js.map +1 -1
  46. package/dist/wu-spawn-prompt-builders.js +11 -34
  47. package/dist/wu-spawn-prompt-builders.js.map +1 -1
  48. package/dist/wu-spawn-strategy-resolver.js +4 -0
  49. package/dist/wu-spawn-strategy-resolver.js.map +1 -1
  50. package/package.json +11 -11
  51. package/packs/agent-runtime/manifest.ts +55 -4
  52. package/packs/agent-runtime/manifest.yaml +16 -6
  53. package/packs/agent-runtime/orchestration.ts +26 -1
  54. package/packs/agent-runtime/package.json +1 -1
  55. package/packs/agent-runtime/remote-controls/operations.ts +6 -0
  56. package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +4 -0
  57. package/packs/agent-runtime/turn-lifecycle-events.ts +90 -1
  58. package/packs/sidekick/manifest.yaml +6 -0
  59. package/packs/sidekick/package.json +2 -1
  60. package/packs/sidekick/sidekick-events.ts +195 -18
  61. package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +18 -10
  62. package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +4 -0
  63. package/packs/sidekick/src/domain/channel.types.ts +34 -54
  64. package/packs/sidekick/src/ports/channel-bridge.port.ts +29 -12
  65. package/packs/sidekick/tool-impl/channel-tools.ts +47 -16
  66. package/packs/sidekick/tool-impl/system-tools.ts +4 -6
  67. package/packs/software-delivery/manifest.ts +94 -7
  68. package/packs/software-delivery/manifest.yaml +42 -5
  69. package/packs/software-delivery/package.json +1 -1
  70. package/packs/software-delivery/src/config/schemas/lumenflow-config-schema-types.ts +26 -2
  71. package/packs/software-delivery/src/config/workspace-reader.ts +67 -2
  72. package/packs/software-delivery/src/constants/wu-paths-constants.ts +26 -0
  73. package/packs/software-delivery/src/domain/orchestration.constants.ts +7 -9
  74. package/packs/software-delivery/src/domain/orchestration.schemas.ts +1 -2
  75. package/packs/software-delivery/src/domain/orchestration.types.ts +0 -2
  76. package/packs/software-delivery/src/state/wu-paths.ts +1 -1
  77. package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +30 -0
  78. package/templates/core/AGENTS.md.template +12 -8
  79. package/templates/core/LUMENFLOW.md.template +14 -9
  80. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +5 -0
@@ -2,8 +2,17 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
 
4
4
  /**
5
- * WU-2735 (INIT-060 WU-7a, ADR-013 §ChannelBridge):
6
- * ChannelBridge port.
5
+ * WU-2735 (INIT-060 WU-7a, ADR-013 §ChannelBridge) + WU-2831 (INIT-062 WU-E):
6
+ *
7
+ * Sidekick-pack ChannelBridge port.
8
+ *
9
+ * As of WU-2831 the canonical neutral port lives in `@lumenflow/conductor-sdk`
10
+ * under Apache-2.0 — consumers that need an AGPL-free surface (e.g. the
11
+ * cloud phone bridge) MUST import from there. The sidekick port below reuses
12
+ * the conductor-sdk identity / config / result types (no parallel drift) but
13
+ * keeps the pack-internal wire-format envelope (`ChannelEnvelope` with
14
+ * `kind: 'ephemeral' | 'queue'`) for on-disk and control-plane compatibility
15
+ * of existing adapters.
7
16
  *
8
17
  * Port contract — load-bearing across adapters (ADR-013 §ChannelBridge):
9
18
  *
@@ -11,14 +20,12 @@
11
20
  * `ephemeral` envelopes (ADR-013 §4 backpressure split).
12
21
  * - `receive` yields envelopes in emit order on a single channel (§3). No
13
22
  * cross-channel ordering guarantee.
14
- * - `register` is idempotent on `BridgeConfig` identity — same
15
- * `(provider, name)` returns the same `ChannelId`.
23
+ * - `connect` (alias: `register`) is idempotent on `BridgeConfig` identity —
24
+ * same `(provider, name)` returns the same `ChannelId`.
16
25
  * - `disconnect` flushes queued envelopes before closing; in-flight ephemerals
17
26
  * may drop.
18
27
  *
19
- * The port MUST NOT leak filesystem- or network-specific types. Both the
20
- * filesystem adapter (this WU) and the cloud adapter (WU-2737) implement this
21
- * interface unmodified.
28
+ * The port MUST NOT leak filesystem- or network-specific types.
22
29
  */
23
30
 
24
31
  import type {
@@ -29,6 +36,21 @@ import type {
29
36
  } from '../domain/channel.types.js';
30
37
 
31
38
  export interface ChannelBridge {
39
+ /**
40
+ * Open (or reuse) a channel. Idempotent on `(provider, name)`.
41
+ *
42
+ * Canonical method name per the neutral conductor-sdk port (WU-2831).
43
+ */
44
+ connect(bridgeConfig: BridgeConfig): Promise<ChannelId>;
45
+
46
+ /**
47
+ * Legacy alias for `connect`. Retained for pack-internal call sites that
48
+ * predate WU-2831.
49
+ *
50
+ * @deprecated Use `connect` instead (WU-2831 port-contract alignment).
51
+ */
52
+ register(bridgeConfig: BridgeConfig): Promise<ChannelId>;
53
+
32
54
  /**
33
55
  * Send an envelope on a registered channel.
34
56
  *
@@ -42,11 +64,6 @@ export interface ChannelBridge {
42
64
  */
43
65
  receive(channelId: ChannelId): AsyncIterable<ChannelEnvelope>;
44
66
 
45
- /**
46
- * Register a channel and return its id. Idempotent on `(provider, name)`.
47
- */
48
- register(bridgeConfig: BridgeConfig): Promise<ChannelId>;
49
-
50
67
  /**
51
68
  * Close the channel. Queued envelopes flush before this resolves; ephemerals
52
69
  * in flight may be dropped.
@@ -22,7 +22,6 @@ import {
22
22
  buildChannelMessageReceivedEvent,
23
23
  buildChannelMessageSentEvent,
24
24
  emitSidekickEvent,
25
- serializeLocalChannelMessage,
26
25
  } from '../sidekick-events.js';
27
26
 
28
27
  // ---------------------------------------------------------------------------
@@ -300,16 +299,18 @@ async function channelSendViaTransport(
300
299
  };
301
300
  }
302
301
 
303
- await emitSidekickEvent(
304
- buildChannelMessageSentEvent({
305
- provider,
306
- channel,
307
- content,
308
- ...(transportResult.externalMessageId !== undefined
309
- ? { external_message_id: transportResult.externalMessageId }
310
- : {}),
311
- }),
312
- );
302
+ // WU-2830 (INIT-062 WU-D): ChannelMessageSentEvent.message is a typed
303
+ // ChannelMessageRecord. Provider sends do not have a local storage row,
304
+ // so we synthesize a record using the external id when available.
305
+ const providerSentMessage: ChannelMessageRecord = {
306
+ id: transportResult.externalMessageId ?? createId('msg'),
307
+ channel_id: `${provider}:${channel}`,
308
+ sender: 'assistant',
309
+ content,
310
+ created_at: nowIso(),
311
+ };
312
+
313
+ await emitSidekickEvent(buildChannelMessageSentEvent(providerSentMessage));
313
314
 
314
315
  return success(outputData);
315
316
  }
@@ -389,9 +390,8 @@ async function channelSendTool(input: unknown, context?: ToolContextLike): Promi
389
390
  );
390
391
  });
391
392
 
392
- await emitSidekickEvent(
393
- buildChannelMessageSentEvent(serializeLocalChannelMessage(resolvedMessage, channelName)),
394
- );
393
+ // WU-2830: pass the typed ChannelMessageRecord directly.
394
+ await emitSidekickEvent(buildChannelMessageSentEvent(resolvedMessage));
395
395
 
396
396
  return success({ message: resolvedMessage as unknown as Record<string, unknown> });
397
397
  }
@@ -462,17 +462,47 @@ async function channelReceiveViaTransport(
462
462
  };
463
463
  }
464
464
 
465
+ // WU-2830: ChannelMessageReceivedEvent now carries typed
466
+ // ChannelMessageRecord[] (symmetric with the sent event). Coerce
467
+ // provider-opaque records into the canonical shape before emission.
468
+ const receivedRecords = (transportResult.records ?? []).map((record) =>
469
+ coerceProviderRecordToMessageRecord(record, `${provider}:${channel}`),
470
+ );
471
+
465
472
  await emitSidekickEvent(
466
473
  buildChannelMessageReceivedEvent({
467
474
  provider,
468
475
  channel,
469
- count: transportResult.records?.length ?? 0,
476
+ messages: receivedRecords,
470
477
  }),
471
478
  );
472
479
 
473
480
  return success(outputData);
474
481
  }
475
482
 
483
+ function coerceProviderRecordToMessageRecord(
484
+ record: unknown,
485
+ fallbackChannelId: string,
486
+ ): ChannelMessageRecord {
487
+ // WU-2830: provider transports declare `records?: unknown[]`. Type-narrow
488
+ // to build a ChannelMessageRecord without unsafe casts; missing fields
489
+ // fall back to sensible defaults so downstream consumers always see a
490
+ // complete, typed payload.
491
+ const source = record && typeof record === 'object' ? (record as Record<string, unknown>) : {};
492
+ const now = nowIso();
493
+ const id = typeof source.id === 'string' && source.id.length > 0 ? source.id : createId('msg');
494
+ const channel_id =
495
+ typeof source.channel_id === 'string' && source.channel_id.length > 0
496
+ ? source.channel_id
497
+ : fallbackChannelId;
498
+ const sender =
499
+ typeof source.sender === 'string' && source.sender.length > 0 ? source.sender : 'external';
500
+ const content = typeof source.content === 'string' ? source.content : '';
501
+ const created_at =
502
+ typeof source.created_at === 'string' && source.created_at.length > 0 ? source.created_at : now;
503
+ return { id, channel_id, sender, content, created_at };
504
+ }
505
+
476
506
  async function channelReceiveTool(input: unknown, _context?: ToolContextLike): Promise<ToolOutput> {
477
507
  const parsed = toRecord(input);
478
508
  const channelName = asNonEmptyString(parsed.channel);
@@ -511,7 +541,8 @@ async function channelReceiveTool(input: unknown, _context?: ToolContextLike): P
511
541
  await emitSidekickEvent(
512
542
  buildChannelMessageReceivedEvent({
513
543
  channel: channelName ?? DEFAULT_CHANNEL_NAME,
514
- count: items.length,
544
+ // WU-2830: carry the full typed ChannelMessageRecord[] payload.
545
+ messages: items,
515
546
  }),
516
547
  );
517
548
 
@@ -12,11 +12,7 @@ import {
12
12
  type ToolContextLike,
13
13
  type ToolOutput,
14
14
  } from './shared.js';
15
- import {
16
- buildStateRehydratedEvent,
17
- emitSidekickEvent,
18
- snapshotSidekickState,
19
- } from '../sidekick-events.js';
15
+ import { emitSidekickStateRehydration, snapshotSidekickState } from '../sidekick-events.js';
20
16
 
21
17
  // ---------------------------------------------------------------------------
22
18
  // Constants
@@ -51,7 +47,9 @@ async function initTool(context?: ToolContextLike): Promise<ToolOutput> {
51
47
  }),
52
48
  );
53
49
 
54
- await emitSidekickEvent(buildStateRehydratedEvent(await snapshotSidekickState()));
50
+ // WU-2830 (INIT-062 WU-D): chunked rehydration replaces the monolithic
51
+ // `state_rehydrated` emission — unbounded snapshots cannot stream.
52
+ await emitSidekickStateRehydration(await snapshotSidekickState());
55
53
 
56
54
  return success({
57
55
  initialized: true,
@@ -31,6 +31,25 @@ export type {
31
31
  } from './manifest-schema.js';
32
32
 
33
33
  const FULL_WORKSPACE_SCOPE_PATTERN = '**';
34
+ // WU-2833 (INIT-062 WU-G): the canonical read-only workspace scope used by
35
+ // validation runners like gates / gates:docs. Exposed so remote callers
36
+ // cannot mis-declare a read-only runner with a broader write scope.
37
+ export const SOFTWARE_DELIVERY_READ_SCOPE_PATTERN = FULL_WORKSPACE_SCOPE_PATTERN;
38
+ /**
39
+ * WU-2833 (INIT-062 WU-G): tools whose runtime handlers perform only
40
+ * read-side inspection (no filesystem mutation, no git mutation). Any
41
+ * attempt to re-declare these tools with permission: write or admin MUST
42
+ * fail the pack:validate gate so the security posture established by
43
+ * WU-2810/2811/2816 cannot drift without an explicit ADR.
44
+ */
45
+ export const SOFTWARE_DELIVERY_READ_ONLY_RUNNER_TOOLS = ['gates', 'gates:docs'] as const;
46
+ export type SoftwareDeliveryReadOnlyRunnerTool =
47
+ (typeof SOFTWARE_DELIVERY_READ_ONLY_RUNNER_TOOLS)[number];
48
+ // WU-2833: metrics:snapshot reads the workspace to compute DORA metrics
49
+ // but writes the computed snapshot back into workspace state. This narrow
50
+ // write scope keeps mobile/cloud tokens for metrics:snapshot from leaking
51
+ // full-tree write access (principle of least privilege).
52
+ const SOFTWARE_DELIVERY_WORKSPACE_STATE_WRITE_PATTERN = '.lumenflow/state/**';
34
53
  const SOFTWARE_DELIVERY_WRITE_DIRECTORY_PATTERNS = [
35
54
  '.changeset/**',
36
55
  '.claude/**',
@@ -99,6 +118,10 @@ const WU_UNBLOCK_TOOL_ENTRY = 'tool-impl/wu-lifecycle-tools.ts#wuUnblockTool';
99
118
  const WU_RELEASE_TOOL_ENTRY = 'tool-impl/wu-lifecycle-tools.ts#wuReleaseTool';
100
119
  const WU_RECOVER_TOOL_ENTRY = 'tool-impl/wu-lifecycle-tools.ts#wuRecoverTool';
101
120
  const WU_REPAIR_TOOL_ENTRY = 'tool-impl/wu-lifecycle-tools.ts#wuRepairTool';
121
+ // WU-2833 (INIT-062 WU-G): admin-mode wu:repair wrapper that forces the
122
+ // `--admin` flag; exposed as a separate manifest tool so approvals can
123
+ // be attached to the privileged surface independently of wu:repair.
124
+ const WU_REPAIR_ADMIN_TOOL_ENTRY = 'tool-impl/wu-lifecycle-tools.ts#wuRepairAdminTool';
102
125
  const GATES_TOOL_ENTRY = 'tool-impl/wu-lifecycle-tools.ts#gatesTool';
103
126
  // WU-2729 (INIT-060 Phase 2): gates:docs exposes docs-only gate runs via a
104
127
  // dedicated manifest entry so remote callers can request the docs gate
@@ -210,6 +233,11 @@ const TOOL_PERMISSIONS = {
210
233
  'wu:recover': 'write',
211
234
  'wu:release': 'write',
212
235
  'wu:repair': 'write',
236
+ // WU-2833 (INIT-062 WU-G): privileged recovery surface for cloud-team
237
+ // phone UX. Distinct tool name so an approval gate + admin permission
238
+ // can be declared without widening the scope of the default wu:repair
239
+ // implementer tool.
240
+ 'wu:repair:admin': 'admin',
213
241
  'wu:sandbox': 'write',
214
242
  'wu:status': 'read',
215
243
  'wu:unblock': 'write',
@@ -241,8 +269,12 @@ const TOOL_PERMISSIONS = {
241
269
  'lane:suggest': 'write',
242
270
  'flow:bottlenecks': 'read',
243
271
  'flow:report': 'read',
244
- gates: 'write',
245
- 'gates:docs': 'write',
272
+ // WU-2833 (INIT-062 WU-G): gates and gates:docs are read-only validation
273
+ // runners. Mobile/cloud tokens for these tools must not carry workspace
274
+ // write access (principle of least privilege; matches the security
275
+ // posture established by WU-2810/2811/2816).
276
+ gates: 'read',
277
+ 'gates:docs': 'read',
246
278
  'file:delete': 'write',
247
279
  'file:edit': 'write',
248
280
  'file:read': 'read',
@@ -278,7 +310,11 @@ const TOOL_PERMISSIONS = {
278
310
  'lumenflow:release': 'write',
279
311
  'lumenflow:upgrade': 'write',
280
312
  metrics: 'read',
281
- 'metrics:snapshot': 'read',
313
+ // WU-2833 (INIT-062 WU-G): metrics:snapshot reads the workspace and
314
+ // writes a DORA snapshot back into .lumenflow/state/. Permission role
315
+ // is "write" because it mutates state; the explicit SCOPE_OVERRIDE
316
+ // narrows the write path to .lumenflow/state/** (no full-tree write).
317
+ 'metrics:snapshot': 'write',
282
318
  'lumenflow:metrics': 'read',
283
319
  'signal:cleanup': 'write',
284
320
  'sync:templates': 'write',
@@ -320,6 +356,7 @@ const TOOL_ENTRY_OVERRIDES: Partial<Record<ToolName, string>> = {
320
356
  'wu:release': WU_RELEASE_TOOL_ENTRY,
321
357
  'wu:recover': WU_RECOVER_TOOL_ENTRY,
322
358
  'wu:repair': WU_REPAIR_TOOL_ENTRY,
359
+ 'wu:repair:admin': WU_REPAIR_ADMIN_TOOL_ENTRY,
323
360
  'wu:infer-lane': WU_INFER_LANE_TOOL_ENTRY,
324
361
  gates: GATES_TOOL_ENTRY,
325
362
  'gates:docs': GATES_DOCS_TOOL_ENTRY,
@@ -415,9 +452,36 @@ function requiredScopesForPermission(permission: ToolPermission): PathScope[] {
415
452
  return createPathScopes([FULL_WORKSPACE_SCOPE_PATTERN], TOOL_SCOPE_ACCESS.READ);
416
453
  }
417
454
 
455
+ // WU-2833: admin permission inherits the same constrained write-scope
456
+ // set as write permission. The admin distinction is carried by the
457
+ // required_approvals gate, not by broader path access.
418
458
  return createPathScopes(SOFTWARE_DELIVERY_WRITE_SCOPE_PATTERNS, TOOL_SCOPE_ACCESS.WRITE);
419
459
  }
420
460
 
461
+ /**
462
+ * WU-2833 (INIT-062 WU-G): per-tool scope overrides for tools whose
463
+ * runtime semantics do not match the default read/write scope set. Used
464
+ * sparingly — only when a tool legitimately needs both read and a narrow
465
+ * write scope (or vice versa).
466
+ */
467
+ const SCOPE_OVERRIDES: Partial<Record<string, PathScope[]>> = {
468
+ // metrics:snapshot reads the full workspace to compute DORA metrics,
469
+ // then writes the snapshot back into workspace state. The narrow write
470
+ // scope prevents mobile/cloud tokens from leaking full-tree write.
471
+ 'metrics:snapshot': [
472
+ {
473
+ type: TOOL_SCOPE_TYPES.PATH,
474
+ pattern: FULL_WORKSPACE_SCOPE_PATTERN,
475
+ access: TOOL_SCOPE_ACCESS.READ,
476
+ },
477
+ {
478
+ type: TOOL_SCOPE_TYPES.PATH,
479
+ pattern: SOFTWARE_DELIVERY_WORKSPACE_STATE_WRITE_PATTERN,
480
+ access: TOOL_SCOPE_ACCESS.WRITE,
481
+ },
482
+ ],
483
+ };
484
+
421
485
  /**
422
486
  * WU-2729 (INIT-060 Phase 2): the 10 software-delivery pack tools that are
423
487
  * callable remotely via POST /tools/:name. The HTTP surface uses this list
@@ -460,17 +524,23 @@ const APPROVAL_OVERRIDES: Partial<Record<ToolName, readonly string[]>> = {
460
524
  'plan:promote': [SOFTWARE_DELIVERY_APPROVAL_IDS.REMOTE_MUTATION],
461
525
  'initiative:create': [SOFTWARE_DELIVERY_APPROVAL_IDS.REMOTE_MUTATION],
462
526
  'initiative:add-wu': [SOFTWARE_DELIVERY_APPROVAL_IDS.REMOTE_MUTATION],
527
+ // WU-2833: privileged recovery MUST present an explicit approval gate
528
+ // to conductor/phone UX before dispatch. Without this the admin tool
529
+ // is indistinguishable from wu:repair from an authorisation standpoint.
530
+ 'wu:repair:admin': [SOFTWARE_DELIVERY_APPROVAL_IDS.REMOTE_MUTATION],
463
531
  };
464
532
 
465
533
  function requiredApprovalsForTool(name: ToolName): string[] | undefined {
466
- // WU-2729: only the 10 remote-callable tools carry explicit
534
+ // WU-2729: the 10 remote-callable tools carry explicit
467
535
  // required_approvals metadata (even if empty). Other tools leave the
468
536
  // field undefined so the manifest stays minimally descriptive.
537
+ // WU-2833: admin-permission tools also carry explicit approvals so the
538
+ // privileged surface cannot be invoked without a visible approval gate.
469
539
  const isRemoteCallable = (REMOTE_CALLABLE_TOOLS as readonly string[]).includes(name);
470
- if (!isRemoteCallable) {
540
+ const override = APPROVAL_OVERRIDES[name];
541
+ if (!isRemoteCallable && override === undefined) {
471
542
  return undefined;
472
543
  }
473
- const override = APPROVAL_OVERRIDES[name];
474
544
  return override ? [...override] : [];
475
545
  }
476
546
 
@@ -487,11 +557,15 @@ function createManifestTools(): SoftwareDeliveryManifestTool[] {
487
557
  return (Object.keys(TOOL_PERMISSIONS) as ToolName[]).map((name) => {
488
558
  const permission = TOOL_PERMISSIONS[name];
489
559
  const approvals = requiredApprovalsForTool(name);
560
+ // WU-2833: per-tool scope overrides take priority over the default
561
+ // permission-derived scope set so read-plus-narrow-write tools like
562
+ // metrics:snapshot can declare both accesses on a single entry.
563
+ const scopeOverride = SCOPE_OVERRIDES[name];
490
564
  const entry: SoftwareDeliveryManifestTool = {
491
565
  name,
492
566
  entry: resolveToolEntry(name),
493
567
  permission,
494
- required_scopes: requiredScopesForPermission(permission),
568
+ required_scopes: scopeOverride ? [...scopeOverride] : requiredScopesForPermission(permission),
495
569
  };
496
570
  if (approvals !== undefined) {
497
571
  entry.required_approvals = approvals;
@@ -543,6 +617,19 @@ const SOFTWARE_DELIVERY_EMITTED_EVENT_KINDS = [
543
617
  'software-delivery:plan_created',
544
618
  'software-delivery:plan_linked',
545
619
  'software-delivery:plan_promoted',
620
+ // WU-2832 (INIT-062 WU-F): close the cloud-team polling gap with 9
621
+ // additional ephemeral kinds. Validation pair (validated/invalid),
622
+ // recovery, preflight, escalation, ratchet, bottleneck, DORA snapshot,
623
+ // and replay-artifact addressable by event_id.
624
+ 'software-delivery:wu_spec_validated',
625
+ 'software-delivery:wu_spec_invalid',
626
+ 'software-delivery:wu_recovered',
627
+ 'software-delivery:wu_preflight_failed',
628
+ 'software-delivery:wu_escalation_resolved',
629
+ 'software-delivery:test_ratchet_adjusted',
630
+ 'software-delivery:flow_bottleneck_detected',
631
+ 'software-delivery:dora_metric_snapshot',
632
+ 'software-delivery:replay_artifact_published',
546
633
  ] as const;
547
634
  const SOFTWARE_DELIVERY_REQUIRED_SURFACES = ['http'] as const;
548
635
 
@@ -200,6 +200,15 @@ tools:
200
200
  entry: tool-impl/wu-lifecycle-tools.ts#wuRepairTool
201
201
  permission: write
202
202
  required_scopes: *softwareDeliveryWriteScopes
203
+ # WU-2833 (INIT-062 WU-G): privileged wu:repair surface for cloud-team
204
+ # phone UX. Distinct tool name so approval gate + admin permission can
205
+ # be declared without widening the default wu:repair surface.
206
+ - name: wu:repair:admin
207
+ entry: tool-impl/wu-lifecycle-tools.ts#wuRepairAdminTool
208
+ permission: admin
209
+ required_scopes: *softwareDeliveryWriteScopes
210
+ required_approvals:
211
+ - software-delivery:remote_mutation
203
212
  - name: wu:sandbox
204
213
  entry: tool-impl/wu-lifecycle-tools.ts#wuSandboxTool
205
214
  permission: write
@@ -377,15 +386,26 @@ tools:
377
386
  access: read
378
387
  required_approvals: []
379
388
  # gate:*
389
+ # WU-2833 (INIT-062 WU-G): gates / gates:docs are read-only validation
390
+ # runners. Declaring them permission: write would grant mobile/cloud
391
+ # tokens full-tree write access via required_scopes (the security model
392
+ # hardened by WU-2810/2811/2816). Re-declare as permission: read and
393
+ # scope them to SOFTWARE_DELIVERY_READ_SCOPE (wildcard path, read).
380
394
  - name: gates
381
395
  entry: tool-impl/wu-lifecycle-tools.ts#gatesTool
382
- permission: write
383
- required_scopes: *softwareDeliveryWriteScopes
396
+ permission: read
397
+ required_scopes:
398
+ - type: path
399
+ pattern: '**'
400
+ access: read
384
401
  required_approvals: []
385
402
  - name: gates:docs
386
403
  entry: tool-impl/wu-lifecycle-tools.ts#gatesDocsTool
387
- permission: write
388
- required_scopes: *softwareDeliveryWriteScopes
404
+ permission: read
405
+ required_scopes:
406
+ - type: path
407
+ pattern: '**'
408
+ access: read
389
409
  required_approvals: []
390
410
  # file:*
391
411
  - name: file:delete
@@ -577,13 +597,20 @@ tools:
577
597
  - type: path
578
598
  pattern: '**'
579
599
  access: read
600
+ # WU-2833 (INIT-062 WU-G): metrics:snapshot reads the workspace and
601
+ # writes a DORA snapshot to .lumenflow/state/. Declared permission:
602
+ # write with narrow write scope + full-read scope (principle of least
603
+ # privilege — no full-tree write).
580
604
  - name: metrics:snapshot
581
605
  entry: tool-impl/flow-metrics-tools.ts#metricsSnapshotTool
582
- permission: read
606
+ permission: write
583
607
  required_scopes:
584
608
  - type: path
585
609
  pattern: '**'
586
610
  access: read
611
+ - type: path
612
+ pattern: .lumenflow/state/**
613
+ access: write
587
614
  - name: lumenflow:metrics
588
615
  entry: tool-impl/runtime-native-tools.ts#lumenflowMetricsTool
589
616
  permission: read
@@ -686,6 +713,16 @@ emitted_event_kinds:
686
713
  - software-delivery:plan_created
687
714
  - software-delivery:plan_linked
688
715
  - software-delivery:plan_promoted
716
+ # WU-2832 (INIT-062 WU-F): close the cloud-team polling gap.
717
+ - software-delivery:wu_spec_validated
718
+ - software-delivery:wu_spec_invalid
719
+ - software-delivery:wu_recovered
720
+ - software-delivery:wu_preflight_failed
721
+ - software-delivery:wu_escalation_resolved
722
+ - software-delivery:test_ratchet_adjusted
723
+ - software-delivery:flow_bottleneck_detected
724
+ - software-delivery:dora_metric_snapshot
725
+ - software-delivery:replay_artifact_published
689
726
  subscribed_event_kinds: []
690
727
  required_approvals: []
691
728
  surfaces_required:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/packs-software-delivery",
3
- "version": "5.0.3",
3
+ "version": "5.2.0",
4
4
  "description": "Software delivery pack for LumenFlow — work units, gates, lanes, initiatives, and agent coordination",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -29,6 +29,11 @@ import {
29
29
  DEFAULT_MAX_ESLINT_WARNINGS,
30
30
  DEFAULT_MIN_COVERAGE,
31
31
  } from '../../constants/gate-constants.js';
32
+ import {
33
+ AGENT_NAMES,
34
+ MANDATORY_AGENT_NAMES,
35
+ MANDATORY_TRIGGERS,
36
+ } from '../../domain/orchestration.constants.js';
32
37
 
33
38
  const DEFAULT_LAYOUT = DOCS_LAYOUT_PRESETS.simple;
34
39
 
@@ -76,7 +81,7 @@ export const DIRECTORIES_DEFAULTS: DirectoriesConfig = {
76
81
  initiativesDir: `${DEFAULT_LAYOUT.tasks}/initiatives`,
77
82
  backlogPath: `${DEFAULT_LAYOUT.tasks}/backlog.md`,
78
83
  statusPath: `${DEFAULT_LAYOUT.tasks}/status.md`,
79
- skillsDir: '.claude/skills',
84
+ skillsDir: '.lumenflow/skills',
80
85
  agentsDir: '.claude/agents',
81
86
  adrDir: DEFAULT_LAYOUT.adrDir,
82
87
  plansDir: `${DEFAULT_LAYOUT.operations}/plans`,
@@ -342,16 +347,35 @@ export const MEMORY_DEFAULTS: MemoryConfig = {
342
347
  };
343
348
 
344
349
  // ---------------------------------------------------------------------------
345
- // Agents (narrow — only defaultClient, which many cohorts need)
350
+ // Agents
346
351
  // ---------------------------------------------------------------------------
347
352
 
353
+ export interface MandatoryAgentsConfig {
354
+ names: string[];
355
+ triggers: Record<string, string[]>;
356
+ [extra: string]: unknown;
357
+ }
358
+
348
359
  export interface AgentsConfig {
349
360
  defaultClient: string;
361
+ roster: string[];
362
+ mandatory: MandatoryAgentsConfig;
350
363
  [extra: string]: unknown;
351
364
  }
352
365
 
366
+ function cloneMandatoryTriggersDefaults(): Record<string, string[]> {
367
+ return Object.fromEntries(
368
+ Object.entries(MANDATORY_TRIGGERS).map(([agentName, patterns]) => [agentName, [...patterns]]),
369
+ );
370
+ }
371
+
353
372
  export const AGENTS_DEFAULTS: AgentsConfig = {
354
373
  defaultClient: LUMENFLOW_CLIENT_IDS.CLAUDE_CODE,
374
+ roster: [...AGENT_NAMES],
375
+ mandatory: {
376
+ names: [...MANDATORY_AGENT_NAMES],
377
+ triggers: cloneMandatoryTriggersDefaults(),
378
+ },
355
379
  };
356
380
 
357
381
  // ---------------------------------------------------------------------------
@@ -172,6 +172,30 @@ function mergeSimple<T>(defaults: T, overrides: unknown): T {
172
172
  return { ...defaults };
173
173
  }
174
174
 
175
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
176
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
177
+ }
178
+
179
+ function isStringArray(value: unknown): value is string[] {
180
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
181
+ }
182
+
183
+ function cloneStringArrayRecord(record: Record<string, string[]>): Record<string, string[]> {
184
+ return Object.fromEntries(Object.entries(record).map(([key, values]) => [key, [...values]]));
185
+ }
186
+
187
+ function readStringArrayRecord(value: unknown): Record<string, string[]> {
188
+ if (!isObjectRecord(value)) {
189
+ return {};
190
+ }
191
+
192
+ return Object.fromEntries(
193
+ Object.entries(value).flatMap(([key, entryValue]) =>
194
+ isStringArray(entryValue) ? [[key, [...entryValue]]] : [],
195
+ ),
196
+ );
197
+ }
198
+
175
199
  function mergeTelemetry(overrides: Partial<TelemetryConfig> | undefined): TelemetryConfig {
176
200
  if (!overrides) {
177
201
  return { methodology: { ...TELEMETRY_DEFAULTS.methodology } };
@@ -189,6 +213,47 @@ function pickEnum<T extends string>(value: unknown, allowed: readonly T[], fallb
189
213
  : fallback;
190
214
  }
191
215
 
216
+ function cloneAgentsDefaults(): AgentsConfig {
217
+ return {
218
+ ...AGENTS_DEFAULTS,
219
+ roster: [...AGENTS_DEFAULTS.roster],
220
+ mandatory: {
221
+ ...AGENTS_DEFAULTS.mandatory,
222
+ names: [...AGENTS_DEFAULTS.mandatory.names],
223
+ triggers: cloneStringArrayRecord(AGENTS_DEFAULTS.mandatory.triggers),
224
+ },
225
+ };
226
+ }
227
+
228
+ function mergeAgents(overrides: unknown): AgentsConfig {
229
+ if (!isObjectRecord(overrides)) {
230
+ return cloneAgentsDefaults();
231
+ }
232
+
233
+ const defaults = cloneAgentsDefaults();
234
+ const { defaultClient, roster, mandatory: overrideMandatory, ...flatOverrides } = overrides;
235
+ const mandatoryOverrides = isObjectRecord(overrideMandatory) ? overrideMandatory : {};
236
+
237
+ return {
238
+ ...defaults,
239
+ ...flatOverrides,
240
+ defaultClient:
241
+ typeof defaultClient === 'string' ? defaultClient : AGENTS_DEFAULTS.defaultClient,
242
+ roster: isStringArray(roster) ? [...roster] : [...AGENTS_DEFAULTS.roster],
243
+ mandatory: {
244
+ ...defaults.mandatory,
245
+ ...mandatoryOverrides,
246
+ names: isStringArray(mandatoryOverrides['names'])
247
+ ? [...mandatoryOverrides['names']]
248
+ : [...AGENTS_DEFAULTS.mandatory.names],
249
+ triggers: {
250
+ ...cloneStringArrayRecord(AGENTS_DEFAULTS.mandatory.triggers),
251
+ ...readStringArrayRecord(mandatoryOverrides['triggers']),
252
+ },
253
+ },
254
+ };
255
+ }
256
+
192
257
  /**
193
258
  * Return the fully-typed pack-local `LumenflowConfig` for the workspace rooted
194
259
  * at `cwd`.
@@ -230,7 +295,7 @@ export function getConfig(cwd: string): LumenflowConfig {
230
295
  memory: mergeSimple<MemoryConfig>(MEMORY_DEFAULTS, section['memory']),
231
296
  ui: mergeSimple<UiConfig>(UI_DEFAULTS, section['ui']),
232
297
  yaml: mergeSimple<YamlConfig>(YAML_DEFAULTS, section['yaml']),
233
- agents: mergeSimple<AgentsConfig>(AGENTS_DEFAULTS, section['agents']),
298
+ agents: mergeAgents(section['agents']),
234
299
  experimental: mergeSimple<ExperimentalConfig>(EXPERIMENTAL_DEFAULTS, section['experimental']),
235
300
  cleanup: mergeSimple<CleanupConfig>(CLEANUP_DEFAULTS, section['cleanup']),
236
301
  telemetry: mergeTelemetry(section['telemetry'] as Partial<TelemetryConfig> | undefined),
@@ -275,7 +340,7 @@ function cloneDefaults(): LumenflowConfig {
275
340
  memory: { ...MEMORY_DEFAULTS },
276
341
  ui: { ...UI_DEFAULTS },
277
342
  yaml: { ...YAML_DEFAULTS },
278
- agents: { ...AGENTS_DEFAULTS },
343
+ agents: cloneAgentsDefaults(),
279
344
  experimental: { ...EXPERIMENTAL_DEFAULTS },
280
345
  cleanup: { ...CLEANUP_DEFAULTS },
281
346
  telemetry: { methodology: { ...TELEMETRY_DEFAULTS.methodology } },
@@ -82,6 +82,32 @@ export const DIRECTORIES = {
82
82
  STATUS_PATH: `${DEFAULT_DOCS_PATHS.tasks}/status.md`,
83
83
  };
84
84
 
85
+ /**
86
+ * Shared agent/client surface paths.
87
+ *
88
+ * Keeps vendor overlay and shared projection roots centralized so runtime code
89
+ * does not scatter hardcoded path literals through CLI/core packages.
90
+ */
91
+ export const AGENT_SURFACES = {
92
+ /** Claude-projected skills directory */
93
+ CLAUDE_SKILLS_DIR: '.claude/skills',
94
+
95
+ /** Claude agent overlay directory */
96
+ CLAUDE_AGENTS_DIR: '.claude/agents',
97
+
98
+ /** Shared cross-vendor projected skills directory */
99
+ SHARED_SKILLS_DIR: '.agents/skills',
100
+
101
+ /** Codex agent overlay directory */
102
+ CODEX_AGENTS_DIR: '.codex/agents',
103
+
104
+ /** Gemini projected skills directory */
105
+ GEMINI_SKILLS_DIR: '.gemini/skills',
106
+
107
+ /** Gemini agent overlay directory */
108
+ GEMINI_AGENTS_DIR: '.gemini/agents',
109
+ };
110
+
85
111
  /**
86
112
  * Build artifact cleanup globs
87
113
  *