@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.
@@ -28,6 +28,10 @@ async function runCodingLoopOwned({
28
28
  shouldStop = () => false,
29
29
  consumeHumanGuidance = () => "",
30
30
  }) {
31
+ if (resume && snapshot.attempts.at(-1)?.ci_review_refresh_pending) {
32
+ return recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm, executeIndependentAudit,
33
+ shouldStop, onUpdate, retryCiReview: true });
34
+ }
31
35
  if (!resume && snapshot.attempts.length > 0 && !snapshot.metadata?.completed_worker_result) {
32
36
  return recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm, onUpdate });
33
37
  }
@@ -263,39 +267,7 @@ async function runCodingLoopOwned({
263
267
  });
264
268
  await publish(snapshot, "independent_auditing", onUpdate);
265
269
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
266
- const auditInstruction = buildIndependentAuditInstruction(snapshot.charter, attempt);
267
- const reviewedMatrix = attempt.observed_evidence?.minimum_completion?.matrix_sha256;
268
- const reviewedEvidenceIdentity = independentAuditEvidenceIdentity(attempt);
269
- const reviewedObjectiveDigest = snapshot.charter.thread_state?.objective_contract?.current_digest;
270
- const reviewedObjectiveVersion = snapshot.charter.thread_state?.objective_contract?.version;
271
- const auditExecution = await safeExecution(() =>
272
- executeIndependentAudit({
273
- attemptNumber,
274
- backend: snapshot.charter.audit_backend,
275
- instruction: auditInstruction,
276
- charter: snapshot.charter,
277
- attempt,
278
- }),
279
- );
280
- if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
281
- attempt.independent_audit = buildIndependentAudit(
282
- auditExecution,
283
- snapshot.charter,
284
- attempt,
285
- );
286
- attempt.token_usage = mergeTokenUsage(attempt.token_usage, auditExecution.tokenUsage);
287
- attempt.budget = refreshBudgetState(snapshot);
288
- recordBudgetWarnings(snapshot, attempt);
289
- if (attempt.budget.token_exhausted) {
290
- await stopForBudget({ snapshot, attempt, reason: "token_budget_exhausted", onUpdate });
291
- return snapshot;
292
- }
293
-
294
- attempt.independent_audit.result_parse = normalizeResultParseDiagnostic(auditExecution.result_parse);
295
- attempt.independent_audit.minimum_matrix_sha256 = reviewedMatrix;
296
- attempt.independent_audit.evidence_identity_sha256 = reviewedEvidenceIdentity;
297
- attempt.independent_audit.objective_digest = reviewedObjectiveDigest;
298
- attempt.independent_audit.objective_version = reviewedObjectiveVersion;
270
+ if (await reviewAttempt({ snapshot, attempt, executeIndependentAudit, shouldStop, onUpdate })) return snapshot;
299
271
 
300
272
  // Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
301
273
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt, { retries: snapshot.metadata?.completed_worker_result ? 2 : 0, shouldStop });
@@ -369,7 +341,63 @@ async function stopCodingLoop(snapshot, onUpdate) {
369
341
  return snapshot;
370
342
  }
371
343
 
372
- async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = null, onUpdate }) {
344
+ async function reviewAttempt({ snapshot, attempt, executeIndependentAudit, shouldStop, onUpdate }) {
345
+ const auditInstruction = buildIndependentAuditInstruction(snapshot.charter, attempt);
346
+ const reviewedMatrix = attempt.observed_evidence?.minimum_completion?.matrix_sha256;
347
+ const reviewedEvidenceIdentity = independentAuditEvidenceIdentity(attempt);
348
+ const reviewedChecks = structuredClone(attempt.observed_evidence?.github?.checks || []);
349
+ const reviewedObjectiveDigest = snapshot.charter.thread_state?.objective_contract?.current_digest;
350
+ const reviewedObjectiveVersion = snapshot.charter.thread_state?.objective_contract?.version;
351
+ const auditExecution = await safeExecution(() =>
352
+ executeIndependentAudit({
353
+ attemptNumber: attempt.attempt_number,
354
+ backend: snapshot.charter.audit_backend,
355
+ instruction: auditInstruction,
356
+ charter: snapshot.charter,
357
+ attempt,
358
+ }),
359
+ );
360
+ if (shouldStop()) { await stopCodingLoop(snapshot, onUpdate); return true; }
361
+ attempt.independent_audit = buildIndependentAudit(
362
+ auditExecution,
363
+ snapshot.charter,
364
+ attempt,
365
+ );
366
+ attempt.token_usage = mergeTokenUsage(attempt.token_usage, auditExecution.tokenUsage);
367
+ attempt.budget = refreshBudgetState(snapshot);
368
+ recordBudgetWarnings(snapshot, attempt);
369
+ if (attempt.budget.token_exhausted) {
370
+ await stopForBudget({ snapshot, attempt, reason: "token_budget_exhausted", onUpdate });
371
+ return true;
372
+ }
373
+
374
+ attempt.independent_audit.result_parse = normalizeResultParseDiagnostic(auditExecution.result_parse);
375
+ attempt.independent_audit.reviewed_checks = reviewedChecks;
376
+ attempt.independent_audit.minimum_matrix_sha256 = reviewedMatrix;
377
+ attempt.independent_audit.evidence_identity_sha256 = reviewedEvidenceIdentity;
378
+ attempt.independent_audit.objective_digest = reviewedObjectiveDigest;
379
+ attempt.independent_audit.objective_version = reviewedObjectiveVersion;
380
+ return false;
381
+ }
382
+
383
+ // Only a CI-only change to the already reviewed candidate may automatically
384
+ // renew its review. Head/base/preparation/acceptance changes need explicit work.
385
+ function ciReviewRefreshIdentity(attempt, previousEvidence) {
386
+ const reviewed = attempt.independent_audit;
387
+ const github = attempt.observed_evidence?.github;
388
+ if (!reviewed?.passed || !reviewed.evidence_identity_sha256 || !github?.available ||
389
+ !attempt.observed_evidence?.local?.available || !attempt.observed_evidence.local.working_tree_clean ||
390
+ attempt.observed_evidence.local.errors?.length || github.error || !github.required_checks_present || !github.checks_passing ||
391
+ !github.checks?.length || github.checks.some(check => check.state !== 'success' && check.state !== 'skipped')) return null;
392
+ const identity = independentAuditEvidenceIdentity(attempt);
393
+ if (identity === reviewed.evidence_identity_sha256) return null;
394
+ const prior = { ...attempt, observed_evidence: { ...attempt.observed_evidence,
395
+ github: { ...github, checks: reviewed.reviewed_checks || previousEvidence?.github?.checks } } };
396
+ return independentAuditEvidenceIdentity(prior) === reviewed.evidence_identity_sha256 ? identity : null;
397
+ }
398
+
399
+ async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = null, executeIndependentAudit, shouldStop = () => false, onUpdate, retryCiReview = false }) {
400
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
373
401
  if (!verifyObjective(snapshot.charter).valid) {
374
402
  snapshot.implementation_gate_passed = false;
375
403
  snapshot.done_gate_passed = false;
@@ -391,6 +419,7 @@ async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = n
391
419
  code: "OBJECTIVE_EVIDENCE_STALE", severity: "blocker", target: "objective_contract",
392
420
  explanation: "Make a new attempt and independently review evidence against the current objective.", repairability: "manual",
393
421
  }]);
422
+ if (retryCiReview) attempt.ci_review_refresh_pending = false;
394
423
  await publish(snapshot, "waiting_human", onUpdate);
395
424
  return snapshot;
396
425
  }
@@ -398,16 +427,72 @@ async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = n
398
427
  attempt_number: attempt.attempt_number,
399
428
  });
400
429
  await publish(snapshot, "checking_pr", onUpdate);
430
+ const previousEvidence = attempt.observed_evidence;
431
+ // Recover an old review's CI basis only while the saved full identity still
432
+ // proves it. Later monitoring observations cannot reconstruct that history.
433
+ if (!attempt.independent_audit?.reviewed_checks &&
434
+ attempt.independent_audit?.evidence_identity_sha256 === independentAuditEvidenceIdentity(attempt) &&
435
+ Array.isArray(previousEvidence?.github?.checks)) {
436
+ attempt.independent_audit.reviewed_checks = structuredClone(previousEvidence.github.checks);
437
+ }
401
438
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
439
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
402
440
  attempt.sqm = await safeSqm(checkSqm, attempt, attempt.sqm, "full");
441
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
442
+ if (retryCiReview && attempt.observed_evidence?.local?.available && attempt.observed_evidence?.github?.available &&
443
+ independentAuditEvidenceIdentity(attempt) !== attempt.ci_review_refresh_identity) {
444
+ attempt.ci_review_refresh_pending = false;
445
+ snapshot.final_audit = humanGate("The candidate changed while its CI review was interrupted.", [{
446
+ code: "CI_REVIEW_CANDIDATE_CHANGED", severity: "blocker", target: "independent_audit",
447
+ explanation: "Reconcile the changed HEAD, base or evidence, then explicitly continue work; the prior review cannot certify this candidate.", repairability: "manual",
448
+ }]);
449
+ await publish(snapshot, "waiting_human", onUpdate);
450
+ return snapshot;
451
+ }
452
+ const retryIdentity = retryCiReview && attempt.ci_review_refresh_pending &&
453
+ independentAuditEvidenceIdentity(attempt) === attempt.ci_review_refresh_identity &&
454
+ attempt.observed_evidence?.local?.available && attempt.observed_evidence.local.working_tree_clean &&
455
+ !attempt.observed_evidence.local.errors?.length && attempt.observed_evidence?.github?.available &&
456
+ !attempt.observed_evidence.github.error && attempt.observed_evidence.github.checks_passing &&
457
+ attempt.observed_evidence.github.required_checks_present ? attempt.ci_review_refresh_identity : null;
458
+ const refreshIdentity = retryIdentity || ciReviewRefreshIdentity(attempt, previousEvidence);
459
+ if (executeIndependentAudit && refreshIdentity && (retryIdentity || attempt.ci_review_refresh_identity !== refreshIdentity)) {
460
+ const budget = canStartProvider(snapshot, { attemptNumber: attempt.attempt_number, phase: "reviewer" });
461
+ if (!budget.allowed) { await stopForBudget({ snapshot, attempt, reason: budget.reason, onUpdate }); return snapshot; }
462
+ // Persist before dispatch so an interrupted/failed review cannot repeat on
463
+ // every monitor tick. Explicit resume retries only this bound review.
464
+ attempt.ci_review_refresh_identity = refreshIdentity;
465
+ attempt.ci_review_refresh_pending = true;
466
+ appendEvent(snapshot, "independent_auditing", "CI completed; refreshing independent review of the same candidate.", {
467
+ attempt_number: attempt.attempt_number, evidence_identity: refreshIdentity,
468
+ });
469
+ await publish(snapshot, "independent_auditing", onUpdate);
470
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
471
+ if (await reviewAttempt({ snapshot, attempt, executeIndependentAudit, shouldStop, onUpdate })) return snapshot;
472
+ if (!attempt.independent_audit.passed &&
473
+ !attempt.independent_audit.findings.some(finding => finding.code === "INDEPENDENT_AUDIT_UNAVAILABLE")) {
474
+ // A completed adverse review belongs to the normal repair path. Only
475
+ // unavailable/interrupted reviews need a reviewer-only retry.
476
+ attempt.ci_review_refresh_pending = false;
477
+ }
478
+ attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
479
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
480
+ attempt.sqm = await safeSqm(checkSqm, attempt, attempt.sqm, "full");
481
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
482
+ }
403
483
  attempt.audit = await auditAttempt(snapshot.charter, attempt, {
404
484
  previousAttempt: snapshot.attempts.at(-2) ?? null,
405
485
  sqm: attempt.sqm,
406
486
  });
487
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
407
488
  attempt.outcome_progress = assessOutcomeProgress(
408
489
  snapshot.charter, attempt, snapshot.attempts.at(-2) ?? null,
409
490
  outcomeOptions(snapshot),
410
491
  );
492
+ if (attempt.ci_review_refresh_pending && attempt.audit.passed && attempt.independent_audit?.passed &&
493
+ attempt.independent_audit.evidence_identity_sha256 === independentAuditEvidenceIdentity(attempt)) {
494
+ attempt.ci_review_refresh_pending = false;
495
+ }
411
496
  snapshot.implementation_gate_passed = attempt.audit.passed;
412
497
  attempt.pr_readiness = evaluatePrReadiness(snapshot.charter, attempt);
413
498
  attempt.pr_gate = attempt.pr_readiness;
package/src/runner.js CHANGED
@@ -51,7 +51,7 @@ import {
51
51
  normalizeWorkflowStartIntent,
52
52
  } from "./workflow-observation-sender.js";
53
53
  import { saveCodingRunSnapshot } from "./coding-run-store.js";
54
- import { acquireDesktopProcessLock } from "./desktop-process-lock.js";
54
+ import { acquireDesktopProcessLock, PROCESS_CONFLICT_CODE } from "./desktop-process-lock.js";
55
55
  import {
56
56
  assertRecoveryActor, completedWorkerResult, recoveryActorBinding, refreshRecoveryAudit, refreshRecoveryBudgetAudit,
57
57
  } from "./coding-run-recovery.js";
@@ -924,7 +924,24 @@ async function cleanupLocalAttachments(run) {
924
924
  }
925
925
  }
926
926
 
927
- export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
927
+ export async function recheckLocalCodingRun(run, options = {}) {
928
+ if (!run?.snapshot?.attempts?.length) return run?.snapshot || null;
929
+ if (!["pr_ready_for_review", "merge_ready"].includes(run.status)) return run.snapshot;
930
+ if (run.operation || run.child || run.stopRequested) return run.snapshot;
931
+ const operation = (async () => {
932
+ assertRecoveryActor(run);
933
+ const lease = run.localStateRoot ? await acquireDesktopProcessLock(
934
+ path.join(run.localStateRoot, ".controllers", executionTextSha256(run.id)),
935
+ ) : null;
936
+ try { return await recheckLocalCodingRunOwned(run, options); }
937
+ finally { await lease?.release(); }
938
+ })();
939
+ run.operation = operation;
940
+ try { return await operation; }
941
+ finally { if (run.operation === operation) run.operation = null; }
942
+ }
943
+
944
+ async function recheckLocalCodingRunOwned(run, { collectEvidence, executeIndependentAudit, authorizeReview } = {}) {
928
945
  if (!run?.snapshot?.attempts?.length) return run?.snapshot || null;
929
946
  if (!["pr_ready_for_review", "merge_ready"].includes(run.status)) return run.snapshot;
930
947
  const repositoryPath = await resolveRepositoryPath(run.request);
@@ -956,6 +973,15 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
956
973
  await recheckPrReadiness({
957
974
  snapshot: run.snapshot,
958
975
  collectEvidence: evidenceCollector,
976
+ executeIndependentAudit: async context => {
977
+ await authorizeReview?.();
978
+ assertRecoveryActor(run);
979
+ if (run.stopRequested) throw new Error("Review cancelled by the local operator.");
980
+ return executeIndependentAudit ? executeIndependentAudit(context) : executeProcessAttempt({
981
+ ...context, repositoryPath, run, marker: "SIMY_AUDIT_JSON:",
982
+ });
983
+ },
984
+ shouldStop: () => run.stopRequested,
959
985
  checkSqm: sqmChecker,
960
986
  onUpdate: async (snapshot) => {
961
987
  run.snapshot = snapshot;
@@ -966,6 +992,8 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
966
992
  }
967
993
 
968
994
  export async function markCodingRunMonitorFailure(run, error) {
995
+ // A losing controller never owns the right to persist its stale snapshot.
996
+ if (run.stopRequested || run.status === "stopped" || error?.code === PROCESS_CONFLICT_CODE) return run.snapshot;
969
997
  const message = error instanceof Error ? error.message : String(error);
970
998
  run.snapshot.done_gate_passed = false;
971
999
  run.snapshot.final_audit = {
@@ -1026,6 +1054,7 @@ export async function executeProcessAttempt({
1026
1054
  model: phase === "reviewer" ? charter?.audit_model : charter?.model,
1027
1055
  reasoningEffort: phase === "reviewer" ? charter?.audit_reasoning_effort : charter?.reasoning_effort,
1028
1056
  });
1057
+ if (run.stopRequested) throw new Error("Execution cancelled by the local operator.");
1029
1058
  const startedAt = new Date().toISOString();
1030
1059
  const stdout = [];
1031
1060
  const stderr = [];
@@ -22,6 +22,7 @@ export async function claudePluginFiles() {
22
22
  files.set(relative, content.replaceAll("current Codex thread", "current Claude Code conversation"));
23
23
  }
24
24
  files.set("hooks/delivery-loop.mjs", await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8"));
25
+ files.set("hooks/minimal-sufficient.mjs", await readFile(path.join(SOURCE, "hooks/minimal-sufficient.mjs"), "utf8"));
25
26
  files.set("hooks/outcome-progress.mjs", await readFile(path.join(SOURCE, "hooks/outcome-progress.mjs"), "utf8"));
26
27
  files.set("hooks/continuation-policy.mjs", await readFile(path.join(SOURCE, "hooks/continuation-policy.mjs"), "utf8"));
27
28
  files.set("hooks/action-association.mjs", await readFile(path.join(SOURCE, "hooks/action-association.mjs"), "utf8"));
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { maintainWindowsUpdate, selectedWindowsRoot } from './windows-update.js';
6
+ import { desktopArguments } from './windows-inbound.js';
6
7
  const installation = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
7
8
  const stateRoot = path.join(process.env.LOCALAPPDATA, 'SIMY CLI', 'updates');
8
9
  const argv = process.argv.slice(2);
@@ -22,7 +23,7 @@ if (updating) {
22
23
  const desktop = argv[0] === '--desktop';
23
24
  if (desktop && !existsSync(path.join(root, 'simy-desktop.exe'))) root = installation;
24
25
  const child = spawn(path.join(root, desktop ? 'simy-desktop.exe' : 'runtime/node.exe'),
25
- desktop ? ['--selected'] : [path.join(root, 'cli/src/index.js'), ...argv], {
26
+ desktop ? desktopArguments(argv) : [path.join(root, 'cli/src/index.js'), ...argv], {
26
27
  stdio: 'inherit', env: { ...process.env, ...(desktop ? {} : { SIMY_AUTO_UPDATE: '0' }) },
27
28
  });
28
29
  // Windows console signals are already delivered to both processes.
@@ -0,0 +1,9 @@
1
+ // Keep the URI handoff separate from update and launch code so its exact
2
+ // command shape is covered by a platform-neutral test. desktop.cs remains the
3
+ // authority that validates the URI before it changes a visible selection.
4
+ export function desktopArguments(argv) {
5
+ // This internal marker distinguishes the desktop spawned by the update
6
+ // bootstrap from the bootstrap itself. It carries no inbound URI.
7
+ if (argv.length === 1) return ['--selected'];
8
+ return ['--selected', ...argv.slice(1)];
9
+ }