@awak-app/simy-cli 0.7.15 → 0.7.17

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 (121) hide show
  1. package/README.md +225 -98
  2. package/package.json +5 -3
  3. package/src/agent.js +129 -7
  4. package/src/backend-executable.js +7 -6
  5. package/src/bounded-local-task-subtask-pool.js +32 -6
  6. package/src/bounded-local-task-subtasks-contract.js +1 -1
  7. package/src/cli-contract.js +1 -1
  8. package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
  9. package/src/codex-plugins/simy/hooks/README.md +8 -3
  10. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +302 -78
  11. package/src/codex-plugins/simy/hooks/hooks.json +1 -1
  12. package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
  13. package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
  14. package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
  15. package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
  16. package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
  17. package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
  18. package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
  19. package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
  20. package/src/codex-plugins/simy/hooks/outcome-progress.mjs +16 -5
  21. package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
  22. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
  23. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +28 -38
  24. package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +1 -2
  25. package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +5 -63
  26. package/src/codex-plugins/simy/hooks/purpose-search.mjs +21 -4
  27. package/src/codex-plugins/simy/hooks/result-cycle.mjs +166 -0
  28. package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +98 -12
  29. package/src/codex-plugins/simy/hooks/user-experience.mjs +26 -0
  30. package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
  31. package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
  32. package/src/coding-run-recovery.js +4 -2
  33. package/src/delivery-hooks/claude-workflow.md +10 -7
  34. package/src/delivery-hooks/command.js +76 -10
  35. package/src/delivery-hooks/consistency-monitor.js +20 -4
  36. package/src/delivery-hooks/consult.js +60 -0
  37. package/src/delivery-hooks/diagnosis.js +2 -2
  38. package/src/delivery-hooks/legacy-hook-routing.js +46 -8
  39. package/src/delivery-hooks/local-runtime.js +81 -1
  40. package/src/delivery-hooks/local-supervisor.js +13 -4
  41. package/src/delivery-hooks/repair-continuation.js +80 -0
  42. package/src/delivery-hooks/repair.js +164 -0
  43. package/src/delivery-hooks/supervisor-handover.js +84 -0
  44. package/src/delivery-hooks/supervisor-owner.js +3 -2
  45. package/src/delivery-hooks/supervisor-service.js +38 -6
  46. package/src/delivery-hooks/supervisor-watchdog.js +15 -4
  47. package/src/delivery-hooks/supervisor.js +151 -18
  48. package/src/desktop-access.js +2 -1
  49. package/src/desktop-host.js +35 -2
  50. package/src/durable-local-task-worker.js +372 -58
  51. package/src/execution-capability-contract.js +15 -1
  52. package/src/index.js +3 -3
  53. package/src/local-photos-read.swift +33 -0
  54. package/src/local-photos.js +47 -0
  55. package/src/local-task-attachment-store.js +31 -0
  56. package/src/local-task-destination.js +174 -0
  57. package/src/local-task.js +119 -7
  58. package/src/native-delegation-contract.js +68 -0
  59. package/src/native-provider-identity.js +60 -0
  60. package/src/orchestrator/audit.js +24 -323
  61. package/src/orchestrator/budget.js +22 -11
  62. package/src/orchestrator/charter-validation.js +3 -41
  63. package/src/orchestrator/checklist-continuation.js +2 -14
  64. package/src/orchestrator/completion-contract.js +4 -49
  65. package/src/orchestrator/contract.js +5 -9
  66. package/src/orchestrator/delivery/WORKFLOW.md +30 -710
  67. package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -216
  68. package/src/orchestrator/delivery/references/change-boundary-matrix.md +4 -103
  69. package/src/orchestrator/delivery/references/chrome-user-verification.md +5 -133
  70. package/src/orchestrator/delivery/references/cross-repository-control.md +4 -74
  71. package/src/orchestrator/delivery/references/delivery-levels.md +3 -117
  72. package/src/orchestrator/delivery/references/local-sqm-evidence.md +1 -1
  73. package/src/orchestrator/delivery/references/minimum-completion.md +4 -144
  74. package/src/orchestrator/delivery/references/pr-closure.md +4 -200
  75. package/src/orchestrator/delivery/references/shared-system-specifications.md +4 -161
  76. package/src/orchestrator/delivery/references/state-and-receipts.md +4 -245
  77. package/src/orchestrator/delivery/references/state-model.md +4 -185
  78. package/src/orchestrator/delivery/references/ux-specification.md +4 -95
  79. package/src/orchestrator/delivery-level.js +2 -10
  80. package/src/orchestrator/evidence.js +2 -3
  81. package/src/orchestrator/independent-audit.js +15 -164
  82. package/src/orchestrator/instruction.js +19 -254
  83. package/src/orchestrator/loop.js +35 -95
  84. package/src/orchestrator/outcome-monitor.js +21 -0
  85. package/src/orchestrator/pr-closure.js +2 -16
  86. package/src/orchestrator/recovery.js +32 -2
  87. package/src/orchestrator/ux-specification.js +3 -12
  88. package/src/provider-delegation.js +336 -0
  89. package/src/runner.js +5 -2
  90. package/src/session-supervision/acceptance-reads.js +34 -0
  91. package/src/session-supervision/artifact-observer.js +138 -0
  92. package/src/session-supervision/browser-cdp.js +59 -0
  93. package/src/session-supervision/browser-recovery.js +97 -0
  94. package/src/session-supervision/codex-app-server.js +61 -22
  95. package/src/session-supervision/completion-artifacts.js +44 -0
  96. package/src/session-supervision/consistency-auditor.js +8 -11
  97. package/src/session-supervision/continuation-analysis.js +47 -28
  98. package/src/session-supervision/final-response-recovery.js +5 -0
  99. package/src/session-supervision/index.js +22 -5
  100. package/src/session-supervision/interaction-proxy.js +250 -0
  101. package/src/session-supervision/interaction-task.js +189 -0
  102. package/src/session-supervision/native-interaction.js +102 -0
  103. package/src/session-supervision/provider-failure.js +40 -0
  104. package/src/session-supervision/purpose-instruction.js +14 -14
  105. package/src/session-supervision/purpose-recovery.js +32 -12
  106. package/src/session-supervision/purpose-verification.js +50 -216
  107. package/src/session-supervision/recovery-frontier.js +55 -0
  108. package/src/session-supervision/stop-notifications.js +8 -8
  109. package/src/sqm/agent-skill.js +1 -1
  110. package/src/sqm/checkers.js +12 -6
  111. package/src/sqm/claude-plugin-update.js +22 -8
  112. package/src/sqm/claude-plugin.js +6 -0
  113. package/src/sqm/local-tests.js +2 -1
  114. package/src/sqm/plugin-cache.js +67 -0
  115. package/src/sqm/proof.js +25 -0
  116. package/src/sqm/report.js +1 -0
  117. package/src/sqm/workflow-onboarding.js +3 -39
  118. package/src/sqm/workflow-update.js +17 -6
  119. package/src/startup-preflight.js +2 -20
  120. package/src/team-status-fence-store.js +32 -0
  121. package/src/team-threads.js +457 -0
package/src/agent.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { readLocalPhotoSource } from "./local-photos.js";
2
+ import { resolveLocalTaskDestination, saveLocalTaskDestination } from "./local-task-destination.js";
3
+ import { observePairedNativeIdentities } from './native-provider-identity.js';
4
+ import { providerDelegationFromRequest } from './provider-delegation.js';
1
5
  import { createLedgerExecutionWorker, discoverLedgerWorkers } from './update-detachment/worker-ledger.js';
2
6
  import { normalizeProviderModelSelection } from "./provider-model-selection.js";
3
7
  import { assertConnectionScope, connectionScope, connectionDiagnostic } from "./connection-diagnostics.js";
@@ -147,6 +151,7 @@ import {
147
151
  import {
148
152
  createDurableLocalTaskApi,
149
153
  createDurableLocalTaskWorker,
154
+ prepareRepositoryWorkspace,
150
155
  } from "./durable-local-task-worker.js";
151
156
  import { createLocalTaskAttachmentStore } from "./local-task-attachment-store.js";
152
157
  import {
@@ -297,12 +302,20 @@ export async function startAgent({
297
302
  if (!link?.connected || expected && link.epoch !== expected.epoch) throw Object.assign(new Error("Reconnect this provider to SIMY before using it."), { code: "provider_link_disconnected" });
298
303
  return link;
299
304
  };
305
+ let nativeIdentityCapability = null, nativeIdentityActor = null, nativeIdentityObservedAt = 0, nativeIdentityReadInFlight = null;
306
+ const nativeActor = () => JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]);
300
307
  let providerStatuses = { codex: { state: "unknown" }, claude: { state: "unknown" } };
301
308
  const providerRecoveryRevisions = { codex: 0, claude: 0 };
302
309
  const refreshAvailableCapabilities = () => {
303
310
  const features = contractSourceCapabilities.features;
304
311
  availableCapabilities = withCliContract({
305
312
  ...contractSourceCapabilities,
313
+ ...(features?.native_delegation === true || dependencies.observePairedNativeIdentities ? {
314
+ native_delegation: nativeIdentityActor === nativeActor() && isSessionValid(session, Date.now(), apiOrigin)
315
+ ? { ...nativeIdentityCapability, identities: (nativeIdentityCapability?.identities || []).filter(identity =>
316
+ providerLinks[identity.provider]?.connected && (providerLinks[identity.provider]?.epoch ?? null) === identity.connection_epoch) }
317
+ : { schema_version: 'native_delegation_capability.v1', device_id: session?.device_id || null, identities: [] },
318
+ } : {}),
306
319
  ...(purposeSigner ? { purpose_verification: purposeSigner } : {}),
307
320
  ...(features ? { features: { ...features, purpose_host_verification: Boolean(purposeSigner) && isSessionValid(session, Date.now(), apiOrigin), ...(Object.hasOwn(features, "sqm_local_review")
308
321
  ? { sqm_local_review: features.sqm_local_review === true && sqmEnabled && !pendingOrganization && desktopWorkAllowed() } : {}) } } : {}),
@@ -489,6 +502,24 @@ export async function startAgent({
489
502
  notifyDesktopStatusChange();
490
503
  }
491
504
  })]);
505
+ if ((contractSourceCapabilities.features?.native_delegation === true || dependencies.observePairedNativeIdentities) && session?.device_id && !nativeIdentityReadInFlight && (force || nativeIdentityActor !== nativeActor() || Date.now() - nativeIdentityObservedAt > 60000)) {
506
+ const actor = nativeActor();
507
+ const observation = (async () => {
508
+ const observed = await (dependencies.observePairedNativeIdentities || observePairedNativeIdentities)({
509
+ deviceId: session.device_id, providerStatuses, providerLinks });
510
+ if (!shuttingDown && actor === nativeActor()) {
511
+ nativeIdentityCapability = observed; nativeIdentityActor = actor; nativeIdentityObservedAt = Date.now();
512
+ refreshAvailableCapabilities();
513
+ notifyDesktopStatusChange();
514
+ // Official account observation must not delay the existing desktop
515
+ // startup/status path. Publish its catalog when it is actually ready.
516
+ if (deferStartupSynchronization) void heartbeat().catch(error => reportHeartbeatError(error, quiet));
517
+ }
518
+ })();
519
+ nativeIdentityReadInFlight = observation;
520
+ void observation.finally(() => { if (nativeIdentityReadInFlight === observation) nativeIdentityReadInFlight = null; }).catch(() => {});
521
+ if (!deferStartupSynchronization) await observation;
522
+ }
492
523
  const binaryStateChanged = ["codex", "claude"].some((provider) =>
493
524
  previous[provider]?.version !== providerStatuses[provider]?.version ||
494
525
  previous[provider]?.state !== providerStatuses[provider]?.state);
@@ -544,7 +575,21 @@ export async function startAgent({
544
575
  await callback?.(...args);
545
576
  await guard();
546
577
  };
547
- return { ...options, sessionRoot, beforeProviderInvocation: wrap(options.beforeProviderInvocation), beforeSpawn: wrap(options.beforeSpawn) };
578
+ return { ...options, sessionRoot, delegationOptions: { supervisionRoot: join(sessionRoot, "delivery-hooks"), pairedDeviceId: session?.device_id, providerLinkEpoch: link?.epoch ?? null, ...options.delegationOptions,
579
+ onWorkspaceAuthorized: async roots => {
580
+ await guard();
581
+ if (task.local_destination?.authorized_root) {
582
+ await resolveLocalTaskDestination({ local_destination: task.local_destination, operation: task.operation,
583
+ permission_mode: task.permission_mode, workspace_mode: task.workspace_mode, repository: task.repository }, {
584
+ deviceId: session?.device_id, resolveRepository: ensureRepositoryIndexed, nativeWritableRoots: roots,
585
+ });
586
+ }
587
+ task.native_writable_roots = [...roots];
588
+ await options.delegationOptions?.onWorkspaceAuthorized?.(roots);
589
+ await guard();
590
+ } },
591
+ beforeCompletion: options.beforeCompletion ? wrap(options.beforeCompletion) : null,
592
+ beforeProviderInvocation: wrap(options.beforeProviderInvocation), beforeSpawn: wrap(options.beforeSpawn) };
548
593
  };
549
594
  const codingRuntimeOptions = run => {
550
595
  const wrap = (custom, marker) => async context => {
@@ -856,7 +901,7 @@ export async function startAgent({
856
901
  notifyDesktopStatusChange();
857
902
  let deviceHeartbeatSucceeded = false;
858
903
  const requestedSession = session;
859
- const advertisedProviders = JSON.stringify(providerStatuses);
904
+ const advertisedProviders = JSON.stringify([providerStatuses, nativeIdentityCapability]);
860
905
  heartbeatInFlight = Promise.resolve()
861
906
  .then(async () => { if (enforceDesktopAccess) await desktopAccess.refresh(true); })
862
907
  .then(() =>
@@ -878,7 +923,7 @@ export async function startAgent({
878
923
  // browser authorization. Publish verified readiness after their probe.
879
924
  const publishVerifiedProviders = async () => {
880
925
  await readProviderStatuses();
881
- if (!shuttingDown && JSON.stringify(providerStatuses) !== advertisedProviders) {
926
+ if (!shuttingDown && JSON.stringify([providerStatuses, nativeIdentityCapability]) !== advertisedProviders) {
882
927
  await heartbeatDevice({ apiOrigin, apiBaseUrl: session.api_base_url, token: session.token,
883
928
  port, capabilities: refreshAvailableCapabilities(), repoInventory: repositoryInventory });
884
929
  }
@@ -1011,6 +1056,8 @@ export async function startAgent({
1011
1056
  createDurableLocalTaskWorker;
1012
1057
  durableLocalTaskWorker = createWorker({
1013
1058
  api: durableApi,
1059
+ enableBoundedSubtasks:
1060
+ availableCapabilities.features.bounded_local_task_subtasks === true,
1014
1061
  registry: localTaskRegistry,
1015
1062
  storageRoot: taskStorageRoot,
1016
1063
  workspaceRoot: dependencies.localTaskWorkspaceRoot,
@@ -1029,6 +1076,7 @@ export async function startAgent({
1029
1076
  return (dependencies.runLocalTask || startLocalTask)(task, providerRuntimeOptions(task, options, scope, link));
1030
1077
  },
1031
1078
  attachmentStore: localTaskAttachmentStore,
1079
+ readPhotoSource: dependencies.readLocalPhotoSource || readLocalPhotoSource,
1032
1080
  getDeviceId: () => session?.device_id || null,
1033
1081
  beginWork: () => acquireUpdateWork(updateManager),
1034
1082
  onError: (error) => reportDurableLocalTaskError(error, quiet),
@@ -1777,12 +1825,17 @@ export async function startAgent({
1777
1825
  });
1778
1826
  return;
1779
1827
  }
1780
- let requestFingerprint;
1828
+ let requestFingerprint, providerDelegation;
1781
1829
  try {
1830
+ providerDelegation = providerDelegationFromRequest(body, idempotencyKey);
1782
1831
  requestFingerprint = createLocalTaskRequestFingerprint({
1783
1832
  instruction,
1784
1833
  backend: body.backend,
1785
1834
  model: body.model,
1835
+ delegation: providerDelegation,
1836
+ localDestination: body.local_destination,
1837
+ localPhotoSource: body.local_photo_source,
1838
+ nativeIdentity: body.native_identity,
1786
1839
  operation: guardrail.operation,
1787
1840
  permissionMode: guardrail.permission_mode,
1788
1841
  workspaceMode,
@@ -1882,7 +1935,7 @@ export async function startAgent({
1882
1935
  workspace = {
1883
1936
  ...prepared,
1884
1937
  id,
1885
- workspace_path: indexedRepository.local_path,
1938
+ ...(await prepareRepositoryWorkspace({ repositoryPath: indexedRepository.local_path, expectedRepository: repository, localTaskId: id, prepareOutputs: body.permission_mode === "workspace_write" })),
1886
1939
  };
1887
1940
  } else {
1888
1941
  const id = createLocalTaskId();
@@ -1895,6 +1948,19 @@ export async function startAgent({
1895
1948
  })),
1896
1949
  };
1897
1950
  }
1951
+ if (body.local_destination?.authorized_root && providerDelegation?.provider !== "codex") {
1952
+ throw Object.assign(new Error("The selected folder requires an already authorized native Codex workspace."), { code: "local_task_destination_unconfirmed" });
1953
+ }
1954
+ let deliveryTask = null;
1955
+ const destinationSpec = { ...body, workspace_mode: workspaceMode, repository };
1956
+ const resolveDestination = () => resolveLocalTaskDestination(destinationSpec, {
1957
+ deviceId: session?.device_id,
1958
+ resolveRepository: ensureRepositoryIndexed,
1959
+ nativeWritableRoots: deliveryTask?.native_writable_roots,
1960
+ });
1961
+ let destination = body.local_destination && !body.local_destination.authorized_root ? await resolveDestination() : null;
1962
+ const photoContext = body.local_photo_source ? await (dependencies.readLocalPhotoSource || readLocalPhotoSource)(body.local_photo_source, { deviceId: session?.device_id }) : null;
1963
+ await authorizeDesktopWork(requestAccessScope);
1898
1964
  const stagedAttachments = await stagePreparedRunAttachments({
1899
1965
  runId: workspace.id,
1900
1966
  prepared: preparedAttachments,
@@ -1905,6 +1971,10 @@ export async function startAgent({
1905
1971
  instruction,
1906
1972
  backend: body.backend,
1907
1973
  model: body.model,
1974
+ delegation: providerDelegation,
1975
+ localDestination: body.local_destination,
1976
+ localPhotoSource: body.local_photo_source,
1977
+ nativeIdentity: body.native_identity,
1908
1978
  operation: guardrail.operation,
1909
1979
  permissionMode: guardrail.permission_mode,
1910
1980
  workspaceMode,
@@ -1915,6 +1985,7 @@ export async function startAgent({
1915
1985
  outputsRoot: workspace.outputs_root,
1916
1986
  outputsRootExpected: workspace.outputs_root_expected,
1917
1987
  attachments: stagedAttachments,
1988
+ localPhotoContext: photoContext,
1918
1989
  expectedOutputs: body.expected_outputs,
1919
1990
  scenarioPack,
1920
1991
  providerTokenBudget: body.provider_token_budget,
@@ -1927,6 +1998,7 @@ export async function startAgent({
1927
1998
  workflowStartIntent,
1928
1999
  purposeVerification,
1929
2000
  });
2001
+ deliveryTask = task;
1930
2002
  if (purposeVerification) await preparePurposeVerification(task, { session, apiOrigin, clientRequestId: idempotencyKey, keyPath: purposeKeyPath,
1931
2003
  authorize: async () => {
1932
2004
  await authorizeDesktopWork(requestAccessScope);
@@ -1937,7 +2009,27 @@ export async function startAgent({
1937
2009
  requestClaim.complete(task);
1938
2010
  requestClaimCompleted = true;
1939
2011
  const runLocalTask = dependencies.runLocalTask || startLocalTask;
1940
- const runtimeOptions = providerRuntimeOptions(task, { session, apiOrigin, beforeSpawn: async localTask => {
2012
+ const runtimeOptions = providerRuntimeOptions(task, { session, apiOrigin,
2013
+ beforeCompletion: body.local_destination ? async completed => {
2014
+ await authorizeDesktopWork(requestAccessScope);
2015
+ const current = await resolveDestination();
2016
+ if (destination && (current.root !== destination.root || current.directory !== destination.directory)) {
2017
+ throw Object.assign(new Error("The authorized save folder changed."), { code: "local_task_destination_unconfirmed" });
2018
+ }
2019
+ destination ||= current;
2020
+ completed.local_destination_receipts = [];
2021
+ await saveLocalTaskDestination({ task: completed, destination: current,
2022
+ assertActive: async () => {
2023
+ await authorizeDesktopWork(requestAccessScope);
2024
+ if (completed.stop_requested) throw Object.assign(new Error("Local delivery was stopped."), { code: "local_task_destination_unconfirmed" });
2025
+ },
2026
+ onReceipt: async receipt => {
2027
+ completed.local_destination_receipts.push(receipt);
2028
+ await persistLocalTask(completed);
2029
+ },
2030
+ });
2031
+ } : null,
2032
+ beforeSpawn: async localTask => {
1941
2033
  if (purposeWorkspaceBusy(localTask.workspace_path)) throw Object.assign(new Error("Independent verification owns this workspace."), { code: "purpose_verification_workspace_busy" });
1942
2034
  if (purposeVerification) {
1943
2035
  await authorizeDesktopWork(requestAccessScope);
@@ -1999,6 +2091,10 @@ export async function startAgent({
1999
2091
  return;
2000
2092
  }
2001
2093
  const body = await readJson(req);
2094
+ if (body.delegation || body.delegation_target) {
2095
+ json(res, 409, { error: 'Provider delegation requires the existing Local Task admission.', code: 'delegation_local_task_required' });
2096
+ return;
2097
+ }
2002
2098
  const instruction = String(body.instruction || "").trim();
2003
2099
  if (!instruction) {
2004
2100
  json(res, 400, { error: "instruction is required" });
@@ -2256,6 +2352,7 @@ export async function startAgent({
2256
2352
  ? body.expected_evidence
2257
2353
  : [],
2258
2354
  required_checks: Array.isArray(body.required_checks) ? body.required_checks : [],
2355
+ independent_review_required: body.independent_review_required === true,
2259
2356
  require_human_approval: body.require_human_approval !== false,
2260
2357
  design_summary:
2261
2358
  typeof body.design_summary === "string" ? body.design_summary : "",
@@ -2704,7 +2801,7 @@ export async function startAgent({
2704
2801
  if (codingRunMonitorTimer) clearInterval(codingRunMonitorTimer);
2705
2802
  updateManager?.stop?.();
2706
2803
  await Promise.all([stopDesktopTasks(), codexWorkPatternSync?.close?.(),
2707
- codexWorkPatternSyncInFlight, codingRunMonitorInFlight]);
2804
+ codexWorkPatternSyncInFlight, codingRunMonitorInFlight, nativeIdentityReadInFlight?.catch(() => {})]);
2708
2805
  try {
2709
2806
  await (dependencies.removeConnectionDiscovery || removeConnectionDiscovery)(
2710
2807
  connectionDiscovery,
@@ -2829,6 +2926,29 @@ export async function startAgent({
2829
2926
  sqmLocalReviewWorker?.retry?.(); notifyDesktopStatusChange();
2830
2927
  },
2831
2928
  async retrySqmEnvironment() { await authorizeDesktopWork(); sqmLocalReviewWorker?.retry?.(); notifyDesktopStatusChange(); },
2929
+ getTeamThreadsScope() {
2930
+ return { key: desktopScopeRevision(),
2931
+ fenceKey: createHash('sha256').update(JSON.stringify([apiOrigin, session?.account_id ?? null, session?.organization_id ?? null, session?.device_id ?? null])).digest('hex'),
2932
+ organizationId: session?.organization_id ?? null,
2933
+ authenticated: !shuttingDown && !pendingOrganization && isSessionValid(session, Date.now(), apiOrigin) && Boolean(session?.device_id),
2934
+ allowed: !shuttingDown && desktopWorkAllowed() && desktopAccessReady(desktopAccess.snapshot()) };
2935
+ },
2936
+ async requestTeamThreads(body, signal) {
2937
+ const selected = session;
2938
+ const scope = desktopScopeRevision();
2939
+ const off = body?.operation === 'consent' && body.enabled === false;
2940
+ if (!off) await authorizeDesktopWork(scope);
2941
+ if (shuttingDown || pendingOrganization || !isSessionValid(selected, Date.now(), apiOrigin) || selected !== session) throw new Error('Team sign-in required');
2942
+ const response = await (dependencies.fetch || globalThis.fetch)(webApiUrl('team-threads', { webOrigin: apiOrigin, apiBaseUrl: selected.api_base_url }), {
2943
+ method: body ? 'POST' : 'GET', headers: webApiHeaders({ token: selected.token }, body ? { 'Content-Type': 'application/json' } : {}),
2944
+ ...(body ? { body: JSON.stringify(body) } : {}), signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]), cache: 'no-store', redirect: 'error',
2945
+ });
2946
+ if (scope !== desktopScopeRevision() || selected !== session || signal.aborted) throw new Error('Team sign-in changed');
2947
+ if (!response.ok) throw Object.assign(new Error('Shared conversations unavailable'), { status: response.status });
2948
+ const raw = await response.text();
2949
+ if (raw.length > 1_048_576) throw new Error('Team response too large');
2950
+ return JSON.parse(raw);
2951
+ },
2832
2952
  async createDesktopWebSession() {
2833
2953
  const scope = await authorizeDesktopWork();
2834
2954
  const selected = session;
@@ -3297,6 +3417,7 @@ function capabilitiesFromInspections(codex, claude) {
3297
3417
  },
3298
3418
  },
3299
3419
  features: {
3420
+ team_threads: true,
3300
3421
  repository_scan_approval: true,
3301
3422
  executor_version_preflight: true,
3302
3423
  desktop_executor: true,
@@ -3307,6 +3428,7 @@ function capabilitiesFromInspections(codex, claude) {
3307
3428
  local_task_streaming: true,
3308
3429
  local_task_idempotency: true,
3309
3430
  local_task_model: true,
3431
+ native_delegation: true,
3310
3432
  progress_continuation_policy: true,
3311
3433
  local_task_reconciliation: true,
3312
3434
  local_task_control_plane: true,
@@ -26,7 +26,7 @@ export async function resolveBackendExecutable(backend, options = {}) {
26
26
 
27
27
  export async function inspectBackendExecutable(
28
28
  backend,
29
- { spawnImpl = spawn, candidates, timeoutMs = 5_000, signal, discoveryOptions = {}, minimumVersion } = {},
29
+ { spawnImpl = spawn, candidates, timeoutMs = 5_000, signal, discoveryOptions = {}, minimumVersion, executionEnvironment } = {},
30
30
  ) {
31
31
  const assertActive = () => { if (signal?.aborted) throw Object.assign(new Error('Backend inspection was cancelled.'), { code: 'cancelled' }); };
32
32
  assertActive();
@@ -41,7 +41,7 @@ export async function inspectBackendExecutable(
41
41
  // still fall back to automatic discovery, as before.
42
42
  if (explicitCodex && candidates.includes(explicitCodex)) {
43
43
  const pinned = await inspectBackendExecutable(backend, {
44
- candidates: [explicitCodex], spawnImpl, timeoutMs, signal, minimumVersion,
44
+ candidates: [explicitCodex], spawnImpl, timeoutMs, signal, minimumVersion, executionEnvironment,
45
45
  discoveryOptions: { ...discoveryOptions, environment: {} },
46
46
  });
47
47
  if (pinned.compatible) return pinned;
@@ -51,11 +51,11 @@ export async function inspectBackendExecutable(
51
51
  // Comparing all Codex installs must not turn several slow wrappers into a
52
52
  // serial startup delay. Keep concurrency bounded and results in source order.
53
53
  const probes = backend === "codex"
54
- ? await probeCandidates(candidates, { spawnImpl, timeoutMs, signal }, assertActive) : null;
54
+ ? await probeCandidates(candidates, { spawnImpl, timeoutMs, signal, executionEnvironment }, assertActive) : null;
55
55
  let selected = null;
56
56
  for (const [index, candidate] of candidates.entries()) {
57
57
  assertActive();
58
- const probe = probes ? probes[index] : await probeExecutable(candidate, { spawnImpl, timeoutMs, signal });
58
+ const probe = probes ? probes[index] : await probeExecutable(candidate, { spawnImpl, timeoutMs, signal, executionEnvironment });
59
59
  assertActive();
60
60
  if (!probe.available) continue;
61
61
  const installedVersion = parseBackendVersion(probe.output);
@@ -111,6 +111,7 @@ export function backendCandidates(backend, {
111
111
  environment.SIMY_CODEX_BIN,
112
112
  "codex",
113
113
  ...codexPathCandidates(platform, environment),
114
+ ...(platform !== "win32" ? [path.posix.join(home, ".local", "bin", "codex")] : []),
114
115
  ...(platform === "darwin" ? ["/opt/homebrew/bin/codex", "/usr/local/bin/codex"] : []),
115
116
  ...(platform === "darwin" ? ["/Applications", path.posix.join(home, "Applications")].flatMap(directory =>
116
117
  ["ChatGPT.app", "Codex.app"].flatMap(app => {
@@ -228,13 +229,13 @@ function numericVersionParts(value) {
228
229
  return match.slice(1).map(Number);
229
230
  }
230
231
 
231
- function probeExecutable(command, { spawnImpl, timeoutMs, signal }) {
232
+ function probeExecutable(command, { spawnImpl, timeoutMs, signal, executionEnvironment }) {
232
233
  return new Promise((resolve) => {
233
234
  let settled = false, terminating = false;
234
235
  let timeout, force, limit;
235
236
  let output = Buffer.alloc(0);
236
237
  let child;
237
- try { child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "pipe"] }); }
238
+ try { child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "pipe"], ...(executionEnvironment ? { env: { ...process.env, ...executionEnvironment } } : {}) }); }
238
239
  catch { resolve({ available: false, output: "" }); return; }
239
240
  const capture = chunk => {
240
241
  // Version probes need a tiny prefix. Drain noisy executables without
@@ -58,6 +58,7 @@ export function createBoundedLocalTaskSubtaskPool({
58
58
  prepareWorkspace = prepareLocalTaskWorkspace,
59
59
  leaseSeconds = DEFAULT_LEASE_SECONDS,
60
60
  heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
61
+ now = Date.now,
61
62
  setIntervalImpl = setInterval,
62
63
  clearIntervalImpl = clearInterval,
63
64
  onError = () => {},
@@ -111,10 +112,12 @@ export function createBoundedLocalTaskSubtaskPool({
111
112
  let completionConfirmed = false;
112
113
  let completionState = null;
113
114
  let providerStarted = false;
115
+ let knownLeaseExpiry = Number.NaN;
114
116
  const startedAt = Date.now();
115
117
  try {
116
118
  normalized = normalizeDurableLocalTaskSubtaskClaim(claim, {
117
119
  parentAuthority: parent,
120
+ nowMs: now(),
118
121
  leaseSeconds,
119
122
  });
120
123
  if (normalized.disposition !== "execute") {
@@ -129,6 +132,12 @@ export function createBoundedLocalTaskSubtaskPool({
129
132
  }
130
133
  batchId ??= normalized.batch.batch_id;
131
134
 
135
+ knownLeaseExpiry = normalized.attempt.leaseExpiresAtMs;
136
+ const checkLease = async () => {
137
+ if (!controller.signal.aborted && !completionConfirmed && now() >= knownLeaseExpiry) await cancel("lease_lost");
138
+ return !controller.signal.aborted;
139
+ };
140
+
132
141
  const execution = boundedLocalTaskSubtaskExecution(normalized, parent);
133
142
  const runtime = await materializeSubtaskRuntime({
134
143
  normalized,
@@ -140,7 +149,7 @@ export function createBoundedLocalTaskSubtaskPool({
140
149
  downloadSource: sourceDownloader,
141
150
  signal: controller.signal,
142
151
  });
143
- if (controller.signal.aborted) return;
152
+ if (!await checkLease()) return;
144
153
 
145
154
  const fence = durableLocalTaskSubtaskFence(normalized, parent);
146
155
  let boundaryConfirmed = false;
@@ -161,7 +170,7 @@ export function createBoundedLocalTaskSubtaskPool({
161
170
  }).catch(onError);
162
171
  return;
163
172
  }
164
- if (boundaryConfirmed !== true || controller.signal.aborted) {
173
+ if (boundaryConfirmed !== true || !await checkLease()) {
165
174
  await api.abandon_local_task_subtask_attempt_v2({
166
175
  ...fence,
167
176
  p_reason_code: controller.signal.aborted
@@ -174,6 +183,8 @@ export function createBoundedLocalTaskSubtaskPool({
174
183
 
175
184
  const heartbeat = async () => {
176
185
  if (controller.signal.aborted || completionConfirmed) return;
186
+ const renewalStartedAt = now();
187
+ if (!await checkLease()) return;
177
188
  try {
178
189
  const response =
179
190
  await api.heartbeat_local_task_subtask_attempt_v2({
@@ -190,6 +201,7 @@ export function createBoundedLocalTaskSubtaskPool({
190
201
  ) {
191
202
  return;
192
203
  }
204
+ if (!await checkLease()) return;
193
205
  if (
194
206
  response?.ok !== true ||
195
207
  response.cancel_requested === true ||
@@ -198,6 +210,8 @@ export function createBoundedLocalTaskSubtaskPool({
198
210
  normalized.parent.cancel_generation)
199
211
  ) {
200
212
  await cancel("server_cancelled");
213
+ } else {
214
+ knownLeaseExpiry = renewalStartedAt + leaseSeconds * 1_000;
201
215
  }
202
216
  } catch (error) {
203
217
  if (completionConfirmed && isKnownSubtaskFenceError(error)) return;
@@ -214,9 +228,11 @@ export function createBoundedLocalTaskSubtaskPool({
214
228
  }
215
229
  };
216
230
  heartbeatTimer = setIntervalImpl(() => {
217
- if (heartbeatInFlight) return;
218
- heartbeatInFlight = heartbeat().finally(() => {
219
- heartbeatInFlight = null;
231
+ void checkLease().then((valid) => {
232
+ if (!valid || heartbeatInFlight || completionConfirmed) return;
233
+ heartbeatInFlight = heartbeat().finally(() => {
234
+ heartbeatInFlight = null;
235
+ });
220
236
  });
221
237
  }, heartbeatIntervalMs);
222
238
 
@@ -245,6 +261,13 @@ export function createBoundedLocalTaskSubtaskPool({
245
261
  ),
246
262
  );
247
263
  }
264
+ // Lease loss after the paid boundary cannot establish a cancelled result.
265
+ // Preserve a known result, but leave interrupted work for server resolution.
266
+ if (providerStarted && controller.signal.reason === "lease_lost" &&
267
+ rawResult?.status === "cancelled") {
268
+ rawResult = { ...rawResult, status: "unknown",
269
+ failure_code: "local_task_subtask_provider_result_unknown" };
270
+ }
248
271
  const result = normalizeDurableLocalTaskSubtaskResult(
249
272
  normalizeRunResult(rawResult, startedAt),
250
273
  { normalizedClaim: normalized },
@@ -305,12 +328,15 @@ export function createBoundedLocalTaskSubtaskPool({
305
328
  }
306
329
  } finally {
307
330
  if (heartbeatTimer) clearIntervalImpl(heartbeatTimer);
308
- if (heartbeatInFlight) await heartbeatInFlight;
331
+ // Pending renewal has its own fenced completion handler; it must not
332
+ // hold a finished/cancelled slot open after its timer is cleared.
309
333
  }
310
334
  };
311
335
 
312
336
  const scheduler = async () => {
313
337
  if (!initialized) {
338
+ // Initialization only acknowledges the canonical plan. Backend keeps
339
+ // existing attempts/reservations intact; claim reconciles old generations.
314
340
  const initializedBatch =
315
341
  await api.initialize_local_task_subtask_batch_v2({
316
342
  p_turn_id: parent.turnId,
@@ -1043,7 +1043,7 @@ function uploadedAttachmentRefs(value, name) {
1043
1043
  return refs;
1044
1044
  }
1045
1045
 
1046
- function normalizeParentUploadedRef(item, name) {
1046
+ export function normalizeParentUploadedRef(item, name = "uploaded source") {
1047
1047
  record(item, name);
1048
1048
  const allowed = new Set([
1049
1049
  "schema_version",
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.7.15";
16
+ export const CLI_VERSION = "0.7.17";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function narrativeImageCapabilityMetadata() {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "simy",
3
3
  "version": "0.1.0",
4
- "description": "SIMY quality and PR review. Delivery uses the explicitly requested CLI Agentic Loop.",
4
+ "description": "SIMY quality and PR review, with automatic checkpoint tracking through enabled provider hooks.",
5
5
  "author": {
6
6
  "name": "Specialist Doctors Inc.",
7
7
  "url": "https://github.com/SpecialistDoctors-Inc"
@@ -17,7 +17,7 @@
17
17
  "interface": {
18
18
  "displayName": "SIMY",
19
19
  "shortDescription": "Quality and PR review.",
20
- "longDescription": "Run explicitly requested quality checks and PR reviews. Work directly in Codex by default. When the user explicitly requests SIMY Delivery Loop, use the CLI Agentic Loop; the local hook-based Delivery skill is retired.",
20
+ "longDescription": "Run explicitly requested quality checks and PR reviews. Enabled hooks automatically track eligible conversations from user prompts through checkpoints. Stops, permission waits and opt-outs are preserved. The local Delivery skill remains retired; independently managed work uses the CLI Agentic Loop.",
21
21
  "developerName": "Specialist Doctors Inc.",
22
22
  "category": "Developer Tools",
23
23
  "capabilities": [
@@ -18,9 +18,14 @@ scans ports or reads stored session tokens.
18
18
 
19
19
  ## Delivery Loop hooks
20
20
 
21
- `delivery-loop.mjs` is an independent opt-in workflow, shared by the Codex and
22
- Claude distributions. SessionStart supplies the exact session identity. It does
23
- not start a loop. After an explicit `simy delivery start`, Stop checks that
21
+ `delivery-loop.mjs` is shared by the Codex and Claude distributions. The CLI
22
+ prepares missing plugin sources automatically and maintains enabled plugin caches.
23
+ SessionStart supplies identity without starting work. A nonempty UserPromptSubmit
24
+ starts standard Delivery Loop by default for eligible connected conversations,
25
+ including when SessionStart was missed. No per-project enable command is needed.
26
+ `simy delivery auto off --provider codex|claude` saves a project override; updates
27
+ preserve it. Existing loops, explicit stops, session opt-outs and revoked provider
28
+ consent remain authoritative. No hook grants provider trust. Stop checks that
24
29
  session's checkpoint and returns bounded next-step instructions. Approval/input
25
30
  blocks, interruption, completion, failures and missing progress stop automatic continuation.
26
31
  Provider errors/exits are surfaced to SIMY's desktop; a dead provider is not