@ashlr/hub 3.2.7 → 3.3.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 (149) hide show
  1. package/CHANGELOG.md +268 -0
  2. package/README.md +4 -1
  3. package/dist/build-identity.json +2 -2
  4. package/dist/cli/activation.d.ts +41 -0
  5. package/dist/cli/activation.js +485 -0
  6. package/dist/cli/activation.js.map +1 -0
  7. package/dist/cli/audit.js +18 -0
  8. package/dist/cli/audit.js.map +1 -1
  9. package/dist/cli/backlog.d.ts +0 -5
  10. package/dist/cli/backlog.js +18 -0
  11. package/dist/cli/backlog.js.map +1 -1
  12. package/dist/cli/comms.js +20 -0
  13. package/dist/cli/comms.js.map +1 -1
  14. package/dist/cli/completions.d.ts +6 -1
  15. package/dist/cli/completions.js +35 -11
  16. package/dist/cli/completions.js.map +1 -1
  17. package/dist/cli/daemon.js +10 -3
  18. package/dist/cli/daemon.js.map +1 -1
  19. package/dist/cli/demo.d.ts +0 -27
  20. package/dist/cli/demo.js +17 -0
  21. package/dist/cli/demo.js.map +1 -1
  22. package/dist/cli/help.js +19 -3
  23. package/dist/cli/help.js.map +1 -1
  24. package/dist/cli/inbox.d.ts +14 -2
  25. package/dist/cli/inbox.js +137 -23
  26. package/dist/cli/inbox.js.map +1 -1
  27. package/dist/cli/index.d.ts +1 -0
  28. package/dist/cli/index.js +10 -0
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/onboard.d.ts +0 -13
  31. package/dist/cli/onboard.js +20 -0
  32. package/dist/cli/onboard.js.map +1 -1
  33. package/dist/cli/resources.js +15 -0
  34. package/dist/cli/resources.js.map +1 -1
  35. package/dist/cli/run.js +1 -1
  36. package/dist/cli/run.js.map +1 -1
  37. package/dist/cli/sandbox.js +43 -0
  38. package/dist/cli/sandbox.js.map +1 -1
  39. package/dist/cli/tui.d.ts +0 -6
  40. package/dist/cli/tui.js +14 -0
  41. package/dist/cli/tui.js.map +1 -1
  42. package/dist/core/config.js +70 -0
  43. package/dist/core/config.js.map +1 -1
  44. package/dist/core/daemon/activation-permit.d.ts +268 -33
  45. package/dist/core/daemon/activation-permit.js +1111 -106
  46. package/dist/core/daemon/activation-permit.js.map +1 -1
  47. package/dist/core/daemon/loop.d.ts +13 -1
  48. package/dist/core/daemon/loop.js +192 -9
  49. package/dist/core/daemon/loop.js.map +1 -1
  50. package/dist/core/daemon/runtime-activation-resident-start-protocol.d.ts +141 -0
  51. package/dist/core/daemon/runtime-activation-resident-start-protocol.js +462 -0
  52. package/dist/core/daemon/runtime-activation-resident-start-protocol.js.map +1 -0
  53. package/dist/core/daemon/runtime-activation-resident-start-runtime.d.ts +27 -0
  54. package/dist/core/daemon/runtime-activation-resident-start-runtime.js +28 -0
  55. package/dist/core/daemon/runtime-activation-resident-start-runtime.js.map +1 -0
  56. package/dist/core/daemon/runtime-activation-stopped-consumer.d.ts +139 -0
  57. package/dist/core/daemon/runtime-activation-stopped-consumer.js +995 -0
  58. package/dist/core/daemon/runtime-activation-stopped-consumer.js.map +1 -0
  59. package/dist/core/daemon/runtime-activation-stopped-runtime.d.ts +36 -0
  60. package/dist/core/daemon/runtime-activation-stopped-runtime.js +134 -0
  61. package/dist/core/daemon/runtime-activation-stopped-runtime.js.map +1 -0
  62. package/dist/core/daemon/runtime-activation-transaction.d.ts +41 -17
  63. package/dist/core/daemon/runtime-activation-transaction.js +80 -11
  64. package/dist/core/daemon/runtime-activation-transaction.js.map +1 -1
  65. package/dist/core/daemon/runtime-release-dependency-inventory.js +1 -0
  66. package/dist/core/daemon/runtime-release-dependency-inventory.js.map +1 -1
  67. package/dist/core/daemon/service-install-authority.d.ts +0 -4
  68. package/dist/core/daemon/service-install-authority.js +18 -3
  69. package/dist/core/daemon/service-install-authority.js.map +1 -1
  70. package/dist/core/dashboard.d.ts +10 -0
  71. package/dist/core/dashboard.js +103 -28
  72. package/dist/core/dashboard.js.map +1 -1
  73. package/dist/core/effective-config.js +7 -0
  74. package/dist/core/effective-config.js.map +1 -1
  75. package/dist/core/fleet/automerge-pass.js +33 -0
  76. package/dist/core/fleet/automerge-pass.js.map +1 -1
  77. package/dist/core/fleet/decisions-ledger.js +35 -12
  78. package/dist/core/fleet/decisions-ledger.js.map +1 -1
  79. package/dist/core/fleet/external-skill-git-capture.js +9 -1
  80. package/dist/core/fleet/external-skill-git-capture.js.map +1 -1
  81. package/dist/core/fleet/judge-decision-metadata.d.ts +8 -1
  82. package/dist/core/fleet/judge-decision-metadata.js +14 -1
  83. package/dist/core/fleet/judge-decision-metadata.js.map +1 -1
  84. package/dist/core/fleet/manager.d.ts +15 -1
  85. package/dist/core/fleet/manager.js +108 -30
  86. package/dist/core/fleet/manager.js.map +1 -1
  87. package/dist/core/fleet/post-merge-credit.d.ts +134 -9
  88. package/dist/core/fleet/post-merge-credit.js +350 -10
  89. package/dist/core/fleet/post-merge-credit.js.map +1 -1
  90. package/dist/core/fleet/self-improve.d.ts +34 -1
  91. package/dist/core/fleet/self-improve.js +106 -1
  92. package/dist/core/fleet/self-improve.js.map +1 -1
  93. package/dist/core/fleet/skill-library.d.ts +21 -2
  94. package/dist/core/fleet/skill-library.js +54 -12
  95. package/dist/core/fleet/skill-library.js.map +1 -1
  96. package/dist/core/fleet/status.js +1 -0
  97. package/dist/core/fleet/status.js.map +1 -1
  98. package/dist/core/fleet/worked-ledger.d.ts +22 -0
  99. package/dist/core/fleet/worked-ledger.js +32 -0
  100. package/dist/core/fleet/worked-ledger.js.map +1 -1
  101. package/dist/core/foundry/automerge-scope-policy.d.ts +2 -2
  102. package/dist/core/foundry/automerge-scope-policy.js +9 -2
  103. package/dist/core/foundry/automerge-scope-policy.js.map +1 -1
  104. package/dist/core/goals/store.js +18 -4
  105. package/dist/core/goals/store.js.map +1 -1
  106. package/dist/core/inbox/apply.js +5 -2
  107. package/dist/core/inbox/apply.js.map +1 -1
  108. package/dist/core/inbox/merge.d.ts +94 -1
  109. package/dist/core/inbox/merge.js +349 -16
  110. package/dist/core/inbox/merge.js.map +1 -1
  111. package/dist/core/integrations/github.d.ts +2 -0
  112. package/dist/core/integrations/github.js +2 -1
  113. package/dist/core/integrations/github.js.map +1 -1
  114. package/dist/core/learn/reflect.d.ts +42 -4
  115. package/dist/core/learn/reflect.js +56 -5
  116. package/dist/core/learn/reflect.js.map +1 -1
  117. package/dist/core/run/engines.d.ts +8 -0
  118. package/dist/core/run/engines.js +9 -1
  119. package/dist/core/run/engines.js.map +1 -1
  120. package/dist/core/run/learned-router.js +28 -3
  121. package/dist/core/run/learned-router.js.map +1 -1
  122. package/dist/core/run/orchestrator.d.ts +7 -2
  123. package/dist/core/run/orchestrator.js +86 -60
  124. package/dist/core/run/orchestrator.js.map +1 -1
  125. package/dist/core/swarm/rollback.d.ts +11 -0
  126. package/dist/core/swarm/rollback.js +54 -0
  127. package/dist/core/swarm/rollback.js.map +1 -1
  128. package/dist/core/swarm/runner.js +94 -8
  129. package/dist/core/swarm/runner.js.map +1 -1
  130. package/dist/core/types.d.ts +100 -1
  131. package/dist/core/types.js.map +1 -1
  132. package/dist/core/web/api.js +105 -7
  133. package/dist/core/web/api.js.map +1 -1
  134. package/dist/core/web/control.d.ts +20 -0
  135. package/dist/core/web/control.js +44 -32
  136. package/dist/core/web/control.js.map +1 -1
  137. package/dist/core/web/fleet-status-cache.d.ts +68 -0
  138. package/dist/core/web/fleet-status-cache.js +120 -0
  139. package/dist/core/web/fleet-status-cache.js.map +1 -0
  140. package/dist/core/web/public/next/assets/index-CY0U241s.css +1 -0
  141. package/dist/core/web/public/next/assets/index-Ct0mf8n4.js +69 -0
  142. package/dist/core/web/public/next/assets/index-Ct0mf8n4.js.map +1 -0
  143. package/dist/core/web/public/next/index.html +15 -0
  144. package/dist/release-dependency-inventory.json +1 -1
  145. package/docs/RUNTIME_ACTIVATION_AUTHORITY.md +126 -21
  146. package/docs/contracts/CONTRACT-M521.md +102 -0
  147. package/package.json +20 -3
  148. package/schema/config.schema.json +800 -5
  149. package/node_modules/which/CHANGELOG.md +0 -166
@@ -45,7 +45,7 @@ import { loadQueuedAutonomyItems } from '../portfolio/queued-autonomy.js';
45
45
  import { acquireDaemonLock, accountDaemonSpendGuard, armDaemonSpendGuard, clearDaemonSpendGuard, DAEMON_SPEND_GUARD_ITEM_CAPACITY, daemonStatePath, loadDaemonState, loadDaemonStateStrict, readDaemonLockOwner, readDaemonSpendGuard, releaseDaemonLock, resetDayIfNeeded, saveDaemonStateResult, upgradeLegacyDaemonSpendGuard, } from './state.js';
46
46
  import { acquireLocalStoreLock, releaseLocalStoreLock, } from '../fleet/local-store-lock.js';
47
47
  import { readDaemonActivity, writeDaemonActivity, } from './activity.js';
48
- import { consumeDaemonActivationPermit, isDaemonActivationCapability, } from './activation-permit.js';
48
+ import { consumeDaemonActivationPermit, daemonActivationScopeGranted, isDaemonActivationCapability, } from './activation-permit.js';
49
49
  import { nullSink } from '../run/streaming.js';
50
50
  import { createOuterAttemptIdentity } from '../fleet/attempt-identity.js';
51
51
  import { runSwarm } from '../swarm/runner.js';
@@ -93,7 +93,7 @@ import { observeShadowSkills } from '../fleet/skill-shadow-observer.js';
93
93
  import { selectWorkQueueCoordinator } from '../seams/work-queue-coordinator.js';
94
94
  // M220: verdict-feedback sweep — feed judge rejections back to the ledger so
95
95
  // re-clogging items (e.g. "CI is failing") are suppressed for the cooldown window.
96
- import { GENERATED_REPAIR_DISPATCH_BLOCKED_COOLDOWN_MS, latestWorkedEventForKeys, recordOutcome as recordWorkedOutcome, sweepJudgedProposals, workedEventIsCooling, } from '../fleet/worked-ledger.js';
96
+ import { DEFAULT_MAX_REJECTION_ATTEMPTS, GENERATED_REPAIR_DISPATCH_BLOCKED_COOLDOWN_MS, latestWorkedEventForKeys, recordOutcome as recordWorkedOutcome, rejectionAttemptCount, sweepJudgedProposals, workedEventIsCooling, } from '../fleet/worked-ledger.js';
97
97
  import { blockingPendingProposalsForBacklog, pendingProposalItemKeysForBacklog, workItemCoverageKey, } from '../fleet/proposal-matching.js';
98
98
  import { loadConfig } from '../config.js';
99
99
  import { hostname as osHostname } from 'node:os';
@@ -727,6 +727,13 @@ function generatedRepairShouldSkip(workedEvents, policy) {
727
727
  const latest = latestWorkedEventForKeys(workedEvents, policy.itemIds);
728
728
  if (!latest)
729
729
  return false;
730
+ // Attempt cap: cooldown alone only delays a retry — after enough windows
731
+ // elapse the same rejected item comes back forever. Once it has racked up
732
+ // DEFAULT_MAX_REJECTION_ATTEMPTS real judge rejections, park it instead of
733
+ // continuing to re-dispatch (see workItemAttemptCapExceeded/summarizeSelectionBlockers
734
+ // for the operator-visible counter).
735
+ if (workItemAttemptCapExceeded(workedEvents, policy))
736
+ return true;
730
737
  const override = policy.outcomeCooldownMs?.[latest.outcome];
731
738
  if (override !== undefined) {
732
739
  const eventMs = Date.parse(latest.ts);
@@ -734,6 +741,16 @@ function generatedRepairShouldSkip(workedEvents, policy) {
734
741
  }
735
742
  return workedEventIsCooling(latest, policy.cooldownMs);
736
743
  }
744
+ /**
745
+ * True once a work item has racked up DEFAULT_MAX_REJECTION_ATTEMPTS real
746
+ * judge rejections (judged-review/judged-noise/judged-decline). Distinct
747
+ * from the time-based cooldown: cooldown expires and the item comes back;
748
+ * an exceeded attempt cap parks the item for good (until something resets
749
+ * its worked-event history, e.g. the item is meaningfully re-authored).
750
+ */
751
+ function workItemAttemptCapExceeded(workedEvents, policy) {
752
+ return rejectionAttemptCount(workedEvents, policy.itemIds) >= DEFAULT_MAX_REJECTION_ATTEMPTS;
753
+ }
737
754
  const RESOURCE_SNAPSHOT_MAX_AGE_MS = 30_000;
738
755
  const GENERATED_REPAIR_EMPTY_FAST_COOLDOWN_MS = 30 * 60 * 1000;
739
756
  const MAX_GENERATED_REPAIR_DECISION_EVENTS = 20;
@@ -779,6 +796,26 @@ const LOCAL_ONLY_BACKENDS = new Set(['builtin', 'local-coder', 'ashlrcode', 'aw'
779
796
  const DRAIN_MODE_TAG_PREFIX = 'drain:';
780
797
  const MAX_DRAIN_SELECTED_IDS = 12;
781
798
  const MAX_DIAGNOSTIC_RESLICE_DRAIN_LIMIT = 50;
799
+ /**
800
+ * M470: the exact authority the pre-M470 boolean capability check conveyed.
801
+ *
802
+ * Declared here rather than imported from activation-permit.js on purpose:
803
+ * several suites mock that module wholesale, and importing a *value* from it
804
+ * would throw at module load under those mocks. The explicit type annotation
805
+ * still makes tsc fail if `DaemonActivationGrantScope` ever gains a key, so
806
+ * this cannot silently drift into granting more than it should.
807
+ */
808
+ const LEGACY_PROPOSAL_ONLY_SCOPE = Object.freeze({
809
+ once: true,
810
+ resident: false,
811
+ residentStanding: false,
812
+ conductor: false,
813
+ automerge: false,
814
+ repair: false,
815
+ deploy: false,
816
+ install: false,
817
+ proposalOnly: true,
818
+ });
782
819
  /** Schedule advisory post-state work only after a durable resident tick returns. */
783
820
  export function scheduleResolutionObserverAfterTick(tickResult, opts, schedule = scheduleResolutionObserverChild, killIsOn = killSwitchOn) {
784
821
  if (opts.dryRun || opts.once || tickResult.reason === 'state-persistence-failed' ||
@@ -1573,7 +1610,8 @@ function recordQueueSelectionAgentAction(fields) {
1573
1610
  action: 'daemon:selection',
1574
1611
  summary: `${lane}: claimed ${fields.claimedItems.length}/${fields.selectedItems.length} ` +
1575
1612
  `from ${fields.eligibleItems}/${fields.backlogItems} eligible; ` +
1576
- `cooldown ${fields.cooldownBlocked}, pending ${fields.pendingBlocked}, route ${fields.routeBlocked}`,
1613
+ `cooldown ${fields.cooldownBlocked}, pending ${fields.pendingBlocked}, route ${fields.routeBlocked}` +
1614
+ (fields.attemptCapBlocked > 0 ? `, attempt-cap-parked ${fields.attemptCapBlocked}` : ''),
1577
1615
  reason: fields.dryRun ? 'dry-run' : fields.claimedItems.length > 0 ? 'selected' : 'no-claim',
1578
1616
  ...(first?.repo ? { repo: first.repo } : {}),
1579
1617
  ...(first?.id ? { itemId: boundedText(first.id, 120) } : {}),
@@ -1593,6 +1631,7 @@ function recordQueueSelectionAgentAction(fields) {
1593
1631
  cooldownBlocked: fields.cooldownBlocked,
1594
1632
  routeBlocked: fields.routeBlocked,
1595
1633
  fastRepairCooldown: fields.fastRepairCooldown,
1634
+ attemptCapBlocked: fields.attemptCapBlocked,
1596
1635
  rawSelectCount: fields.rawSelectCount,
1597
1636
  selectCount: fields.selectCount,
1598
1637
  selected: fields.selectedItems.length,
@@ -2841,7 +2880,21 @@ function claimCooldownPolicyForSelectionItem(item, baseCooldownMs, repairRecover
2841
2880
  */
2842
2881
  export async function tick(cfg, opts) {
2843
2882
  const now = new Date().toISOString();
2844
- const activationAccepted = !opts.dryRun && isDaemonActivationCapability(opts.activationCapability);
2883
+ // A pre-verified scope (resident path) is accepted WITHOUT consuming, since
2884
+ // runDaemon already consumed the capability once on entry. Otherwise fall back
2885
+ // to consuming the capability here, which is the one-shot path.
2886
+ const preVerifiedScope = !opts.dryRun ? opts.activationScope : undefined;
2887
+ const consumedCapability = !opts.dryRun && preVerifiedScope === undefined
2888
+ ? isDaemonActivationCapability(opts.activationCapability)
2889
+ : false;
2890
+ // When the capability check passes but no scope object is readable (a caller
2891
+ // or test that supplies only the claim), fall back to the legacy proposal-only
2892
+ // scope — the exact authority the pre-M470 boolean check conveyed. Never widen.
2893
+ const activationScope = preVerifiedScope
2894
+ ?? (consumedCapability
2895
+ ? (opts.activationCapability?.scope ?? LEGACY_PROPOSAL_ONLY_SCOPE)
2896
+ : undefined);
2897
+ const activationAccepted = !opts.dryRun && activationScope !== undefined;
2845
2898
  if (!opts.dryRun && !activationAccepted) {
2846
2899
  persistAudit({
2847
2900
  action: 'daemon:activation-refused',
@@ -2858,7 +2911,52 @@ export async function tick(cfg, opts) {
2858
2911
  reason: 'activation-refused',
2859
2912
  };
2860
2913
  }
2861
- const proposalOnlyActivation = activationAccepted && opts.activationCapability !== undefined;
2914
+ // A standing `residentStanding` grant is NOT single-use, unlike the
2915
+ // one-shot WeakMap capability that forces `preVerifiedScope` to be trusted
2916
+ // for the run's whole lifetime (see the TickOptions.activationScope doc).
2917
+ // That means it CAN be safely re-checked on every tick with no consumption
2918
+ // side effect — so a resident daemon that started off a standing grant
2919
+ // gets a real "stop the next tick" guarantee from `ashlr activation
2920
+ // revoke --grant <id>`, not just "blocks the next `daemon start`." This is
2921
+ // strictly stronger than what the one-shot capability path can offer; it
2922
+ // cannot interrupt a tick already in flight.
2923
+ if (!opts.dryRun && preVerifiedScope?.residentStanding === true) {
2924
+ const stillGranted = daemonActivationScopeGranted('residentStanding');
2925
+ if (!stillGranted.granted) {
2926
+ persistAudit({
2927
+ action: 'daemon:activation-refused',
2928
+ repo: null,
2929
+ sandboxId: null,
2930
+ summary: `live tick refused: resident standing grant no longer authorized: ${stillGranted.reason}`,
2931
+ result: 'refused',
2932
+ });
2933
+ return {
2934
+ ts: now,
2935
+ itemsConsidered: 0,
2936
+ proposalsCreated: 0,
2937
+ spentUsd: 0,
2938
+ reason: 'activation-refused',
2939
+ };
2940
+ }
2941
+ }
2942
+ // M470: proposal-only is derived from the GRANTED scope rather than from "an
2943
+ // activation exists at all" — the previous form forced every authorized tick
2944
+ // to proposal-only, so an automerge/repair/deploy grant could never take
2945
+ // effect.
2946
+ //
2947
+ // The `grantedScope` distinction matters: the pre-M470 condition was
2948
+ // `opts.activationCapability !== undefined`, i.e. it keyed off a real permit
2949
+ // object being supplied, NOT off the boolean claim. Callers that pass only a
2950
+ // claim (and the legacy fallback above) must keep the old
2951
+ // `proposalOnlyActivation === false` behavior, otherwise
2952
+ // runAncillaryMaintenance — which early-returns on proposal-only — silently
2953
+ // stops running self-heal, invent, and drain.
2954
+ const grantedScope = preVerifiedScope ?? opts.activationCapability?.scope;
2955
+ const proposalOnlyActivation = activationAccepted &&
2956
+ grantedScope !== undefined &&
2957
+ !grantedScope.automerge &&
2958
+ !grantedScope.repair &&
2959
+ !grantedScope.deploy;
2862
2960
  let ownershipLost = false;
2863
2961
  const stillOwnsTick = () => {
2864
2962
  if (!opts.ownerLock)
@@ -3434,6 +3532,42 @@ export async function tick(cfg, opts) {
3434
3532
  console.warn('[ashlr] daemon:tick outcomeWatcher failed:', err?.message ?? err);
3435
3533
  }
3436
3534
  }
3535
+ // M505: Learning-loop sweeps. Each of these was previously unreachable in a
3536
+ // live tick — rejection learning sat behind `autoMerge.enabled`, post-merge
3537
+ // credit was a stubbed `false`, and reflection was a manual CLI command — so
3538
+ // the fleet observed every run and learned from none of them.
3539
+ if (!stopRequested() && liveCfg.foundry?.['selfImprove'] !== false) {
3540
+ try {
3541
+ const { sweepRejectionLearning } = await import('../fleet/self-improve.js');
3542
+ sweepRejectionLearning(liveCfg);
3543
+ }
3544
+ catch (err) {
3545
+ console.warn('[ashlr] daemon:tick sweepRejectionLearning failed:', err?.message ?? err);
3546
+ }
3547
+ }
3548
+ // Credit is released only by the M505 protocol's own fail-closed checks
3549
+ // (authenticated merge witness, 14-day quiet window, no adverse observation
3550
+ // on the exact event, healthy ledger read). This call only drives the sweep.
3551
+ if (!stopRequested()) {
3552
+ try {
3553
+ const { sweepPostMergeCreditReleases } = await import('../fleet/post-merge-credit.js');
3554
+ sweepPostMergeCreditReleases();
3555
+ }
3556
+ catch (err) {
3557
+ console.warn('[ashlr] daemon:tick sweepPostMergeCreditReleases failed:', err?.message ?? err);
3558
+ }
3559
+ }
3560
+ // Low cadence, matching the counterfactual-replay pattern above: reflection
3561
+ // persists a snapshot and distills playbooks, and does not need every tick.
3562
+ if (!stopRequested() && state.ticks.length % 20 === 0) {
3563
+ try {
3564
+ const { runReflectionCycle } = await import('../learn/reflect.js');
3565
+ await runReflectionCycle(liveCfg);
3566
+ }
3567
+ catch (err) {
3568
+ console.warn('[ashlr] daemon:tick runReflectionCycle failed:', err?.message ?? err);
3569
+ }
3570
+ }
3437
3571
  // M189: Regression sentinel — detect regressions introduced by auto-merge and bisect/revert.
3438
3572
  const regressionSentinel = liveCfg.foundry?.regressionSentinel;
3439
3573
  if (!stopRequested() &&
@@ -4218,11 +4352,19 @@ export async function tick(cfg, opts) {
4218
4352
  let cooldownBlocked = 0;
4219
4353
  let routeBlocked = 0;
4220
4354
  let fastRepairCooldown = 0;
4355
+ let attemptCapBlocked = 0;
4221
4356
  for (const item of items) {
4222
4357
  const pending = pendingItemKeys.has(workItemCoverageKey(item));
4223
4358
  const itemPolicy = claimCooldownPolicies.get(item.id);
4224
4359
  const itemCooldownMs = cooldownMsForSelectionItem(selectionWorkedEvents, itemPolicy);
4225
4360
  const cooling = generatedRepairShouldSkip(selectionWorkedEvents, itemPolicy);
4361
+ // Attempt-cap-parked items are already folded into `cooling` (they
4362
+ // must never be dispatched), but they get their own counter so an
4363
+ // operator can tell "will retry after cooldown" apart from "parked
4364
+ // for good — rejected too many times" instead of it being silently
4365
+ // indistinguishable in the tick summary.
4366
+ if (cooling && workItemAttemptCapExceeded(selectionWorkedEvents, itemPolicy))
4367
+ attemptCapBlocked++;
4226
4368
  if (pending)
4227
4369
  pendingBlocked++;
4228
4370
  if (cooling)
@@ -4238,7 +4380,7 @@ export async function tick(cfg, opts) {
4238
4380
  fastRepairCooldown++;
4239
4381
  }
4240
4382
  }
4241
- return { eligibleItems, pendingBlocked, cooldownBlocked, routeBlocked, fastRepairCooldown };
4383
+ return { eligibleItems, pendingBlocked, cooldownBlocked, routeBlocked, fastRepairCooldown, attemptCapBlocked };
4242
4384
  };
4243
4385
  const selectRoundRobinCandidates = (items, count) => {
4244
4386
  // Group selectable items by repo (score-sorted within each group by buildBacklog).
@@ -4438,6 +4580,7 @@ export async function tick(cfg, opts) {
4438
4580
  cooldownBlocked: selectionBlockers.cooldownBlocked,
4439
4581
  routeBlocked: selectionBlockers.routeBlocked,
4440
4582
  fastRepairCooldown: selectionBlockers.fastRepairCooldown,
4583
+ attemptCapBlocked: selectionBlockers.attemptCapBlocked,
4441
4584
  rawSelectCount: selectionTelemetryRawSelectCount,
4442
4585
  selectCount: selectionTelemetrySelectCount,
4443
4586
  selectedItems: selected,
@@ -7118,6 +7261,20 @@ export async function runDaemon(cfg, opts) {
7118
7261
  process.env['ASHLR_IN_DAEMON'] = '1';
7119
7262
  const activationCfg = activation.configSnapshot ?? cfg;
7120
7263
  const dcfg = resolveCfg(activationCfg);
7264
+ // M470: verify the capability EXACTLY ONCE, here, and carry the resulting
7265
+ // scope into every tick. isDaemonActivationCapability() consumes on first
7266
+ // call, so verifying per-tick would authorize only the first resident tick
7267
+ // and refuse all subsequent ones. Entering the loop is the authorized act;
7268
+ // each tick then reads the scope as plain data.
7269
+ //
7270
+ // `activation.scope` is the standing-`residentStanding`-grant path
7271
+ // (consumeDaemonActivationPermit tried it first): no WeakMap capability
7272
+ // exists for it at all, since standing grants aren't single-use — tick()
7273
+ // re-checks it live every cycle instead (see its `residentStanding` guard).
7274
+ const runActivationScope = activation.scope
7275
+ ?? (!opts.dryRun && activation.capability && isDaemonActivationCapability(activation.capability)
7276
+ ? activation.capability.scope
7277
+ : undefined);
7121
7278
  // -------------------------------------------------------------------------
7122
7279
  // Mark daemon as running.
7123
7280
  // -------------------------------------------------------------------------
@@ -7226,10 +7383,18 @@ export async function runDaemon(cfg, opts) {
7226
7383
  sandboxId: null,
7227
7384
  summary: `daemon started: once=${opts.once}, dryRun=${opts.dryRun}, budget=$${dcfg.dailyBudgetUsd}, ` +
7228
7385
  `intervalMs=${dcfg.intervalMs}${opts.drain ? `, drain=${opts.drain}` : ''}` +
7229
- `${opts.drainLimit ? `, drainLimit=${opts.drainLimit}` : ''}`,
7386
+ `${opts.drainLimit ? `, drainLimit=${opts.drainLimit}` : ''}` +
7387
+ `${activation.grantId ? `, activatedByStandingGrant=${activation.grantId}` : ''}`,
7230
7388
  result: 'ok',
7231
7389
  });
7232
- if (!opts.dryRun && !activation.capability) {
7390
+ // `!activation.scope` preserves this exactly as it behaved before the
7391
+ // standing-grant path existed: a live (non-dry-run) authorized start always
7392
+ // has EITHER `activation.capability` (one-shot path) OR `activation.scope`
7393
+ // (standing-grant path) set, so this was — and remains — unreachable on any
7394
+ // real authorized run. Written this way rather than changed so the
7395
+ // standing-grant path doesn't silently start exercising a branch nothing
7396
+ // has exercised live before.
7397
+ if (!opts.dryRun && !activation.capability && !activation.scope) {
7233
7398
  reconcilePreparedGeneratedRepairReservations();
7234
7399
  }
7235
7400
  // -------------------------------------------------------------------------
@@ -7324,7 +7489,7 @@ export async function runDaemon(cfg, opts) {
7324
7489
  transitionActivity('tick');
7325
7490
  const tickResult = await tick(liveCfg, {
7326
7491
  dryRun: opts.dryRun,
7327
- ...(activation.capability ? { activationCapability: activation.capability } : {}),
7492
+ ...(runActivationScope ? { activationScope: runActivationScope } : {}),
7328
7493
  ...(opts.drain ? { drain: opts.drain } : {}),
7329
7494
  ...(opts.drainLimit ? { drainLimit: opts.drainLimit } : {}),
7330
7495
  signal: shutdown.signal,
@@ -7377,6 +7542,10 @@ export async function runDaemon(cfg, opts) {
7377
7542
  transitionActivity('tick');
7378
7543
  const tickResult = await tick(liveCfg, {
7379
7544
  dryRun: opts.dryRun,
7545
+ // M470: the resident loop previously passed NO activation at all, so
7546
+ // every live resident tick was refused. It now carries the scope
7547
+ // verified once at runDaemon entry.
7548
+ ...(runActivationScope ? { activationScope: runActivationScope } : {}),
7380
7549
  ...(opts.drain ? { drain: opts.drain } : {}),
7381
7550
  ...(opts.drainLimit ? { drainLimit: opts.drainLimit } : {}),
7382
7551
  signal: shutdown.signal,
@@ -7672,6 +7841,20 @@ export function buildItemGoal(item) {
7672
7841
  '(e.g. a platform-gated or intentionally-skipped test, an issue requiring ' +
7673
7842
  'product decisions, or already done), make NO changes and stop — ' +
7674
7843
  'do not force an edit.');
7844
+ // M507: completion contract — derived from real inbox data showing the two
7845
+ // most common ways an otherwise-reasonable run still gets rejected: (1) the
7846
+ // engine "fixes" a narrow task by deleting/regenerating an entire file (seen
7847
+ // repeatedly on dependency bumps that wiped package.json instead of editing
7848
+ // one version string — judged 'harmful' every time), and (2) a run hits its
7849
+ // budget mid-task and files a half-finished '[partial]' diff that fails
7850
+ // verification and is auto-drained after retries. Neither failure mode is
7851
+ // fixed by a more lenient judge — both are avoidable at generation time.
7852
+ parts.push('Do not delete, regenerate, or wholesale-rewrite a file to make this change — ' +
7853
+ 'edit only what the task requires. A diff that removes unrelated dependencies, ' +
7854
+ 'scripts, or content is treated as destructive and rejected outright, even when ' +
7855
+ 'the stated intent was minor. If you cannot produce a complete diff that passes ' +
7856
+ 'this repo’s tests, typecheck, and lint within the available budget, produce NO ' +
7857
+ 'diff and stop — a filed partial/unverified change is worse than no proposal.');
7675
7858
  return parts.join('\n\n');
7676
7859
  }
7677
7860
  //# sourceMappingURL=loop.js.map