@awak-app/simy-cli 0.6.17 → 0.6.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,18 @@ and an already consumed remote result prevent automatic recovery. An unfinished
86
86
  worker or a missing, invalid or redacted structured result still requires
87
87
  reconciliation; no unknown external operation is automatically replayed.
88
88
 
89
+ After CI finishes successfully, the authenticated daemon refreshes the independent
90
+ review of the same saved candidate without dispatching its implementation worker.
91
+ The reviewed HEAD, fetched base, objective, preparation evidence and minimum
92
+ completion matrix must still match; changed candidates require explicit work.
93
+ Pending/failing checks, a dirty checkout and missing required checks do not trigger
94
+ this review. Human peer approval remains required when the charter requires it.
95
+ The refreshed review is checked against newly collected evidence before readiness
96
+ can pass. Stop cancels review continuation. A failed or interrupted refresh is not
97
+ repeated on every monitor tick; explicitly continue the same run to recover.
98
+ Older reviews without a saved CI basis can refresh only when their prior evidence
99
+ still establishes the complete reviewed identity; otherwise explicitly continue.
100
+
89
101
  For interrupted Agentic work, `GET /v1/agentic-loop/:id/recovery` provides a
90
102
  read-only objective and checkout fingerprint. Reconcile an uncertain side effect
91
103
  with `POST` to that endpoint (`action: reconcile`, authoritative
@@ -821,6 +833,11 @@ not a GitHub hook that mechanically intercepts every external close command.
821
833
 
822
834
  ### Submit repaired local delivery evidence to SQM
823
835
 
836
+ The [integrated delivery state model](src/orchestrator/delivery/references/state-model.md)
837
+ connects delivery milestones, repair/wait/resume branches and actual controller
838
+ states. Both executor and auditor receive its path. It documents existing control
839
+ and evidence duties; it adds no scheduler or transition-enforcement mechanism.
840
+
824
841
  The bundled Delivery Loop maintains an external acceptance checklist with separate
825
842
  user-journey and technical-invariant results. It refreshes the table after each
826
843
  evaluated action and marks evidence pending when the code or runtime context
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.6.17",
3
+ "version": "0.6.19",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -496,9 +496,8 @@ export async function startAgent({
496
496
  return inspection;
497
497
  };
498
498
 
499
- const requireRunProvider = async (run) => {
499
+ const requireRunProvider = async (run, { backend = run.request.backend } = {}) => {
500
500
  const scope = await authorizeDesktopWork();
501
- const backend = run.request.backend;
502
501
  const inspection = await inspectReadyBackend(backend, { force: true });
503
502
  await authorizeDesktopWork(scope);
504
503
  await requireLinkedProvider(backend, inspectedLinks.get(inspection));
@@ -507,17 +506,17 @@ export async function startAgent({
507
506
  code: `desktop_executor_${inspection.status}`,
508
507
  });
509
508
  }
510
- await bindRunAuthorization(run, scope, inspection);
509
+ await bindRunAuthorization(run, scope, inspection, backend);
511
510
  };
512
511
 
513
512
  const authorizeProviderDispatch = async (backend, scope, link) => {
514
513
  await authorizeDesktopWork(scope);
515
514
  await requireLinkedProvider(backend, link);
516
515
  };
517
- const bindRunAuthorization = async (run, scope, inspection) => {
516
+ const bindRunAuthorization = async (run, scope, inspection, backend = run.request.backend) => {
518
517
  const links = await readProviderLinks(sessionRoot);
519
- links[run.request.backend] = inspectedLinks.get(inspection);
520
- await authorizeProviderDispatch(run.request.backend, scope, links[run.request.backend]);
518
+ links[backend] = inspectedLinks.get(inspection);
519
+ await authorizeProviderDispatch(backend, scope, links[backend]);
521
520
  runAuthorizations.set(run, { scope, links });
522
521
  };
523
522
  const providerRuntimeOptions = (task, options = {}, scope, link) => {
@@ -550,6 +549,22 @@ export async function startAgent({
550
549
  executeIndependentAudit: wrap(runOptions.executeIndependentAudit, "SIMY_AUDIT_JSON:") };
551
550
  };
552
551
 
552
+ const recheckAuthorizedCodingRun = (run, options = {}) => {
553
+ const scope = desktopScopeRevision();
554
+ return recheckLocalCodingRun(run, { collectEvidence: runOptions.collectEvidence, ...options,
555
+ executeIndependentAudit: context => withUpdateWork(updateManager,
556
+ () => codingRuntimeOptions(run).executeIndependentAudit(context)),
557
+ authorizeReview: async () => {
558
+ await authorizeDesktopWork(scope);
559
+ if (!isSessionValid(session, Date.now(), apiOrigin) || !sameRunScope(run.session, session)) {
560
+ throw new Error("The original authorized coding run session changed.");
561
+ }
562
+ await requireRunProvider(run, { backend: run.snapshot.charter.audit_backend });
563
+ await authorizeDesktopWork(scope);
564
+ },
565
+ });
566
+ };
567
+
553
568
  let organizationCancellation = Promise.resolve();
554
569
  const suspendOrganizationWork = () => {
555
570
  durableLocalTaskWorker?.pause?.();
@@ -796,7 +811,7 @@ export async function startAgent({
796
811
  for (const run of registry.list()) {
797
812
  if (!["pr_ready_for_review", "merge_ready"].includes(run.status) || run.operation || run.child) continue;
798
813
  try {
799
- await (dependencies.recheckCodingRun || recheckLocalCodingRun)(run);
814
+ await (dependencies.recheckCodingRun || recheckAuthorizedCodingRun)(run);
800
815
  } catch (error) {
801
816
  await markCodingRunMonitorFailure(run, error);
802
817
  }
@@ -2538,7 +2553,7 @@ export async function startAgent({
2538
2553
  json(res, 409, { error: "run is not ready for PR evidence recheck" });
2539
2554
  return;
2540
2555
  }
2541
- const snapshot = await recheckLocalCodingRun(run);
2556
+ const snapshot = await recheckAuthorizedCodingRun(run);
2542
2557
  json(res, 200, { ok: true, run_id: run.id, state: snapshot.state });
2543
2558
  return;
2544
2559
  }
@@ -2792,6 +2807,12 @@ export async function startAgent({
2792
2807
  !Number.isFinite(ticket.expiresAt) || ticket.expiresAt <= Date.now() || ticket.expiresAt > Date.now() + 60_000) throw new Error("Desktop sign-in changed");
2793
2808
  return { code: ticket.code, expiresAt: ticket.expiresAt, userId: ticket.userId, orgSlug: ticket.orgSlug };
2794
2809
  },
2810
+ // This is an in-process capability for the native host. It is deliberately
2811
+ // absent from status frames and never written to disk by the host.
2812
+ getLocalCliApiSession() {
2813
+ if (!isSessionValid(session, Date.now(), apiOrigin) || pendingOrganization) return null;
2814
+ return { token: session.token, api_base_url: session.api_base_url };
2815
+ },
2795
2816
  getDesktopStatus,
2796
2817
  stopDesktopTask,
2797
2818
  stopDesktopTasks,
@@ -2912,7 +2933,7 @@ export async function startAgent({
2912
2933
  recheck: (run) =>
2913
2934
  withUpdateWork(updateManager, async () => {
2914
2935
  await authorizeDesktopWork();
2915
- return recheckLocalCodingRun(run, { collectEvidence: runOptions.collectEvidence });
2936
+ return recheckAuthorizedCodingRun(run, { collectEvidence: runOptions.collectEvidence });
2916
2937
  }),
2917
2938
  },
2918
2939
  };
@@ -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.6.17";
16
+ export const CLI_VERSION = "0.6.19";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -8,6 +8,7 @@ import { execFile } from 'node:child_process';
8
8
  import { promisify } from 'node:util';
9
9
  import { acquireWorkspaceLease, releaseWorkspaceLease } from './workspace-lease.mjs';
10
10
  import { associationContract, validateAssociationResult } from './action-association.mjs';
11
+ import { minimalSufficientChangeInstruction } from './minimal-sufficient.mjs';
11
12
  import { continuationMode } from './continuation-policy.mjs';
12
13
  import { evaluateOutcomeProgress, restartOutcomeProgressWindow } from './outcome-progress.mjs';
13
14
 
@@ -318,10 +319,10 @@ export function evaluateAssessedPlan(plan, policy, scopes, now = Date.now()) {
318
319
  // Shared by live Stop hooks and cold exact-session recovery. Keep the result
319
320
  // contract identical across transports; the host still verifies acceptance.
320
321
  export function assessedCheckpointContract(decision) {
321
- return `Return ONLY one JSON object with top-level status:"in_progress|completed|blocked|error" and summary:"what happened". Use the key status, NEVER kind, for an ordinary work checkpoint. Record actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"what actually happened",evidence:["concrete observation"]} for THIS selected action. This is provider-reported, not independent verification. For in_progress/error include nextStep, remainingWork, and plan:{diagnosis,alternatives:[{id,action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification,rollback}],selectedOptionId,recommendation:{optionId,reason},userQuestion?} for the NEXT action; select one assessed option but do not execute it in this turn. Valid action.kind values are exactly read, local_edit, local_test, external, destructive, scope_change, unknown; use local_edit for writing local files. Risk is low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown. Missing facts are unknown. Completed requires evidence, and SIMY independently checks frozen acceptance. Blocked requires blocker set to exactly authorization, missing_information, or external_dependency, plus nextStep. For authorization, include an assessed plan and set approval to an exact copy of the selected plan alternative action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. missing_information requires a specific question. external_dependency requires waitCondition:{condition,owner,nextCheck} with the observable resumption condition, dependency owner, and bounded recheck instruction, plus nextStep.`;
322
+ return `${minimalSufficientChangeInstruction({ turn: 'assessed' })}\nReturn ONLY one JSON object with top-level status:"in_progress|completed|blocked|error" and summary:"what happened". Use the key status, NEVER kind, for an ordinary work checkpoint. Record actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"what actually happened",evidence:["concrete observation"]} for THIS selected action. This is provider-reported, not independent verification. For in_progress/error include nextStep, remainingWork, and plan:{diagnosis,alternatives:[{id,action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification,rollback}],selectedOptionId,recommendation:{optionId,reason},userQuestion?} for the NEXT action; select one assessed option but do not execute it in this turn. Valid action.kind values are exactly read, local_edit, local_test, external, destructive, scope_change, unknown; use local_edit for writing local files. Risk is low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown. Missing facts are unknown. Completed requires evidence, and SIMY independently checks frozen acceptance. Blocked requires blocker set to exactly authorization, missing_information, or external_dependency, plus nextStep. For authorization, include an assessed plan and set approval to an exact copy of the selected plan alternative action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. missing_information requires a specific question. external_dependency requires waitCondition:{condition,owner,nextCheck} with the observable resumption condition, dependency owner, and bounded recheck instruction, plus nextStep.`;
322
323
  }
323
324
  export function understandingCheckpointContract(decision) {
324
- return `This is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; do not edit files, call external services, or start the proposed next action. Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require review. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. For semantic conditions use manual. If the complete expected file contents are not yet known from the original user request or observed evidence in this understanding turn, use manual for that criterion and choose a read as the next action to learn them. Never emit file_equals with null, omitted, guessed, or placeholder text; an exact file check requires a bounded concrete UTF-8 string now. Do not perform the proposed next action merely to fill the check, and do not claim that a later read or model report has independently verified a manual criterion. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. If material context is missing, set exactly one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
325
+ return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\nThis is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; do not edit files, call external services, or start the proposed next action. Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require review. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. For semantic conditions use manual. If the complete expected file contents are not yet known from the original user request or observed evidence in this understanding turn, use manual for that criterion and choose a read as the next action to learn them. Never emit file_equals with null, omitted, guessed, or placeholder text; an exact file check requires a bounded concrete UTF-8 string now. Do not perform the proposed next action merely to fill the check, and do not claim that a later read or model report has independently verified a manual criterion. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. If material context is missing, set exactly one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
325
326
  }
326
327
  function boundedContextList(value, maxItems = 8, maxLength = 300) {
327
328
  if (!Array.isArray(value) || value.length > maxItems) throw new Error('Session understanding has too many items.');
@@ -1030,7 +1031,7 @@ export function decideStop(run, now = Date.now()) {
1030
1031
  }
1031
1032
  next.reason = fresh ? 'continue_pending_work' : 'checkpoint_missing';
1032
1033
  const selected = next.managed?.autonomyPolicy ? next.managed.decision : null;
1033
- const instruction = selected && next.managed.phase === 'action_association' ? `${associationContract(next.managed.actionAssociation.job)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping.` : selected && next.managed.phase === 'understanding' ? `Understand the current user request in this ORIGINAL conversation. ${next.managed.understandingAnswer ? `The user answered question ${next.managed.understandingAnswer.questionId}: ${next.managed.understandingAnswer.text}. ` : ''}${understandingCheckpointContract(selected)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable. This updates SIMY metadata only; do not create a checkpoint file in the task workspace.` : selected ? `${initialHook ? `Managed objective: ${next.objective}. Frozen acceptance criteria: ${JSON.stringify(next.managed.acceptanceCriteria)}. ` : ''}${replanHook ? `Replan context: ${next.managed.plan.diagnosis}. ` : ''}Execute only selected ${selected.action.kind} action in scope ${selected.action.scope}: ${selected.action.summary}. Expected verification: ${selected.expectedVerification}. Rollback: ${selected.rollback || 'none needed'}. ${assessedCheckpointContract(selected)} Submit that JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable; do not create a task-workspace checkpoint file.` : fresh ? next.nextStep : `Check the actual delivery status first. Record an honest checkpoint with simy delivery checkpoint before stopping; do not claim completion without verification. Pending step: ${next.nextStep}`;
1034
+ const instruction = selected && next.managed.phase === 'action_association' ? `${associationContract(next.managed.actionAssociation.job)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping.` : selected && next.managed.phase === 'understanding' ? `Understand the current user request in this ORIGINAL conversation. ${next.managed.understandingAnswer ? `The user answered question ${next.managed.understandingAnswer.questionId}: ${next.managed.understandingAnswer.text}. ` : ''}${understandingCheckpointContract(selected)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable. This updates SIMY metadata only; do not create a checkpoint file in the task workspace.` : selected ? `${initialHook ? `Managed objective: ${next.objective}. Frozen acceptance criteria: ${JSON.stringify(next.managed.acceptanceCriteria)}. ` : ''}${replanHook ? `Replan context: ${next.managed.plan.diagnosis}. ` : ''}Execute only selected ${selected.action.kind} action in scope ${selected.action.scope}: ${selected.action.summary}. Expected verification: ${selected.expectedVerification}. Rollback: ${selected.rollback || 'none needed'}. ${assessedCheckpointContract(selected)} Submit that JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable; do not create a task-workspace checkpoint file.` : fresh ? `${minimalSufficientChangeInstruction()}\n${next.nextStep}` : `${minimalSufficientChangeInstruction()}\nCheck the actual delivery status first. Record an honest checkpoint with simy delivery checkpoint before stopping; do not claim completion without verification. Pending step: ${next.nextStep}`;
1034
1035
  return { run: next, output: { decision: 'block', reason: `SIMY Delivery Loop (${next.continuations} continuations). ${next.executionCwd ? `Use task execution directory ${JSON.stringify(next.executionCwd)} for task work and acceptance. ` : ''}${deliveryAcceptanceContext(next)} ${instruction}\nStay within the original request and permissions. If authorization or information is missing, record a blocked checkpoint and stop. Resume this same task; do not start a replacement loop.` } };
1035
1036
  }
1036
1037
  // A process signature protects against PID reuse. Never treat a stale timer alone
@@ -0,0 +1,15 @@
1
+ export function minimalSufficientChangeInstruction({ auditor = false, turn = 'implementation' } = {}) {
2
+ return [
3
+ turn === 'understanding'
4
+ ? "For planning only, propose the smallest change sufficient for the original purpose and acceptance criteria. Explain which criterion or observed failure each proposed behavior, abstraction, configuration, compatibility branch, document or test would serve."
5
+ : "Choose the smallest change sufficient for the original purpose and acceptance criteria. Explain which criterion or observed failure each substantive behavior, abstraction, configuration, compatibility branch, document or test serves.",
6
+ "Prefer existing conventions and a simpler equivalent. Avoid speculative features, generalization, duplicate implementations and repetitive prose or tests. Do not optimize line counts or remove required behavior, authorization, data integrity, concurrency, failure recovery, regression coverage or evidence to make a change shorter.",
7
+ auditor
8
+ ? "Independently inspect the final diff for unnecessary scope and complexity. Report concrete findings with file/line, harm or maintenance burden, a smaller alternative, preserved criteria and required verification; do not invent findings when the change is already sufficient."
9
+ : turn === 'understanding'
10
+ ? "Use read-only evidence to plan; do not inspect the final diff as a completion step or perform cleanup in this understanding turn. Record any cleanup proposal for a next assessed action, without executing it."
11
+ : turn === 'assessed'
12
+ ? "Execute only the selected decision.action. Cleanup is allowed only when explicitly included in that selected action and within its scope, expected verification and rollback. Defer any additional diff inspection or cleanup: report it in the existing checkpoint or propose it as the next assessed action, without executing it in this turn. Summarize the selected action's sufficiency in the existing result; a plan or reduced diff is not proof of acceptance."
13
+ : "Before completion, inspect the final diff against the original purpose and remove unnecessary scope or complexity only when required behavior and verification remain intact. Summarize that judgment in the existing result; a plan or reduced diff is not proof of acceptance.",
14
+ ].join("\n");
15
+ }
@@ -1,3 +1,4 @@
1
+ import { minimalSufficientChangeInstruction } from '../codex-plugins/simy/hooks/minimal-sufficient.mjs';
1
2
  import { requireProviderLink } from '../provider-links.js';
2
3
  import { spawn } from 'node:child_process';
3
4
  import { randomUUID } from 'node:crypto';
@@ -83,7 +84,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
83
84
  remainingWork: run.managed.remainingWork, acceptanceCriteria: run.managed.acceptanceCriteria, proposedAction: run.managed.proposedAction,
84
85
  currentDecision: run.managed.autonomyPolicy ? run.managed.decision : undefined, diagnosis: run.managed.plan?.diagnosis,
85
86
  progress: deliveryProgressContext(run) };
86
- const prompt = run.managed.autonomyPolicy ? `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nExecute ONLY currentDecision.action. Its expectedVerification and rollback apply to this turn. Stay within provider permissions. End with ONLY one JSON checkpoint. ${assessedCheckpointContract(run.managed.decision)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nStay within the original task and provider permissions. End with ONLY one JSON object: {"status":"in_progress|completed|blocked|error","summary":"...","nextStep":"...","remainingWork":"...","proposedAction":{"kind":"read|local_edit|local_test|external|destructive|scope_change|unknown","scope":"...","summary":"..."},"evidence":[],"acceptanceMet":false}. For blocked include blocker (authorization, missing_information, external_dependency), and for authorization include approval {kind,scope,summary}. Do not claim completion without checking frozen acceptance. SIMY independently verifies supported criteria. Do not write SIMY state directly. If approval or input is needed, return blocked and stop.`;
87
+ const prompt = run.managed.autonomyPolicy ? `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nExecute ONLY currentDecision.action. Its expectedVerification and rollback apply to this turn. Stay within provider permissions. End with ONLY one JSON checkpoint. ${assessedCheckpointContract(run.managed.decision)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\n${minimalSufficientChangeInstruction()}\nStay within the original task and provider permissions. End with ONLY one JSON object: {"status":"in_progress|completed|blocked|error","summary":"...","nextStep":"...","remainingWork":"...","proposedAction":{"kind":"read|local_edit|local_test|external|destructive|scope_change|unknown","scope":"...","summary":"..."},"evidence":[],"acceptanceMet":false}. For blocked include blocker (authorization, missing_information, external_dependency), and for authorization include approval {kind,scope,summary}. Do not claim completion without checking frozen acceptance. SIMY independently verifies supported criteria. Do not write SIMY state directly. If approval or input is needed, return blocked and stop.`;
87
88
  const sandbox = run.managed.autonomyPolicy && run.managed.decision?.action.kind === 'read' ? 'read-only' : run.managed.autoRecoverySandbox;
88
89
  const args = ['exec', '--json', '--skip-git-repo-check', '-s', sandbox, '-m', run.managed.resumeModel, 'resume', record.session, prompt];
89
90
  if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
@@ -12,7 +12,8 @@ const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'authorization
12
12
  'provider_disconnect', 'desktop_access_refresh', 'desktop_access_settings', 'desktop_access_members']);
13
13
  export function desktopActionAllowed(access, command) {
14
14
  return desktopAccessReady(access) || recoveryActions.has(command?.action) ||
15
- command?.action === 'session_set_auto_continue' && command.enabled === false;
15
+ command?.action === 'session_set_auto_continue' && command.enabled === false ||
16
+ command?.action === 'session_set_email_reports' && command.enabled === false;
16
17
  }
17
18
  export function requireDesktopAccess(access) {
18
19
  if (!desktopAccessReady(access)) throw Object.assign(new Error('Sign in to an organization with SQM and Developer access before using SIMY.'), { code: 'desktop_access_required' });
@@ -37,6 +37,8 @@ import { openAuthorizationUrl } from "./browser.js";
37
37
  import { inspectBackendExecutable } from "./backend-executable.js";
38
38
  import { inspectSqmAgentSkill } from "./sqm/agent-skill.js";
39
39
  import { startWorkflowUpdates, syncInstalledWorkflows } from "./sqm/workflow-update.js";
40
+ import { createNativeDevelopmentReportsApi, normalizeNativeDevelopmentReportSave } from "./native-development-reports.js";
41
+ import { projectNativeDevelopmentReportCompletion, sendNativeDevelopmentReportCompletion } from "./native-development-report-completion.js";
40
42
  import {
41
43
  acquireDesktopProcessLock,
42
44
  assertNoLegacyCliDaemon,
@@ -147,10 +149,30 @@ export async function startDesktopHost({ input = process.stdin, output = process
147
149
  let statisticsSelection = { key: null, generation: 0 };
148
150
  const stopNotifier = createStopNotifier({ emit: event => { if (!closed && !relay) emit(event); } });
149
151
  let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
152
+ let nativeDevelopmentReports = { state: 'loading', enrollments: [] };
153
+ let nativeDevelopmentReportsScope = null;
154
+ let nativeDevelopmentReportsRequest = null;
155
+ let nativeDevelopmentReportsGeneration = 0;
156
+ let nativeDevelopmentReportsRetryAt = 0;
157
+ let nativeDevelopmentReportsMutation = false;
158
+ const nativeCompletionSuccess = new Set();
159
+ const nativeCompletionRejected = new Set();
160
+ const nativeCompletionInFlight = new Map();
161
+ const nativeCompletionRetryAt = new Map();
150
162
  const accessScope = access => JSON.stringify([access?.revision ?? null, access?.organizationId ?? null]);
151
163
  const setAccessSnapshot = async access => {
152
164
  const previousScope = accessScope(currentAccess);
165
+ const previouslyReady = desktopAccessReady(currentAccess);
153
166
  currentAccess = access || { state: 'unavailable', organizationId: null, organizationName: null };
167
+ if (previousScope !== accessScope(currentAccess) || previouslyReady && !desktopAccessReady(currentAccess)) {
168
+ // A privilege loss also invalidates consent reads, even when the
169
+ // credential/organization revision itself did not change.
170
+ nativeDevelopmentReports = { state: 'loading', enrollments: [] };
171
+ nativeDevelopmentReportsScope = null;
172
+ nativeDevelopmentReportsGeneration++;
173
+ nativeDevelopmentReportsRetryAt = 0;
174
+ nativeCompletionInFlight.clear();
175
+ }
154
176
  if (previousScope !== accessScope(currentAccess)) {
155
177
  statisticsCache.clear();
156
178
  activeStatisticsRead?.controller.abort();
@@ -201,6 +223,76 @@ export async function startDesktopHost({ input = process.stdin, output = process
201
223
  const failure = results.find(result => result.status === 'rejected');
202
224
  if (failure) throw failure.reason;
203
225
  };
226
+ const localCliApiSession = () => dependencies.getLocalCliApiSession?.() || agent?.getLocalCliApiSession?.() || null;
227
+ const nativeReportsApi = (fixedSession = null) => (dependencies.createNativeDevelopmentReportsApi || createNativeDevelopmentReportsApi)({
228
+ webOrigin,
229
+ readSession: async () => fixedSession || localCliApiSession(),
230
+ fetchImpl: dependencies.fetch || globalThis.fetch,
231
+ });
232
+ const refreshNativeDevelopmentReports = async ({ force = false } = {}) => {
233
+ if (!desktopAccessReady(currentAccess)) return nativeDevelopmentReports;
234
+ if (nativeDevelopmentReportsMutation) return nativeDevelopmentReports;
235
+ const requestedScope = accessScope(currentAccess);
236
+ if (!force && nativeDevelopmentReportsScope === requestedScope && nativeDevelopmentReports.state === 'ready') return nativeDevelopmentReports;
237
+ if (!force && Date.now() < nativeDevelopmentReportsRetryAt) return nativeDevelopmentReports;
238
+ if (nativeDevelopmentReportsRequest?.scope === requestedScope) return nativeDevelopmentReportsRequest.promise;
239
+ nativeDevelopmentReports = { state: 'loading', enrollments: nativeDevelopmentReportsScope === requestedScope ? nativeDevelopmentReports.enrollments : [] };
240
+ nativeDevelopmentReportsScope = requestedScope;
241
+ const generation = nativeDevelopmentReportsGeneration;
242
+ const job = { scope: requestedScope, generation, promise: null };
243
+ job.promise = nativeReportsApi().list().then(enrollments => {
244
+ if (accessScope(currentAccess) === requestedScope && nativeDevelopmentReportsGeneration === generation) {
245
+ nativeDevelopmentReports = { state: 'ready', enrollments };
246
+ nativeDevelopmentReportsRetryAt = 0;
247
+ }
248
+ return nativeDevelopmentReports;
249
+ }).catch(() => {
250
+ if (accessScope(currentAccess) === requestedScope && nativeDevelopmentReportsGeneration === generation) {
251
+ nativeDevelopmentReports = { state: 'unavailable', enrollments: [] };
252
+ nativeDevelopmentReportsRetryAt = Date.now() + 30_000;
253
+ }
254
+ return nativeDevelopmentReports;
255
+ }).finally(() => {
256
+ if (nativeDevelopmentReportsRequest === job) nativeDevelopmentReportsRequest = null;
257
+ if (!closed && !relay) void publish().catch(() => {});
258
+ });
259
+ nativeDevelopmentReportsRequest = job;
260
+ return job.promise;
261
+ };
262
+ const submitNativeDevelopmentReportCompletions = ({ records, scope, deviceSession }) => {
263
+ if (!deviceSession?.token || !deviceSession?.api_base_url || nativeDevelopmentReports.state !== "ready" || nativeDevelopmentReportsScope !== scope) return;
264
+ const generation = nativeDevelopmentReportsGeneration;
265
+ for (const enrollment of nativeDevelopmentReports.enrollments) {
266
+ if (enrollment.provider !== "codex" || enrollment.enabled !== true) continue;
267
+ const record = records.find(item => item.provider === enrollment.provider && item.session?.toLowerCase() === enrollment.provider_session_id?.toLowerCase());
268
+ const completion = projectNativeDevelopmentReportCompletion({ enrollment, record });
269
+ if (!completion) continue;
270
+ const key = [scope, completion.provider, completion.provider_session_id, completion.task_id, completion.enrollment_revision, completion.managed_revision, completion.acceptance_digest].join(":");
271
+ // A terminal response is tied to the consent identity, rather than a
272
+ // particular proof revision: a new consent revision is required before
273
+ // the desktop may offer that server a different attestation.
274
+ const rejectionKey = [scope, completion.provider, completion.provider_session_id, completion.task_id, completion.enrollment_revision].join(":");
275
+ if (nativeCompletionSuccess.has(key) || nativeCompletionRejected.has(rejectionKey) || nativeCompletionInFlight.has(key) || Date.now() < (nativeCompletionRetryAt.get(key) || 0)) continue;
276
+ const job = Promise.resolve().then(async () => {
277
+ if (closed || relay || nativeDevelopmentReportsGeneration !== generation || accessScope(currentAccess) !== scope) return null;
278
+ const receipt = await sendNativeDevelopmentReportCompletion({ api: nativeReportsApi(deviceSession), enrollment, record });
279
+ if (receipt && nativeDevelopmentReportsGeneration === generation && accessScope(currentAccess) === scope) {
280
+ nativeCompletionSuccess.add(key);
281
+ nativeCompletionRetryAt.delete(key);
282
+ }
283
+ return receipt;
284
+ }).catch(error => {
285
+ if (nativeDevelopmentReportsGeneration === generation && accessScope(currentAccess) === scope) {
286
+ // These are immutable authorization/contract rejections. A new
287
+ // enrollment revision or scope produces a fresh rejection identity.
288
+ if ([400, 403, 409].includes(error?.status)) nativeCompletionRejected.add(rejectionKey);
289
+ else nativeCompletionRetryAt.set(key, Date.now() + 30_000);
290
+ }
291
+ return null;
292
+ }).finally(() => nativeCompletionInFlight.delete(key));
293
+ nativeCompletionInFlight.set(key, job);
294
+ }
295
+ };
204
296
  let organizationAnalysesSuspended = false;
205
297
  let organizationAnalysisDrain = Promise.resolve();
206
298
  let organizationAnalysisEpoch = 0;
@@ -482,6 +574,10 @@ export async function startDesktopHost({ input = process.stdin, output = process
482
574
  ? structuredClone(lastStatus.deliveryHooks) : { runs: [], providers: {} };
483
575
  status.sessionSupervision = sameScope && lastStatus?.sessionSupervision
484
576
  ? structuredClone(lastStatus.sessionSupervision) : { sessions: [], providers: {}, refreshedAt: null, initialLoading: desktopAccessReady(currentAccess) };
577
+ // Complete fast frames must retain the current saved preference so OFF
578
+ // remains available while unrelated conversation enrichment is waiting.
579
+ status.nativeDevelopmentReports = sameScope && nativeDevelopmentReportsScope === accessScope(currentAccess)
580
+ ? structuredClone(nativeDevelopmentReports) : { state: 'loading', enrollments: [] };
485
581
  // Cached enrichment must follow the current connection preference even
486
582
  // while a full projection is waiting on records or remote metadata.
487
583
  status.deliveryHooks.runs = linkedSessions(status.deliveryHooks.runs || []);
@@ -538,10 +634,24 @@ export async function startDesktopHost({ input = process.stdin, output = process
538
634
  status.sessionSupervision = { sessions: [], providers: {}, refreshedAt: null };
539
635
  if (JSON.stringify(await readProviderLinks(root)) !== requestedLinks) { publishAgain = true; return; }
540
636
  if (connectionRevision !== requestedConnectionRevision || !sameAccess(sampledAccess, agent?.getDesktopAccess ? agent.getDesktopAccess() : await readAccess())) { publishAgain = true; return; }
637
+ status.nativeDevelopmentReports = { state: 'loading', enrollments: [] };
541
638
  if (!closed && !relay) emitStatus(status);
542
639
  return;
543
640
  }
544
641
  await analysisManager.updateProviderStatuses?.(status.providers);
642
+ // Listing only reads rows previously registered by this device/account/org;
643
+ // it never passes the locally discovered catalog to Web.
644
+ if (agent.getLocalCliApiSession || dependencies.getLocalCliApiSession) {
645
+ void refreshNativeDevelopmentReports();
646
+ }
647
+ status.nativeDevelopmentReports = structuredClone(nativeDevelopmentReports);
648
+ // This is an asynchronous local attestation. It neither changes a
649
+ // Delivery Loop nor places receipt or device data in status frames.
650
+ submitNativeDevelopmentReportCompletions({
651
+ records: allRecords,
652
+ scope: requestedScope,
653
+ deviceSession: localCliApiSession(),
654
+ });
545
655
  stopNotifier.observe(records);
546
656
  const projection = await actionAssociations.beginProjection?.({ records });
547
657
  status.deliveryHooks = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: deliveryRoot, records: projection?.records || records }).catch(() => ({ error: "Delivery hook status unavailable", providers: {}, runs: [] }));
@@ -1080,6 +1190,92 @@ export async function startDesktopHost({ input = process.stdin, output = process
1080
1190
  // forced request could invalidate a concurrent manual recommendation.
1081
1191
  void publish().catch(() => {});
1082
1192
  return;
1193
+ } else if (action === "session_set_email_reports") {
1194
+ if (typeof command.enabled !== 'boolean') throw new Error('Choose whether development report emails are enabled.');
1195
+ if (command.enabled) {
1196
+ if (command.provider !== 'codex') throw new Error('Development report email is not available for this provider yet.');
1197
+ // Initial consent binds only to a currently observed local session.
1198
+ // The title remains caller-provided; the host never reads transcript
1199
+ // content to populate it.
1200
+ await selectedSession(command, { preferObserved: true });
1201
+ }
1202
+ if (command.enabled) await authorizeWork(commandScope);
1203
+ // Validate before the recovery path: an invalid local command is not
1204
+ // an ambiguous remote write and must never be accepted from a list.
1205
+ const request = normalizeNativeDevelopmentReportSave({
1206
+ provider: command.provider,
1207
+ provider_session_id: command.session,
1208
+ request_title: command.requestTitle,
1209
+ enabled: command.enabled,
1210
+ expected_revision: command.expectedRevision,
1211
+ });
1212
+ const deviceSession = localCliApiSession();
1213
+ if (!deviceSession) throw new Error('SIMY desktop sign-in is required.');
1214
+ // Keep the credential tuple fixed across PUT and its readback. A
1215
+ // changed account/device scope must fail instead of confirming one
1216
+ // user’s write with another user’s registry.
1217
+ const reportsApi = nativeReportsApi(deviceSession);
1218
+ const sameIdentity = enrollment => enrollment.provider === request.provider &&
1219
+ enrollment.provider_session_id === String(request.provider_session_id || '').toLowerCase();
1220
+ const matchesSaved = enrollment => sameIdentity(enrollment) && enrollment.enabled === request.enabled &&
1221
+ (!request.enabled || enrollment.request_title === String(request.request_title || '').trim());
1222
+ const confirmed = enrollments => request.enabled
1223
+ ? enrollments.some(matchesSaved)
1224
+ : !enrollments.some(sameIdentity) || enrollments.some(matchesSaved);
1225
+ const mutationGeneration = ++nativeDevelopmentReportsGeneration;
1226
+ nativeDevelopmentReportsMutation = true;
1227
+ let recoveredCurrent = false;
1228
+ const invalidateMutation = () => {
1229
+ if (nativeDevelopmentReportsGeneration !== mutationGeneration || accessScope(currentAccess) !== commandScope) return;
1230
+ nativeDevelopmentReports = { state: 'unavailable', enrollments: [] };
1231
+ nativeDevelopmentReportsRetryAt = Date.now() + 30_000;
1232
+ };
1233
+ try {
1234
+ try {
1235
+ await reportsApi.save(request);
1236
+ } catch (error) {
1237
+ // The server may have committed before a connection failure. Read
1238
+ // back the device-scoped registry before reporting an error.
1239
+ const recovered = await reportsApi.list().catch(() => null);
1240
+ if (!recovered) {
1241
+ invalidateMutation();
1242
+ throw error;
1243
+ }
1244
+ // A conflict contains useful current revision state. Publish it so
1245
+ // the next explicit choice starts from the server's revision.
1246
+ if (nativeDevelopmentReportsGeneration === mutationGeneration && accessScope(currentAccess) === commandScope) {
1247
+ nativeDevelopmentReports = { state: 'ready', enrollments: recovered };
1248
+ nativeDevelopmentReportsRetryAt = 0;
1249
+ recoveredCurrent = true;
1250
+ }
1251
+ if (!confirmed(recovered)) throw error;
1252
+ }
1253
+ const enrollments = await reportsApi.list();
1254
+ if (!confirmed(enrollments)) throw new Error('Development report email settings could not be confirmed.');
1255
+ if (command.enabled) await authorizeWork(commandScope);
1256
+ const currentDeviceSession = localCliApiSession();
1257
+ if (!currentDeviceSession || currentDeviceSession.token !== deviceSession.token || currentDeviceSession.api_base_url !== deviceSession.api_base_url) {
1258
+ throw Object.assign(new Error('Desktop account or organization changed.'), { code: 'desktop_access_required' });
1259
+ }
1260
+ if (nativeDevelopmentReportsGeneration !== mutationGeneration) throw new Error('Development report email settings changed; refresh and try again.');
1261
+ nativeDevelopmentReportsScope = accessScope(currentAccess);
1262
+ nativeDevelopmentReports = { state: 'ready', enrollments };
1263
+ nativeDevelopmentReportsRetryAt = 0;
1264
+ // Fence a GET that was in flight before this completed mutation.
1265
+ nativeDevelopmentReportsGeneration++;
1266
+ emit({ type: "result", id: command.id, ok: true });
1267
+ void publish().catch(() => {});
1268
+ return;
1269
+ } catch (error) {
1270
+ // A completed PUT without a confirmable readback must not leave a
1271
+ // prior ready revision cached forever. Preserve a 409 readback,
1272
+ // which is itself the current server state.
1273
+ if (!recoveredCurrent) invalidateMutation();
1274
+ throw error;
1275
+ } finally {
1276
+ nativeDevelopmentReportsMutation = false;
1277
+ if (!closed && !relay) void publish().catch(() => {});
1278
+ }
1083
1279
  } else if (action === "session_deadline_decide") {
1084
1280
  if (command.cancel === true) { emit({ type: "result", id: command.id, ok: true }); return; }
1085
1281
  const candidate = await selectedSession(command);
@@ -1150,7 +1346,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
1150
1346
  void execute();
1151
1347
  } else if (command.action === "my_ai_session") {
1152
1348
  myAiSessionQueue = myAiSessionQueue.then(execute).catch(() => {});
1153
- } else if (command.action === "session_set_auto_continue") {
1349
+ } else if (["session_set_auto_continue", "session_set_email_reports"].includes(command.action)) {
1154
1350
  preferenceQueue = preferenceQueue.then(execute).catch(() => {});
1155
1351
  } else if (command.action === 'session_statistics') {
1156
1352
  void execute();