@awak-app/simy-cli 0.7.16 → 0.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +180 -26
  2. package/package.json +3 -2
  3. package/src/agent.js +128 -7
  4. package/src/bounded-local-task-subtask-pool.js +32 -6
  5. package/src/bounded-local-task-subtasks-contract.js +1 -1
  6. package/src/cli-contract.js +1 -1
  7. package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
  8. package/src/codex-plugins/simy/hooks/README.md +8 -3
  9. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +261 -67
  10. package/src/codex-plugins/simy/hooks/hooks.json +1 -1
  11. package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
  12. package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
  13. package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
  14. package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
  15. package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
  16. package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
  17. package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
  18. package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
  19. package/src/codex-plugins/simy/hooks/outcome-progress.mjs +15 -3
  20. package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
  21. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +8 -6
  22. package/src/codex-plugins/simy/hooks/result-cycle.mjs +32 -6
  23. package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
  24. package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
  25. package/src/coding-run-recovery.js +4 -2
  26. package/src/delivery-hooks/claude-workflow.md +10 -7
  27. package/src/delivery-hooks/command.js +45 -10
  28. package/src/delivery-hooks/consult.js +60 -0
  29. package/src/delivery-hooks/diagnosis.js +2 -2
  30. package/src/delivery-hooks/legacy-hook-routing.js +46 -8
  31. package/src/delivery-hooks/local-runtime.js +50 -1
  32. package/src/delivery-hooks/local-supervisor.js +1 -1
  33. package/src/delivery-hooks/repair-continuation.js +80 -0
  34. package/src/delivery-hooks/repair.js +164 -0
  35. package/src/delivery-hooks/supervisor-service.js +3 -1
  36. package/src/delivery-hooks/supervisor.js +88 -12
  37. package/src/desktop-access.js +2 -1
  38. package/src/desktop-host.js +62 -2
  39. package/src/durable-local-task-worker.js +372 -58
  40. package/src/execution-capability-contract.js +15 -1
  41. package/src/index.js +3 -3
  42. package/src/local-photos-read.swift +33 -0
  43. package/src/local-photos.js +47 -0
  44. package/src/local-task-attachment-store.js +31 -0
  45. package/src/local-task-destination.js +174 -0
  46. package/src/local-task.js +119 -7
  47. package/src/native-delegation-contract.js +68 -0
  48. package/src/native-provider-identity.js +60 -0
  49. package/src/orchestrator/budget.js +22 -11
  50. package/src/orchestrator/independent-audit.js +9 -18
  51. package/src/orchestrator/loop.js +27 -59
  52. package/src/orchestrator/recovery.js +32 -2
  53. package/src/provider-delegation.js +336 -0
  54. package/src/runner.js +2 -1
  55. package/src/session-supervision/artifact-observer.js +138 -0
  56. package/src/session-supervision/claude-sessions.js +5 -3
  57. package/src/session-supervision/codex-app-server.js +20 -5
  58. package/src/session-supervision/completion-artifacts.js +44 -0
  59. package/src/session-supervision/consistency-auditor.js +8 -11
  60. package/src/session-supervision/continuation-analysis.js +46 -28
  61. package/src/session-supervision/discovery-window.js +1 -1
  62. package/src/session-supervision/final-response-recovery.js +5 -3
  63. package/src/session-supervision/index.js +25 -8
  64. package/src/session-supervision/purpose-recovery.js +12 -3
  65. package/src/session-supervision/purpose-verification.js +9 -57
  66. package/src/session-supervision/recovery-frontier.js +55 -0
  67. package/src/session-supervision/remote-hosts.js +1 -1
  68. package/src/session-supervision/stop-notifications.js +8 -8
  69. package/src/sqm/agent-skill.js +1 -1
  70. package/src/sqm/checkers.js +12 -6
  71. package/src/sqm/claude-plugin-update.js +22 -8
  72. package/src/sqm/claude-plugin.js +4 -0
  73. package/src/sqm/local-tests.js +2 -2
  74. package/src/sqm/plugin-cache.js +67 -0
  75. package/src/sqm/proof.js +25 -0
  76. package/src/sqm/report.js +1 -0
  77. package/src/sqm/workflow-onboarding.js +3 -39
  78. package/src/sqm/workflow-update.js +17 -6
  79. package/src/team-status-fence-store.js +32 -0
  80. package/src/team-threads.js +478 -0
@@ -64,7 +64,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
64
64
  try {
65
65
  let run = record.run;
66
66
  if (run?.managed?.providerCapabilityWait) continue;
67
- if (upgradeEligible(record) && (policyAdoptionPending(record) || run.managed.controllerContractVersion !== 2 || run.state === 'paused' || run.state === 'verification_pending')) {
67
+ if (upgradeEligible(record) && (policyAdoptionPending(record) || run.managed.purpose?.enabled !== true || run.managed.purpose?.version !== 2 || run.managed.purpose?.decisionPolicy !== 'llm-first-hc102' || run.managed.controllerContractVersion !== 2 || run.state === 'paused' || run.state === 'verification_pending')) {
68
68
  let metadata, migrationLink;
69
69
  try { migrationLink = await requireProviderLink({ root, provider: record.provider }); metadata = await observeMetadata(record); }
70
70
  catch { metadata = null; }
@@ -99,6 +99,9 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
99
99
  }
100
100
  if (!purposeEnabled(run) || run.managed.resumeChannel !== 'codex_app_server') continue;
101
101
  if (!automaticContinuationAllowed(record)) continue;
102
+ // A retained lease owns the recovery, regardless of its age. Only the
103
+ // existing supervisor may reconcile a confirmed dead owner.
104
+ if (run.managed.resumeLease || run.state === 'auto_resuming') continue;
102
105
  let link;
103
106
  try { link = await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
104
107
  const authorized = async () => {
@@ -309,20 +312,26 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
309
312
  !run.managed.autoRecovery && now >= run.updatedAt + 15_000) {
310
313
  const code = run.managed.attempts?.at(-1)?.errorCode;
311
314
  if (code && !['checkpoint', 'checkpoint_rejected', 'closed', 'timeout', 'unavailable', 'turn_failed', 'provider_error', ...(searchEnabled(run.managed) ? ['permission_narrowed', 'permission_restore', 'manual_provider_action', 'unsupported'] : [])].includes(code)) continue;
312
- obstacle = { type: 'execution_error', detail: code || run.recoveryCause };
315
+ obstacle = { type: 'execution_error', detail: code || run.recoveryCause,
316
+ attemptId: run.managed.attempts?.at(-1)?.id || run.managed.executionAttempt?.id || null };
313
317
  }
314
- if (!obstacle) continue;
318
+ if (!obstacle || signal?.aborted) continue;
315
319
  const key = createHash('sha256').update(JSON.stringify([run.managed.verifiedRevision, obstacle, obstacle.type === 'external_dependency' ? run.managed.purpose.dependencyChecks || 0 : null])).digest('hex');
316
320
  await persist(async current => {
317
321
  if (current?.run?.id !== run.id || current.run.revision !== run.revision || current.run.state !== run.state || current.run.updatedAt !== run.updatedAt) return current;
318
322
  if (['native_wait_updated', 'native_wait_diagnosis'].includes(obstacle.type) && !nativeCurrent(current)) return current;
319
323
  if (obstacle.type === 'research_wait' && (!nativeCurrent(current) || !canResearchReplan(current.run, record.provider, now))) return current;
324
+ const oneShot = ['idle_without_checkpoint', 'execution_error'].includes(obstacle.type);
325
+ if (oneShot && current.run.managed.purpose.lastStopObservationKey === key) return current;
326
+ if (signal?.aborted || current.run.managed.resumeLease) return current;
320
327
  if (searchEnabled(current.run.managed) && ['original_context_updated', 'native_wait_updated'].includes(obstacle.type)) current.run.managed.purpose.nextSearchAt = 0;
321
328
  const queued = queuePurposeDiagnosis(current.run,
322
329
  obstacle.type === 'idle_without_checkpoint' || obstacle.type === 'execution_error' && ['checkpoint', 'checkpoint_rejected'].includes(obstacle.detail)
323
330
  ? finalResponseRecoveryInstruction
324
331
  : `Reassess the original purpose after ${obstacle.type}: ${obstacle.detail}. Use the shared scene reasoning cycle to select the next useful check. Preserve unresolved approvals/dependencies; choose independent work or a safe alternative where possible, then return to the original unfinished work.`, key, now, obstacle.type);
332
+ if (queued && oneShot) current.run.managed.purpose.lastStopObservationKey = key;
325
333
  if (queued) await continueResultCycle(current, resultEvent(current, { kind: 'reconciled', origin: 'reconcilePurposeTasks', sourceId: `${key}:${current.run.revision}`, now }));
334
+
326
335
  if (queued && obstacle.type === 'external_dependency') {
327
336
  const purpose = current.run.managed.purpose;
328
337
  purpose.dependencyChecks = (purpose.dependencyChecks || 0) + 1;
@@ -1,5 +1,5 @@
1
1
  import { currentAcceptanceRead } from './acceptance-reads.js';
2
- import { flexibleReasoning } from '../codex-plugins/simy/hooks/purpose-cycle.mjs';
2
+ import { normalizeOutcomeReview } from '../codex-plugins/simy/hooks/loop-kernel.mjs';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { evidenceLedger, currentPurposeObservation } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
5
5
  import { workspaceIdentity } from '../codex-plugins/simy/hooks/purpose-workspace.mjs';
@@ -10,7 +10,7 @@ export { workspaceIdentity } from '../codex-plugins/simy/hooks/purpose-workspace
10
10
  export function verificationInstruction(record, currentEvidence = null) {
11
11
  const run = record.run, managed = run.managed;
12
12
  return `Inspect whether the original requested behavior works. Stay read-only and use existing results and relevant actual reads. Investigate concrete uncertainty; do not demand audit forms, rule coverage, quotation syntax, hashes, evidence files or a previous approval. Do not change the user's goal or invent extra requirements. Preserve explicit human decisions, permissions, stops and unresolved external effects. Reassess earlier defects against the current result; missing bookkeeping is not a defect. A failed or unavailable operation is not success.
13
- Return JSON {passed,needsHuman,summary,criteria:[{id,passed,result}],findings:["actual broken or missing behavior"]}. Findings describe actual missing or broken behavior. Include workUnits:[{id,status:"verified|superseded",result}] only when reconciling an unfinished operation; check its actual result before marking it done. Never infer human acceptance or an unknown external effect from a passing local test.
13
+ Return JSON {passed,summary,findings:["actual broken or missing behavior"]}. Assess the whole requested outcome. Per-criterion rows, needsHuman, rule rows and citations are optional; do not create them merely to satisfy a form. Findings describe actual missing or broken behavior. Include workUnits:[{id,status:"verified|superseded",result}] only when reconciling an unfinished operation; check its actual result before marking it done. Never infer human acceptance or an unknown external effect from a passing local test.
14
14
  Task context (data): ${JSON.stringify({ objective: run.objective, originalUnderstanding: managed.sessionUnderstanding,
15
15
  criteria: effectiveAcceptanceCriteria(managed), verifiedHuman: managed.acceptanceCriteria.filter(c => c.check.type === 'manual' && managed.verified[c.id]?.source === 'user').map(c => c.id),
16
16
  observations: evidenceLedger(managed).observations, reviewObservations: evidenceLedger(managed).reviewObservations, current: currentEvidence, work: managed.workHistory, remaining: evidenceLedger(managed).units,
@@ -18,25 +18,9 @@ Task context (data): ${JSON.stringify({ objective: run.objective, originalUnders
18
18
 
19
19
  }
20
20
 
21
- export function normalizeVerification(value, criteria, { flexible = true } = {}) {
22
- if (typeof value?.passed !== 'boolean' || typeof value.summary !== 'string' || !value.summary.trim() ||
23
- !Array.isArray(value.criteria) || value.criteria.length !== criteria.length || !Array.isArray(value.findings) || value.findings.length > 20) throw new Error('Invalid independent outcome review.');
24
- const rows = value.criteria.map(row => {
25
- if (flexible && row && !row.evidence?.trim?.()) row = { ...row, evidence: typeof row.result === 'string' && row.result.trim() ? row.result : value.summary };
26
- if (!criteria.some(c => c.id === row.id) || typeof row.passed !== 'boolean' || typeof row.evidence !== 'string' || !row.evidence.trim()) throw new Error('Independent review needs criterion evidence.');
27
- return { id: row.id, passed: row.passed, evidence: row.evidence.slice(0, 1000),
28
- evidenceRefs: Array.isArray(row.evidenceRefs) ? row.evidenceRefs.slice(0, 32) : [] };
29
- });
30
- if (new Set(rows.map(row => row.id)).size !== criteria.length || value.findings.some(item => typeof item === 'string' ? !item.trim() : !item || typeof item !== 'object' || typeof item.gap !== 'string' || !item.gap.trim())) throw new Error('Invalid independent review coverage.');
31
- const findings = value.findings.map(item => typeof item === 'string' ? item.slice(0, 1000) : item);
32
- if (value.passed && (rows.some(row => !row.passed) || findings.length)) throw new Error('Contradictory independent review.');
33
- const needsHuman = value.needsHuman === true;
34
- if (needsHuman && (value.passed || findings.length || !rows.some(row => !row.passed) || rows.some(row => !row.passed && criteria.find(c => c.id === row.id)?.check.type !== 'manual'))) throw new Error('Only explicit human criteria may require a human decision.');
35
- if (value.resolutions !== undefined && (!Array.isArray(value.resolutions) || value.resolutions.length > 100)) throw new Error('Invalid finding resolutions.');
36
- return { workUnits: Array.isArray(value.workUnits) ? value.workUnits.slice(0, 192) : [], manifest: value.manifest,
37
- rules: Array.isArray(value.rules) ? value.rules.slice(0, 512).map(row => flexible && row && !row.basis?.trim?.() ? { ...row, basis: value.summary } : row) : [],
38
- resolutions: value.resolutions || [], passed: value.passed, needsHuman, summary: value.summary.slice(0, 1000), criteria: rows, findings };
39
- }
21
+ // Shared normalization preserves the verdict and actual failures without
22
+ // requiring criterion rows, citations, resolutions or other review paperwork.
23
+ export const normalizeVerification = normalizeOutcomeReview;
40
24
 
41
25
  // Recognize only direct file readers. Arbitrary shell/Python commands and echo
42
26
  // are useful investigation but not captured artifact inspection evidence.
@@ -98,42 +82,6 @@ const capturedReadText = item => {
98
82
  return text; // Compatibility with direct plain-text capture fixtures.
99
83
  }
100
84
  };
101
- // JSON sanitization reserializes whitespace before capture. Compare only
102
- // insignificant JSON whitespace, never string values, redactions or missing data.
103
- const jsonWhitespaceTokens = (text, fragment = false) => {
104
- try { JSON.parse(text); } catch {
105
- if (!fragment) return null;
106
- try { JSON.parse(`{${text.replace(/,\s*$/, '')}}`); } catch { return null; }
107
- }
108
- // Preserve each scalar's complete spelling. Character substrings could turn
109
- // a captured 123 into a claimed 1 (or match digits inside a quoted string).
110
- return text.match(/"(?:\\[\s\S]|[^"\\])*"|true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}\[\],:]/g);
111
- };
112
- const matchesJsonExcerpt = (body, part, excerpt) => part !== null
113
- ? body.some((_, index) => part.every((token, offset) => body[index + offset] === token))
114
- : body.some(token => token.startsWith('"') && token.slice(1, -1) === excerpt);
115
- const matchesCapturedExcerpt = (item, excerpt) => {
116
- if (typeof excerpt !== 'string' || !excerpt.trim()) return false;
117
- const captured = capturedReadText(item);
118
- const body = jsonWhitespaceTokens(captured, true), part = jsonWhitespaceTokens(excerpt, true);
119
- if (body !== null) {
120
- // A cut scalar can still parse as JSON (123 cut to 1). Its boundary is unknown.
121
- if (item.bodyObservation?.truncated ?? item.observation?.truncated) return false;
122
- if (matchesJsonExcerpt(body, part, excerpt)) return true;
123
- // Scoped observers wrap each selected JSON field in a bounded excerpt.
124
- // Inspect that controller-created layer without accepting partial values.
125
- const selected = item.source === 'controller_read_only_observer' && Array.isArray(item.recipe?.fields) ? JSON.parse(captured)?.selected : null;
126
- return Array.isArray(selected) && selected.some(row => {
127
- if (row?.found !== true || !item.recipe.fields.includes(row.path) || row.value?.truncated !== false || typeof row.value.text !== 'string') return false;
128
- const field = jsonWhitespaceTokens(row.value.text);
129
- return field !== null && matchesJsonExcerpt(field, part, excerpt);
130
- });
131
- }
132
- // An incomplete JSON document cannot use the literal fallback to certify a
133
- // partial scalar. Acquire a narrower complete read instead.
134
- if (captured.trimStart().startsWith('{') || captured.trimStart().startsWith('[') || /^\s*"(?:\\[\s\S]|[^"\\])*"\s*:/.test(captured)) return false;
135
- return captured.includes(excerpt);
136
- };
137
85
  const capturedDigestFormat = item => item.source === 'controller_file_read' ? 'file-content-sha256-v1' : item.source === 'controller_read_only_observer' ? 'observer-text-sha256-v1' :
138
86
  item.bodyDigestFormat === 'command-output-sha256-v1' && /^[a-f0-9]{64}$/.test(item.bodyDigest) && typeof item.bodyObservation?.text === 'string' ? item.bodyDigestFormat : undefined;
139
87
  const capturedContentDigest = item => item.source === 'controller_file_read' ? item.digest : capturedDigestFormat(item) === 'command-output-sha256-v1' ? item.bodyDigest : createHash('sha256').update(capturedReadText(item)).digest('hex');
@@ -161,6 +109,10 @@ export function assessIndependentInspection(review, { toolEvidence = [], observa
161
109
  }
162
110
 
163
111
  export function certifyReviewManifest(value, managed, toolEvidence, inventory) {
112
+ try { return checkedReviewManifest(value, managed, toolEvidence, inventory); }
113
+ catch { return null; }
114
+ }
115
+ function checkedReviewManifest(value, managed, toolEvidence, inventory) {
164
116
  if (value === undefined) return null; // Legacy reviews keep whole-workspace safety.
165
117
  if (value?.version !== 1 || !Array.isArray(value.exclusions) || value.exclusions.length > 32) throw new Error('Invalid review dependency manifest.');
166
118
  const changedByWorker = (managed.purpose.evidence?.receipts || []).filter(row => row.kind === 'fileChange').map(row => row.observation?.text || '').join('\n');
@@ -0,0 +1,55 @@
1
+ import { evidenceLedger } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
2
+ import { reconcileWorkFrontier, revalidateWorkProposal } from '../codex-plugins/simy/hooks/work-frontier.mjs';
3
+ import { searchEnabled } from '../codex-plugins/simy/hooks/purpose-search.mjs';
4
+ import { taskDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
5
+
6
+ // Enroll the controller's canonical workspace, never a worker-provided lock ID.
7
+ // Read-only diagnosis/verification is the path that resolves observed effects;
8
+ // it must remain possible while the frontier prevents another writer.
9
+ export function recoveryWorkProposal(record) {
10
+ const managed = record.run.managed;
11
+ if (!searchEnabled(managed)) return null;
12
+ const ledger = evidenceLedger(managed);
13
+ const workspace = `workspace:${taskDirectory(record)}`;
14
+ try {
15
+ for (const unit of ledger.units) {
16
+ // Repair pre-migration understanding reads whose checkpoint settled but
17
+ // whose ledger occupancy was never released. Never settle an active or
18
+ // unknown write, or infer acceptance from the read's completion.
19
+ const attempt = managed.executionAttempt;
20
+ const liveAttempt = attempt?.decisionId === unit.id && attempt.phase !== 'checkpointed';
21
+ if (unit.action?.kind === 'read' && unit.status === 'dispatched' && !liveAttempt &&
22
+ managed.workHistory?.some(row => row.decisionId === unit.id && row.status === 'session_understanding' && row.result?.status === 'observed')) {
23
+ unit.status = 'observed';
24
+ }
25
+ if (!unit.action || ['verified', 'superseded', 'superseded_by_user_revision'].includes(unit.status)) continue;
26
+ unit.owner ||= managed.taskId;
27
+ if (unit.id.startsWith('wait:')) unit.waitReason ||= 'required_operation_observation';
28
+ unit.readResources = [...new Set((unit.readResources || []).map(id => id === 'task' ? workspace : id))];
29
+ unit.writeResources = [...new Set((unit.writeResources || []).map(id => id === 'task' ? workspace : id))];
30
+ if (unit.action.kind !== 'read' && !unit.writeResources.includes(workspace)) unit.writeResources.push(workspace);
31
+ }
32
+ // Missing PR readback stays blocked. This boundary never invents CI evidence.
33
+ const proposal = reconcileWorkFrontier(managed, { stopped: ['paused', 'stopped', 'cancelled'].includes(record.run.state) });
34
+ delete managed.frontierIssue;
35
+ return proposal;
36
+ } catch {
37
+ managed.frontierIssue = 'Existing work-unit ledger needs reconciliation before dispatch.';
38
+ return { blocked: true };
39
+ }
40
+ }
41
+
42
+ export function admitRecoveryWork(record, proposal, leaseId, now) {
43
+ if (!proposal) return;
44
+ const managed = record.run.managed;
45
+ const unit = managed.purpose.evidence.units.find(row => row.id === managed.decision.id);
46
+ if (!unit && managed.decision.action.kind === 'read' && ['understanding', 'needs_clarification'].includes(managed.phase)) return;
47
+ if (!unit) throw new Error('Selected work unit is missing.');
48
+ const current = revalidateWorkProposal(managed, proposal, unit.id, {
49
+ stopped: ['paused', 'stopped', 'cancelled'].includes(record.run.state),
50
+ });
51
+ if (!current.eligible) {
52
+ throw new Error(`Work proposal needs reconciliation: ${current.reason || 'not eligible'}`);
53
+ }
54
+ unit.status = 'dispatched'; unit.dispatchedAt = now; unit.attemptId = leaseId;
55
+ }
@@ -13,7 +13,7 @@ export const remoteHostId = alias => `remote-ssh-discovered:${alias}`;
13
13
  const shellQuote = value => "'" + value.replaceAll("'", "'\\''") + "'";
14
14
  const bootstrap = 'import { realpathSync } from "node:fs"; import { dirname, join } from "node:path"; import { pathToFileURL } from "node:url"; const entry = join(dirname(realpathSync(process.argv[1])), "session-supervision", "remote-entry.js"); await import(pathToFileURL(entry).href);';
15
15
  export const REMOTE_HOST_COMMAND = `exec sh -lc ${shellQuote(`exec env SIMY_REMOTE_CHILD=1 SIMY_AUTO_UPDATE=0 node --input-type=module -e ${shellQuote(bootstrap)} "$(command -v simy)"`)}`;
16
- const allowedActions = new Set(['refresh', 'session_statistics', 'session_archive', 'session_restore', 'session_track', 'session_untrack',
16
+ const allowedActions = new Set(['refresh', 'session_set_pinned', 'session_statistics', 'session_archive', 'session_restore', 'session_track', 'session_untrack',
17
17
  'session_monitor', 'session_unmonitor', 'session_enroll', 'session_set_auto_continue', 'session_analyze', 'session_continue', 'session_answer',
18
18
  'session_deadline_decide', 'session_action_refresh', 'session_action_select', 'session_action_unlink',
19
19
  'delivery_pause', 'delivery_stop', 'delivery_reject', 'delivery_resume', 'delivery_replan', 'delivery_approve',
@@ -4,14 +4,15 @@ import { deliveryStop } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
4
4
  // Auto-continue works while nobody is watching, so a person must learn when it
5
5
  // stops. Map a stopped run to a notification kind; the native window owns copy.
6
6
  export function stopNotificationKind(run, now = Date.now()) {
7
- if (!run?.managed?.autoContinueEpoch) return null;
7
+ if (!run?.managed?.autoContinueEpoch && !run?.managed?.repairRequest) return null;
8
8
  // The clock keeps aging while paused; the persisted human decision takes
9
9
  // precedence over a threshold that was reached only after that decision.
10
10
  if (run.state === 'paused') return run.reason === 'Paused by user.' ? null : deliveryStop(run, now) ? 'paused' : null;
11
- if (run.state === 'blocked' || run.state === 'verification_pending') return 'needs_decision';
11
+ if (run.state === 'auto_resuming' && !run.managed.resumeLease?.owner) return 'needs_decision';
12
+ if (run.state === 'blocked' || run.state === 'verification_pending' || run.state === 'reported_done') return 'needs_decision';
12
13
  if (run.state === 'recovery_required' && run.managed.autoRecovery !== true) return 'needs_decision';
13
14
  if (run.state === 'failed' || run.state === 'budget_exhausted') return 'failed';
14
- if (run.state === 'completed' || run.state === 'reported_done') return 'finished';
15
+ if (run.state === 'completed') return 'finished';
15
16
  return null;
16
17
  }
17
18
 
@@ -24,19 +25,18 @@ export function createStopNotifier({ emit, now = Date.now } = {}) {
24
25
  let baseline = true;
25
26
  return {
26
27
  observe(records) {
27
- const present = new Set();
28
28
  for (const record of records) {
29
29
  const run = record?.run, kind = stopNotificationKind(run, now());
30
30
  const key = JSON.stringify([record.provider, record.session, run?.managed?.taskId]);
31
- present.add(key);
32
- if (!kind) { seen.delete(key); continue; }
33
- const id = createHash('sha256').update(JSON.stringify([record.provider, record.session, run.managed.taskId, run.state, run.revision, run.reason])).digest('hex').slice(0, 32);
31
+ if (!kind) continue;
32
+ const id = createHash('sha256').update(JSON.stringify([record.provider, record.session, run.managed.taskId, run.id, kind, run.blocker, run.reason])).digest('hex').slice(0, 32);
34
33
  if (seen.get(key) === id) continue;
35
34
  seen.set(key, id);
36
35
  if (!baseline) emit({ type: 'notify', id, kind, provider: record.provider, session: record.session,
37
36
  objective: String(run.objective || '').slice(0, 200) });
38
37
  }
39
- for (const key of seen.keys()) if (!present.has(key)) seen.delete(key);
38
+ // Temporary provider absence or a diagnostic pass is not a new stop.
39
+ // Retain fingerprints until a materially different outcome is observed.
40
40
  baseline = false;
41
41
  },
42
42
  };
@@ -40,7 +40,7 @@ Prefer the fixed local \`$HOME/.simy/bin/simy\` (or \`$SIMY_HOME/bin/simy\`) whe
40
40
  3. Read \`proof.json\` and require \`outcome.status\` to be \`passed\`.
41
41
  4. If either command fails, SQM is unavailable, or the proof is not passed, report the exact blocker. Do not claim SQM passed or silently bypass the requested check.
42
42
  5. Verify the proof's repository, base, HEAD and working-tree/diff identity and its bound signature with SQM tooling. Use separate artifact paths outside the diff, and rerun after bound inputs change.
43
- 6. Include proof paths, outcome reasons, matched modules, evaluated rules and scenario statuses from \`executions\` in the PR evidence, separating blocking and advisory results. A pass with \`no_applicable_rules\`, zero scenario evaluations or advisory-only checks does not establish feature or historical incident coverage. Scenario statuses are deterministic evaluations, not browser/provider/deployed execution evidence. Retain relevant uncovered incidents and their concrete regression result or gap; do not promote incomplete delivery acceptance.
43
+ 6. Include proof paths, outcome reasons, matched modules, evaluated rules and scenario statuses from \`executions\` in the PR evidence, separating blocking and advisory results. A pass with \`no_applicable_rules\`, zero scenario evaluations or advisory-only checks does not establish feature or historical incident coverage. Scenario statuses are deterministic evaluations, not browser/provider/deployed execution evidence. Retain relevant uncovered incidents and their concrete regression result or gap; do not promote incomplete delivery acceptance. Zero coverage alone is not a Delivery Loop missing_information blocker: investigate rule applicability and available verification, and continue authorized repair or testing. Keep requested SQM acceptance unmet until it is actually satisfied; alternative tests do not substitute for it. Use an information wait only for a specific question that requires the user's answer.
44
44
 
45
45
  Do not replace SQM with an LLM review, and do not claim the check passed without reading the generated proof.
46
46
  Use existing authorization for minimal related repair and recheck; do not request another continuation after ordinary blockers are resolved. LLM judgment is the base, relevant HC-01–HC-102 lenses are supplemental, and explicit stops, irreversible actions and real permissions remain boundaries.
@@ -1,6 +1,6 @@
1
1
  import { isAdvisoryGuidance, advisoryGuidanceDescriptions } from './advisory-guidance.js';
2
2
  import { createHash } from "node:crypto";
3
- import { lstat, readFile } from "node:fs/promises";
3
+ import { lstat, readFile, realpath } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
6
  import { lintKnowledgeModule } from "./reliability.js";
@@ -307,7 +307,9 @@ function activeExceptionPatterns(module) {
307
307
  }
308
308
 
309
309
  function selectPaths(paths, globs) {
310
- return paths.filter((file) => matchesAny(globs, file)).slice(0, MAX_FILES_PER_PATTERN);
310
+ const selected = paths.filter((file) => matchesAny(globs, file));
311
+ if (selected.length > MAX_FILES_PER_PATTERN) throw new Error("SQM inspection scope exceeds file limit; coverage is incomplete.");
312
+ return selected;
311
313
  }
312
314
 
313
315
  function matchesAny(globs, file) {
@@ -365,12 +367,16 @@ async function readText(root, relative) {
365
367
  const resolved = path.resolve(root, relative);
366
368
  if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) throw new Error("SQM path escaped the repository root.");
367
369
  try {
370
+ const actualRoot = await realpath(root);
371
+ const actualFile = await realpath(resolved);
372
+ if (!actualFile.startsWith(`${actualRoot}${path.sep}`)) throw new Error("SQM path escaped the repository root.");
368
373
  const fileStat = await lstat(resolved);
369
- if (!fileStat.isFile() || fileStat.isSymbolicLink()) return "";
370
- const buffer = await readFile(resolved);
371
- if (buffer.length > MAX_FILE_BYTES || buffer.includes(0)) return "";
374
+ if (!fileStat.isFile() || fileStat.isSymbolicLink()) throw new Error("Not a regular file.");
375
+ if (fileStat.size > MAX_FILE_BYTES) throw new Error("Unreadable text scope.");
376
+ const buffer = await readFile(actualFile);
377
+ if (buffer.length > MAX_FILE_BYTES || buffer.includes(0)) throw new Error("Unreadable text scope.");
372
378
  return buffer.toString("utf8");
373
379
  } catch {
374
- return "";
380
+ throw new Error(`SQM could not inspect ${relative}; coverage is incomplete.`);
375
381
  }
376
382
  }
@@ -1,6 +1,7 @@
1
- import { readFile, realpath } from "node:fs/promises";
1
+ import { lstat, readFile, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { CLAUDE_MARKETPLACE, CLAUDE_PLUGIN_SELECTOR } from "./claude-plugin.js";
4
+ import { pluginCacheMatches, repairPluginCache } from "./plugin-cache.js";
4
5
 
5
6
  export async function refreshClaudePlugin({ status, home, claudeHome, resolveExecutable, run }) {
6
7
  if (!status.claude_plugin?.installed) return { outcome: "not_enabled" };
@@ -19,21 +20,34 @@ export async function refreshClaudePlugin({ status, home, claudeHome, resolveExe
19
20
  await realpath(marketplace.path) !== await realpath(status.claude_plugin.marketplace_root)) {
20
21
  throw new Error("Enabled SIMY Claude plugin points to a different marketplace; refresh skipped.");
21
22
  }
22
- const cacheCurrent = async (entry) => {
23
- if (!entry?.installPath || entry.version !== status.claude_plugin.version) return false;
23
+ const cacheLocation = async (entry) => {
24
+ if (!entry?.installPath || entry.version !== status.claude_plugin.version) return null;
24
25
  const cacheRoot = await realpath(path.join(claudeHome, "plugins/cache")).catch(() => null);
25
- if (!cacheRoot) return false;
26
+ if (!cacheRoot) return null;
26
27
  const installedPath = await realpath(entry.installPath).catch(() => null);
27
28
  const relative = installedPath && path.relative(cacheRoot, installedPath);
28
- if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return false;
29
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
30
+ return installedPath;
31
+ };
32
+ const cacheCurrent = async (entry) => {
33
+ const installedPath = await cacheLocation(entry);
34
+ if (!installedPath) return false;
29
35
  try {
30
- return JSON.parse(await readFile(path.join(installedPath, ".claude-plugin/plugin.json"), "utf8")).version === status.claude_plugin.version;
31
- } catch (error) { if (error.code === "ENOENT" || error instanceof SyntaxError) return false; throw error; }
36
+ return JSON.parse(await readFile(path.join(installedPath, ".claude-plugin/plugin.json"), "utf8")).version === status.claude_plugin.version &&
37
+ await pluginCacheMatches(status.claude_plugin.path, installedPath);
38
+ } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code) || error instanceof SyntaxError) return false; throw error; }
32
39
  };
33
40
  if (await cacheCurrent(installed)) return { outcome: "current" };
34
41
  await run(executable, ["plugin", "marketplace", "update", CLAUDE_MARKETPLACE], options);
35
42
  await run(executable, ["plugin", "update", CLAUDE_PLUGIN_SELECTOR, "--scope", "user"], options);
36
- const refreshed = (await list()).find((entry) => entry.id === CLAUDE_PLUGIN_SELECTOR && entry.scope === "user");
43
+ let refreshed = (await list()).find((entry) => entry.id === CLAUDE_PLUGIN_SELECTOR && entry.scope === "user");
44
+ if (refreshed?.enabled && !await cacheCurrent(refreshed)) {
45
+ const location = await cacheLocation(refreshed);
46
+ if (location && !(await lstat(refreshed.installPath)).isSymbolicLink()) {
47
+ await repairPluginCache(status.claude_plugin.path, location);
48
+ refreshed = (await list()).find((entry) => entry.id === CLAUDE_PLUGIN_SELECTOR && entry.scope === "user");
49
+ }
50
+ }
37
51
  if (!refreshed?.enabled || !await cacheCurrent(refreshed)) {
38
52
  throw new Error("Claude Code did not confirm the updated SIMY plugin cache; will retry.");
39
53
  }
@@ -25,6 +25,9 @@ export async function claudePluginFiles() {
25
25
  // Claude workflow without shipping it back into Codex's skill inventory.
26
26
  files.set("skills/delivery-loop/SKILL.md", await readFile(
27
27
  new URL("../delivery-hooks/claude-workflow.md", import.meta.url), "utf8"));
28
+ for (const name of ["loop-kernel.mjs", "loop-kernel/index.mjs", "loop-kernel/policy.json", "loop-kernel/pin.json", "loop-kernel/outcome-review.mjs", "loop-kernel/work-frontier.mjs", "loop-kernel/task-pool.mjs"]) {
29
+ files.set(`hooks/${name}`, await readFile(path.join(SOURCE, "hooks", name), "utf8"));
30
+ }
28
31
  files.set("hooks/delivery-loop.mjs", await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8"));
29
32
  files.set("hooks/recovery-incident.mjs", await readFile(path.join(SOURCE, "hooks/recovery-incident.mjs"), "utf8"));
30
33
  files.set("hooks/minimal-sufficient.mjs", await readFile(path.join(SOURCE, "hooks/minimal-sufficient.mjs"), "utf8"));
@@ -38,6 +41,7 @@ export async function claudePluginFiles() {
38
41
  files.set("hooks/purpose-search.mjs", await readFile(path.join(SOURCE, "hooks/purpose-search.mjs"), "utf8"));
39
42
  files.set("hooks/completion-binding.mjs", await readFile(path.join(SOURCE, "hooks/completion-binding.mjs"), "utf8"));
40
43
  files.set("hooks/consistency-handoff.mjs", await readFile(path.join(SOURCE, "hooks/consistency-handoff.mjs"), "utf8"));
44
+ files.set("hooks/purpose-completion-contract.mjs", await readFile(path.join(SOURCE, "hooks/purpose-completion-contract.mjs"), "utf8"));
41
45
  files.set("hooks/purpose-evidence.mjs", await readFile(path.join(SOURCE, "hooks/purpose-evidence.mjs"), "utf8"));
42
46
  files.set("hooks/user-experience.mjs", await readFile(path.join(SOURCE, "hooks/user-experience.mjs"), "utf8"));
43
47
  files.set("hooks/result-cycle.mjs", await readFile(path.join(SOURCE, "hooks/result-cycle.mjs"), "utf8"));
@@ -8,8 +8,8 @@ import path from 'node:path';
8
8
  // This catalog is shipped with the CLI; a repository cannot supply shell commands.
9
9
  const profiles = {
10
10
  'delivery-loop': [
11
- { id: 'stop-investigation-regressions', files: ['test/result-cycle.test.js', 'test/stop-investigation-regression.test.js', 'test/acceptance-reads.test.js', 'test/browser-recovery.test.js', 'test/browser-cdp.test.js', 'test/interaction-task.test.js', 'test/native-interaction.test.js'] },
12
- { id: 'autonomous-recovery', files: ['test/autonomous-recovery.test.js', 'test/scene-reasoning.test.js', 'test/supervisor-watchdog.test.js', 'test/supervisor-handover.test.js'] },
11
+ { id: 'stop-investigation-regressions', files: ['test/result-cycle.test.js', 'test/delivery-result-followthrough.test.js', 'test/corrective-followup.test.js', 'test/stop-investigation-regression.test.js', 'test/acceptance-reads.test.js', 'test/browser-recovery.test.js', 'test/browser-cdp.test.js', 'test/interaction-task.test.js', 'test/native-interaction.test.js'] },
12
+ { id: 'autonomous-recovery', files: ['test/autonomous-recovery.test.js', 'test/scene-reasoning.test.js', 'test/supervisor-watchdog.test.js', 'test/supervisor-handover.test.js', 'test/work-frontier.test.js', 'test/legacy-hook-routing.test.js', 'test/procedure-free-review.test.js'] },
13
13
  { id: 'perspective-memory', files: ['test/perspective-memory.test.js'] },
14
14
  { id: 'optional-reasoning', files: ['test/purpose-selective-rules.test.js', 'test/purpose-evidence.test.js', 'test/purpose-rule-review.test.js'] },
15
15
  // This connected controller group now contains over 200 cases. Keep a
@@ -0,0 +1,67 @@
1
+ import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+
5
+ // A matching manifest version does not prove that the cached hooks can run.
6
+ // Compare every shipped file; provider-owned cache metadata may coexist.
7
+ export async function pluginCacheMatches(source, cache) {
8
+ async function visit(sourceDirectory, cacheDirectory) {
9
+ const target = await lstat(cacheDirectory);
10
+ if (!target.isDirectory() || target.isSymbolicLink()) return false;
11
+ for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) {
12
+ const from = path.join(sourceDirectory, entry.name);
13
+ const to = path.join(cacheDirectory, entry.name);
14
+ if (entry.isDirectory()) {
15
+ if (!await visit(from, to)) return false;
16
+ } else {
17
+ if (!entry.isFile()) return false;
18
+ const cached = await lstat(to);
19
+ if (!cached.isFile() || cached.isSymbolicLink()) return false;
20
+ if (!(await readFile(from)).equals(await readFile(to))) return false;
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+ try { return await visit(source, cache); }
26
+ catch (error) {
27
+ if (['ENOENT', 'ENOTDIR'].includes(error.code)) return false;
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ // Some providers skip recopying a cache whose version already matches. Restore
33
+ // only the shipped files of an already enabled, source-verified cache. Never
34
+ // follow links or remove provider-owned metadata. Each file replacement is atomic.
35
+ export async function repairPluginCache(source, cache) {
36
+ const directory = async target => {
37
+ const info = await lstat(target);
38
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('Plugin cache repair requires real directories.');
39
+ };
40
+ await directory(source);
41
+ await directory(cache);
42
+ async function visit(from, to) {
43
+ await directory(from);
44
+ await directory(to);
45
+ for (const entry of await readdir(from, { withFileTypes: true })) {
46
+ const original = path.join(from, entry.name), target = path.join(to, entry.name);
47
+ if (entry.isSymbolicLink() || !entry.isDirectory() && !entry.isFile()) throw new Error('Plugin source contains an unsupported file.');
48
+ let existing;
49
+ try { existing = await lstat(target); } catch (error) { if (error.code !== 'ENOENT') throw error; }
50
+ if (existing?.isSymbolicLink()) throw new Error('Plugin cache repair refuses symbolic links.');
51
+ if (entry.isDirectory()) {
52
+ if (!existing) await mkdir(target);
53
+ await visit(original, target);
54
+ } else {
55
+ if (existing && !existing.isFile()) throw new Error('Plugin cache repair requires regular files.');
56
+ const contents = await readFile(original);
57
+ if (existing && contents.equals(await readFile(target))) continue;
58
+ const temporary = path.join(to, `.simy-repair-${randomUUID()}`);
59
+ try {
60
+ await writeFile(temporary, contents, { flag: 'wx', mode: (await lstat(original)).mode & 0o777 });
61
+ await rename(temporary, target);
62
+ } finally { await rm(temporary, { force: true }); }
63
+ }
64
+ }
65
+ }
66
+ await visit(source, cache);
67
+ }
package/src/sqm/proof.js CHANGED
@@ -75,6 +75,7 @@ export function buildProof({ organizationId, repository, bundle, minCheck, evalu
75
75
  enforcement: 'blocking', rule_results: [], scenarios: [], state_models: [], stressors: [],
76
76
  local_tests: localTests,
77
77
  }] : [])],
78
+ inspection_scope: sqmInspectionScope(evaluation.modules),
78
79
  findings,
79
80
  outcome: {
80
81
  status,
@@ -159,6 +160,30 @@ function taggedDigest(value) {
159
160
  return `sha256:${createHash("sha256").update(canonicalize(value), "utf8").digest("hex")}`;
160
161
  }
161
162
 
163
+ // This scope is part of the signed proof. Static scenarios never become UI or
164
+ // human acceptance evidence, even when the legacy enforcement outcome is passed.
165
+ export function sqmInspectionScope(modules) {
166
+ const checks = modules.flatMap(module => (module.rule_results || []).map(rule => ({
167
+ module_id: module.module_id, rule_id: rule.rule_id, status: rule.status,
168
+ })));
169
+ return { evidence_kind: "static_check", checks, checked_count: checks.length,
170
+ status: checks.length === 0 ? "unconfirmed" : checks.every(check => check.status === "passed") ? "verified" : "unmet",
171
+ scenario_evaluations: modules.reduce((count, module) => count + (module.scenarios || []).length, 0),
172
+ scenario_generation: "not_observed", real_ui_test: "not_observed", human_acceptance: "not_observed" };
173
+ }
174
+
175
+ export function verifyProofScope(proof, evidence, { repository, requiredChecks, trustedKeyId } = {}) {
176
+ const verified = verifySignedEvidence(proof, evidence);
177
+ if (!trustedKeyId || verified.key_id !== trustedKeyId) throw new Error("SQM signer is not the enrolled trusted key.");
178
+ if (!repository?.head_commit || !sameRepositoryIdentity(proof.repository_identity, repositoryIdentity(repository))) throw new Error("SQM proof revision is stale.");
179
+ const scope = sqmInspectionScope(proof.executions);
180
+ if (canonicalize(scope) !== canonicalize(proof.inspection_scope || null)) throw new Error("SQM inspection scope is missing or inconsistent.");
181
+ if (!Array.isArray(requiredChecks) || !requiredChecks.length || scope.status !== "verified" ||
182
+ requiredChecks.some(required => !scope.checks.some(check => check.module_id === required.module_id && check.rule_id === required.rule_id && check.status === "passed"))) throw new Error("SQM required inspection coverage is incomplete.");
183
+ if (proof.outcome.status !== "passed") throw new Error("SQM enforcement did not pass.");
184
+ return { ...verified, evidence_kind: "static_check", checked_count: scope.checked_count };
185
+ }
186
+
162
187
  export function localTestsFromProof(proof) {
163
188
  return proof?.executions?.find(row => row.kind === 'local_test_profile')?.local_tests || null;
164
189
  }
package/src/sqm/report.js CHANGED
@@ -25,6 +25,7 @@ export function formatSqmCoverage(proof) {
25
25
  const semantic = rules.filter(rule => rule.assessment).map(rule => rule.assessment);
26
26
  if (semantic.length) lines.push(`Semantic assessments: ${semantic.filter(result => result.executed).length} executed · ${semantic.filter(result => result.outcome === 'scored').length} scored · ${semantic.filter(result => result.outcome !== 'scored').length} inconclusive. Local model judgments are advisory and do not establish independent acceptance.`);
27
27
  lines.push("Scenario evaluations are deterministic checks, not browser/provider/deployed journey evidence. Record incident coverage and delivery acceptance separately.");
28
+ if (proof.inspection_scope) lines.push(`Artifact/UI/human acceptance: not established by this static proof; inspection status=${proof.inspection_scope.status}.`);
28
29
  return lines.join("\n");
29
30
  }
30
31
 
@@ -1,5 +1,3 @@
1
- import { createInterface } from "node:readline/promises";
2
-
3
1
  import { inspectSqmAgentSkill, installSqmAgentSkill } from "./agent-skill.js";
4
2
 
5
3
  const INSTALL_FLAG = "--install-workflows";
@@ -9,11 +7,8 @@ export async function prepareDaemonWorkflows({
9
7
  argv,
10
8
  daemon,
11
9
  daemonChild = process.env.SIMY_DAEMON_CHILD === "1",
12
- stdinIsTTY = process.stdin.isTTY === true,
13
- stdoutIsTTY = process.stdout.isTTY === true,
14
10
  inspect = inspectSqmAgentSkill,
15
11
  install = installSqmAgentSkill,
16
- confirm = confirmWorkflowInstallation,
17
12
  logger = console.log,
18
13
  errorLogger = console.error,
19
14
  } = {}) {
@@ -42,23 +37,12 @@ export async function prepareDaemonWorkflows({
42
37
  return { outcome: "update_pending", status };
43
38
  }
44
39
 
45
- if (!installRequested && (!stdinIsTTY || !stdoutIsTTY)) {
46
- logger(
47
- "SIMY Codex/Claude workflows are missing or outdated. Re-run in a terminal with --install-workflows to install them.",
48
- );
49
- return { outcome: "confirmation_unavailable", status };
50
- }
51
-
52
- const accepted = installRequested || (await confirm({ status }));
53
- if (!accepted) {
54
- logger("SIMY workflow installation skipped. The daemon will still start.");
55
- return { outcome: "declined", status };
56
- }
57
-
40
+ // Source preparation is CLI startup work, including unattended service starts.
41
+ // Provider plugin enablement and hook trust remain provider-owned controls.
58
42
  try {
59
43
  const installed = await install();
60
44
  logger(
61
- "SIMY workflows installed. In Codex, restart, then find and install SIMY in Plugins for SQM and PR review. For the Codex Agentic Loop, run simy in a terminal and select Codex. In Claude Code, add the SIMY marketplace and install simy@simy-local before using /simy:sqm-loop or /simy:pr-review. For Claude Code Delivery Loop, review and enable SIMY hooks in /hooks, then request /simy:delivery-loop for each task. Plugin installation does not grant hook trust.",
45
+ "SIMY workflows installed. In Codex, restart, then find and install SIMY in Plugins. In Claude Code, add the SIMY marketplace and install simy@simy-local before using /simy:sqm-loop or /simy:pr-review. For Delivery Loop, review and enable SIMY hooks in /hooks. Delivery Loop then starts automatically on user prompts, without per-task commands. Plugin installation does not grant hook trust.",
62
46
  );
63
47
  if (installed.claude_plugin?.marketplace_root) {
64
48
  logger(`Claude Code: claude plugin marketplace add ${JSON.stringify(installed.claude_plugin.marketplace_root)}`);
@@ -71,26 +55,6 @@ export async function prepareDaemonWorkflows({
71
55
  }
72
56
  }
73
57
 
74
- export async function confirmWorkflowInstallation({
75
- input = process.stdin,
76
- output = process.stdout,
77
- } = {}) {
78
- output.write(
79
- [
80
- "SIMY can install or update its Codex and Claude Code plugin sources and opt-in SQM skills.",
81
- "Existing unrelated plugins and hooks will be preserved.",
82
- "",
83
- ].join("\n"),
84
- );
85
- const prompt = createInterface({ input, output });
86
- try {
87
- const answer = await prompt.question("Install SIMY workflows now? [y/N] ");
88
- return /^(y|yes)$/i.test(answer.trim());
89
- } finally {
90
- prompt.close();
91
- }
92
- }
93
-
94
58
  function message(error) {
95
59
  return error instanceof Error ? error.message : String(error);
96
60
  }