@awak-app/simy-cli 0.7.15 → 0.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +225 -98
  2. package/package.json +5 -3
  3. package/src/agent.js +129 -7
  4. package/src/backend-executable.js +7 -6
  5. package/src/bounded-local-task-subtask-pool.js +32 -6
  6. package/src/bounded-local-task-subtasks-contract.js +1 -1
  7. package/src/cli-contract.js +1 -1
  8. package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
  9. package/src/codex-plugins/simy/hooks/README.md +8 -3
  10. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +302 -78
  11. package/src/codex-plugins/simy/hooks/hooks.json +1 -1
  12. package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
  13. package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
  14. package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
  15. package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
  16. package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
  17. package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
  18. package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
  19. package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
  20. package/src/codex-plugins/simy/hooks/outcome-progress.mjs +16 -5
  21. package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
  22. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
  23. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +28 -38
  24. package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +1 -2
  25. package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +5 -63
  26. package/src/codex-plugins/simy/hooks/purpose-search.mjs +21 -4
  27. package/src/codex-plugins/simy/hooks/result-cycle.mjs +166 -0
  28. package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +98 -12
  29. package/src/codex-plugins/simy/hooks/user-experience.mjs +26 -0
  30. package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
  31. package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
  32. package/src/coding-run-recovery.js +4 -2
  33. package/src/delivery-hooks/claude-workflow.md +10 -7
  34. package/src/delivery-hooks/command.js +76 -10
  35. package/src/delivery-hooks/consistency-monitor.js +20 -4
  36. package/src/delivery-hooks/consult.js +60 -0
  37. package/src/delivery-hooks/diagnosis.js +2 -2
  38. package/src/delivery-hooks/legacy-hook-routing.js +46 -8
  39. package/src/delivery-hooks/local-runtime.js +81 -1
  40. package/src/delivery-hooks/local-supervisor.js +13 -4
  41. package/src/delivery-hooks/repair-continuation.js +80 -0
  42. package/src/delivery-hooks/repair.js +164 -0
  43. package/src/delivery-hooks/supervisor-handover.js +84 -0
  44. package/src/delivery-hooks/supervisor-owner.js +3 -2
  45. package/src/delivery-hooks/supervisor-service.js +38 -6
  46. package/src/delivery-hooks/supervisor-watchdog.js +15 -4
  47. package/src/delivery-hooks/supervisor.js +151 -18
  48. package/src/desktop-access.js +2 -1
  49. package/src/desktop-host.js +35 -2
  50. package/src/durable-local-task-worker.js +372 -58
  51. package/src/execution-capability-contract.js +15 -1
  52. package/src/index.js +3 -3
  53. package/src/local-photos-read.swift +33 -0
  54. package/src/local-photos.js +47 -0
  55. package/src/local-task-attachment-store.js +31 -0
  56. package/src/local-task-destination.js +174 -0
  57. package/src/local-task.js +119 -7
  58. package/src/native-delegation-contract.js +68 -0
  59. package/src/native-provider-identity.js +60 -0
  60. package/src/orchestrator/audit.js +24 -323
  61. package/src/orchestrator/budget.js +22 -11
  62. package/src/orchestrator/charter-validation.js +3 -41
  63. package/src/orchestrator/checklist-continuation.js +2 -14
  64. package/src/orchestrator/completion-contract.js +4 -49
  65. package/src/orchestrator/contract.js +5 -9
  66. package/src/orchestrator/delivery/WORKFLOW.md +30 -710
  67. package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -216
  68. package/src/orchestrator/delivery/references/change-boundary-matrix.md +4 -103
  69. package/src/orchestrator/delivery/references/chrome-user-verification.md +5 -133
  70. package/src/orchestrator/delivery/references/cross-repository-control.md +4 -74
  71. package/src/orchestrator/delivery/references/delivery-levels.md +3 -117
  72. package/src/orchestrator/delivery/references/local-sqm-evidence.md +1 -1
  73. package/src/orchestrator/delivery/references/minimum-completion.md +4 -144
  74. package/src/orchestrator/delivery/references/pr-closure.md +4 -200
  75. package/src/orchestrator/delivery/references/shared-system-specifications.md +4 -161
  76. package/src/orchestrator/delivery/references/state-and-receipts.md +4 -245
  77. package/src/orchestrator/delivery/references/state-model.md +4 -185
  78. package/src/orchestrator/delivery/references/ux-specification.md +4 -95
  79. package/src/orchestrator/delivery-level.js +2 -10
  80. package/src/orchestrator/evidence.js +2 -3
  81. package/src/orchestrator/independent-audit.js +15 -164
  82. package/src/orchestrator/instruction.js +19 -254
  83. package/src/orchestrator/loop.js +35 -95
  84. package/src/orchestrator/outcome-monitor.js +21 -0
  85. package/src/orchestrator/pr-closure.js +2 -16
  86. package/src/orchestrator/recovery.js +32 -2
  87. package/src/orchestrator/ux-specification.js +3 -12
  88. package/src/provider-delegation.js +336 -0
  89. package/src/runner.js +5 -2
  90. package/src/session-supervision/acceptance-reads.js +34 -0
  91. package/src/session-supervision/artifact-observer.js +138 -0
  92. package/src/session-supervision/browser-cdp.js +59 -0
  93. package/src/session-supervision/browser-recovery.js +97 -0
  94. package/src/session-supervision/codex-app-server.js +61 -22
  95. package/src/session-supervision/completion-artifacts.js +44 -0
  96. package/src/session-supervision/consistency-auditor.js +8 -11
  97. package/src/session-supervision/continuation-analysis.js +47 -28
  98. package/src/session-supervision/final-response-recovery.js +5 -0
  99. package/src/session-supervision/index.js +22 -5
  100. package/src/session-supervision/interaction-proxy.js +250 -0
  101. package/src/session-supervision/interaction-task.js +189 -0
  102. package/src/session-supervision/native-interaction.js +102 -0
  103. package/src/session-supervision/provider-failure.js +40 -0
  104. package/src/session-supervision/purpose-instruction.js +14 -14
  105. package/src/session-supervision/purpose-recovery.js +32 -12
  106. package/src/session-supervision/purpose-verification.js +50 -216
  107. package/src/session-supervision/recovery-frontier.js +55 -0
  108. package/src/session-supervision/stop-notifications.js +8 -8
  109. package/src/sqm/agent-skill.js +1 -1
  110. package/src/sqm/checkers.js +12 -6
  111. package/src/sqm/claude-plugin-update.js +22 -8
  112. package/src/sqm/claude-plugin.js +6 -0
  113. package/src/sqm/local-tests.js +2 -1
  114. package/src/sqm/plugin-cache.js +67 -0
  115. package/src/sqm/proof.js +25 -0
  116. package/src/sqm/report.js +1 -0
  117. package/src/sqm/workflow-onboarding.js +3 -39
  118. package/src/sqm/workflow-update.js +17 -6
  119. package/src/startup-preflight.js +2 -20
  120. package/src/team-status-fence-store.js +32 -0
  121. package/src/team-threads.js +457 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "SIMY connection reminders, opt-in Delivery Loop lifecycle tracking, and development progress reporting. Trusting hooks does not enable automatic continuation for a task.",
2
+ "description": "SIMY connection reminders, automatic Delivery Loop start on user prompts, lifecycle tracking, and development progress reporting. Explicit project/session opt-outs, stops and provider consent are preserved.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
@@ -0,0 +1,136 @@
1
+ [
2
+ {
3
+ "name": "initial",
4
+ "facts": {},
5
+ "action": "execute"
6
+ },
7
+ {
8
+ "name": "self_evaluation",
9
+ "facts": {
10
+ "completion_claim": true,
11
+ "remaining_work": true
12
+ },
13
+ "action": "repair"
14
+ },
15
+ {
16
+ "name": "zero_coverage",
17
+ "facts": {
18
+ "completion_claim": true
19
+ },
20
+ "action": "verify"
21
+ },
22
+ {
23
+ "name": "failed_criterion",
24
+ "facts": {
25
+ "completion_claim": true,
26
+ "review_satisfied": true,
27
+ "acceptance_failed": true
28
+ },
29
+ "action": "repair"
30
+ },
31
+ {
32
+ "name": "whole_outcome",
33
+ "facts": {
34
+ "completion_claim": true,
35
+ "review_satisfied": true
36
+ },
37
+ "action": "accept"
38
+ },
39
+ {
40
+ "name": "remaining_beats_pass",
41
+ "facts": {
42
+ "completion_claim": true,
43
+ "review_satisfied": true,
44
+ "remaining_work": true
45
+ },
46
+ "action": "repair"
47
+ },
48
+ {
49
+ "name": "user_stop",
50
+ "facts": {
51
+ "halted": true,
52
+ "remaining_work": true
53
+ },
54
+ "action": "stop"
55
+ },
56
+ {
57
+ "name": "approval",
58
+ "facts": {
59
+ "boundary": true,
60
+ "remaining_work": true
61
+ },
62
+ "action": "wait"
63
+ },
64
+ {
65
+ "name": "running_owner",
66
+ "facts": {
67
+ "owned": true,
68
+ "remaining_work": true
69
+ },
70
+ "action": "observe"
71
+ },
72
+ {
73
+ "name": "unknown_write",
74
+ "facts": {
75
+ "uncertain_effects": true,
76
+ "remaining_work": true
77
+ },
78
+ "action": "reconcile"
79
+ },
80
+ {
81
+ "name": "budget",
82
+ "facts": {
83
+ "budget_available": false,
84
+ "remaining_work": true
85
+ },
86
+ "action": "exhausted"
87
+ },
88
+ {
89
+ "name": "unknown_write_at_budget",
90
+ "facts": {
91
+ "budget_available": false,
92
+ "uncertain_effects": true
93
+ },
94
+ "action": "exhausted"
95
+ },
96
+ {
97
+ "name": "no_claim_no_accept",
98
+ "facts": {
99
+ "review_satisfied": true
100
+ },
101
+ "action": "execute"
102
+ },
103
+ {
104
+ "name": "review_unavailable_continues",
105
+ "facts": {
106
+ "completion_claim": true,
107
+ "review_unavailable": true
108
+ },
109
+ "action": "execute"
110
+ },
111
+ {
112
+ "name": "zero_rules_with_actual_outcome",
113
+ "facts": {
114
+ "completion_claim": true,
115
+ "review_satisfied": true,
116
+ "review_unavailable": true
117
+ },
118
+ "action": "accept"
119
+ },
120
+ {
121
+ "name": "failed_outcome_with_review_down",
122
+ "facts": {
123
+ "review_unavailable": true,
124
+ "acceptance_failed": true
125
+ },
126
+ "action": "repair"
127
+ },
128
+ {
129
+ "name": "stop_with_review_down",
130
+ "facts": {
131
+ "review_unavailable": true,
132
+ "halted": true
133
+ },
134
+ "action": "stop"
135
+ }
136
+ ]
@@ -0,0 +1,42 @@
1
+ // Canonical JavaScript interpreter owned and tested in simy-crew. The host retains its
2
+ // state writer, receipts, leases, budgets and permission fences. Never feed
3
+ // model-supplied facts here without the host's existing validation.
4
+ import { readFileSync } from 'node:fs';
5
+ import { createHash } from 'node:crypto';
6
+
7
+ const bytes = readFileSync(new URL('./policy.json', import.meta.url));
8
+ const policy = JSON.parse(bytes);
9
+ export const POLICY_DIGEST = createHash('sha256').update(bytes).digest('hex');
10
+ export const POLICY_VERSION = policy.version;
11
+
12
+ export function decideLoop(facts = {}) {
13
+ if (!facts || Array.isArray(facts) || typeof facts !== 'object' ||
14
+ Object.entries(facts).some(([key, value]) => !Object.hasOwn(policy.facts, key) || typeof value !== 'boolean')) {
15
+ throw new Error('Loop kernel accepts only known boolean host facts');
16
+ }
17
+ const current = { ...policy.facts, ...facts };
18
+ const rule = policy.rules.find(row => Object.entries(row.when).every(([key, value]) => current[key] === value));
19
+ if (!rule) throw new Error('Loop policy has no applicable rule');
20
+ return { version: POLICY_VERSION, policy_digest: POLICY_DIGEST,
21
+ reason: rule.id, action: rule.action, role: rule.role };
22
+ }
23
+
24
+ export function agentRole(name) {
25
+ if (!Object.hasOwn(policy.roles, name)) throw new Error('Unknown loop agent role');
26
+ return structuredClone(policy.roles[name]);
27
+ }
28
+
29
+ // Host enrollment is separate from authority: explicit strict/balanced policy
30
+ // survives, but a stale Codex flag cannot select the retired controller mode.
31
+ export function sessionLoopOptions(options) {
32
+ const defaults = Object.hasOwn(policy.session_defaults, options?.provider) ? policy.session_defaults[options.provider] : null;
33
+ if (!defaults) throw new Error('Unsupported loop session provider');
34
+ return { ...options,
35
+ purposeDriven: options.provider === 'codex' ? defaults.purposeDriven : (options.purposeDriven ?? defaults.purposeDriven),
36
+ purposeVersion: defaults.purposeVersion,
37
+ autonomyPolicy: options.autonomyPolicy ?? defaults.autonomyPolicy };
38
+ }
39
+
40
+ export { normalizeOutcomeReview } from './outcome-review.mjs';
41
+ export { compactWorkHistory } from './work-frontier.mjs';
42
+ export { createTaskPool } from './task-pool.mjs';
@@ -0,0 +1,31 @@
1
+ // Outcome assessment is control input; optional audit rows are not an admission gate.
2
+ // This does not establish provenance, permissions, freshness or actual inspection.
3
+ const list = value => Array.isArray(value) ? value : [];
4
+ const text = value => typeof value === 'string' ? value.trim().slice(0, 1000) : '';
5
+ export function normalizeOutcomeReview(value, criteria = []) {
6
+ value = value && typeof value === 'object' ? value : {};
7
+ const summary = text(value.summary) || 'Inspect the requested outcome using available results and continue useful work.';
8
+ const known = new Map(criteria.map(row => [row.id, row]));
9
+ const rows = new Map();
10
+ for (const row of list(value.criteria)) {
11
+ if (!row || !known.has(row.id) || typeof row.passed !== 'boolean') continue;
12
+ // Duplicate/partial forms cannot erase an explicitly observed failure.
13
+ if (rows.has(row.id) && rows.get(row.id).passed === false) continue;
14
+ rows.set(row.id, { id: row.id, passed: row.passed,
15
+ evidence: text(row.result) || text(row.evidence) || summary,
16
+ evidenceRefs: list(row.evidenceRefs).filter(link => link && typeof link === 'object').slice(0, 32) });
17
+ }
18
+ const findings = (typeof value.findings === 'string' ? [value.findings] : list(value.findings))
19
+ .filter(row => text(typeof row === 'string' ? row : row?.gap))
20
+ .map(row => typeof row === 'string' ? text(row) : { ...row, gap: text(row.gap) });
21
+ const failed = [...rows.values()].filter(row => !row.passed);
22
+ const passed = value.passed === true && !failed.length && !findings.length;
23
+ const needsHuman = value.needsHuman === true && !passed && !findings.length && failed.length > 0 &&
24
+ failed.every(row => known.get(row.id).check?.type === 'manual') &&
25
+ criteria.filter(row => row.check?.type !== 'manual').every(row => rows.get(row.id)?.passed === true);
26
+ return { passed, needsHuman, summary, criteria: [...rows.values()], findings,
27
+ workUnits: list(value.workUnits).filter(row => row && typeof row.id === 'string').slice(0, 192),
28
+ resolutions: list(value.resolutions).filter(row => row && typeof row.id === 'string').slice(0, 100),
29
+ rules: list(value.rules).filter(row => row && typeof row.id === 'string').slice(0, 512)
30
+ .map(row => ({ ...row, basis: text(row.basis) || summary })), manifest: value.manifest };
31
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "simy-loop-kernel.v1",
3
+ "sha256": "4630215d85db935a57618e8a45639f8d95d4dc725046896968faab0d3f56ae15",
4
+ "source_repository": "SpecialistDoctors-Inc/simy-crew",
5
+ "source_path": "app/simy_crew/loop_kernel/policy.json"
6
+ }
@@ -0,0 +1,141 @@
1
+ {
2
+ "version": "simy-loop-kernel.v1",
3
+ "facts": {
4
+ "halted": false,
5
+ "boundary": false,
6
+ "owned": false,
7
+ "budget_available": true,
8
+ "uncertain_effects": false,
9
+ "remaining_work": false,
10
+ "acceptance_failed": false,
11
+ "completion_claim": false,
12
+ "review_satisfied": false,
13
+ "review_unavailable": false
14
+ },
15
+ "rules": [
16
+ {
17
+ "id": "explicit_stop",
18
+ "when": {
19
+ "halted": true
20
+ },
21
+ "action": "stop",
22
+ "role": "controller"
23
+ },
24
+ {
25
+ "id": "native_boundary",
26
+ "when": {
27
+ "boundary": true
28
+ },
29
+ "action": "wait",
30
+ "role": "controller"
31
+ },
32
+ {
33
+ "id": "existing_owner",
34
+ "when": {
35
+ "owned": true
36
+ },
37
+ "action": "observe",
38
+ "role": "controller"
39
+ },
40
+ {
41
+ "id": "budget_limit",
42
+ "when": {
43
+ "budget_available": false
44
+ },
45
+ "action": "exhausted",
46
+ "role": "controller"
47
+ },
48
+ {
49
+ "id": "reconcile_before_retry",
50
+ "when": {
51
+ "uncertain_effects": true
52
+ },
53
+ "action": "reconcile",
54
+ "role": "verifier"
55
+ },
56
+ {
57
+ "id": "unfinished_outcome",
58
+ "when": {
59
+ "remaining_work": true
60
+ },
61
+ "action": "repair",
62
+ "role": "worker"
63
+ },
64
+ {
65
+ "id": "failed_acceptance",
66
+ "when": {
67
+ "acceptance_failed": true
68
+ },
69
+ "action": "repair",
70
+ "role": "worker"
71
+ },
72
+ {
73
+ "id": "continue_without_review_service",
74
+ "when": {
75
+ "review_unavailable": true,
76
+ "review_satisfied": false
77
+ },
78
+ "action": "execute",
79
+ "role": "worker"
80
+ },
81
+ {
82
+ "id": "check_completion_claim",
83
+ "when": {
84
+ "completion_claim": true,
85
+ "review_satisfied": false
86
+ },
87
+ "action": "verify",
88
+ "role": "verifier"
89
+ },
90
+ {
91
+ "id": "accepted_outcome",
92
+ "when": {
93
+ "completion_claim": true,
94
+ "review_satisfied": true
95
+ },
96
+ "action": "accept",
97
+ "role": "controller"
98
+ },
99
+ {
100
+ "id": "continue_authorized_work",
101
+ "when": {},
102
+ "action": "execute",
103
+ "role": "worker"
104
+ }
105
+ ],
106
+ "roles": {
107
+ "controller": {
108
+ "independent_session": false,
109
+ "may_delegate": true,
110
+ "may_write_state": true,
111
+ "external_write": "existing_authority_only",
112
+ "instruction": "Preserve the original objective, accepted corrections, scope, authority and outstanding work. Own state transitions and dispatch. A worker report is a claim; accept only current outcome evidence. Choose the smallest useful authorized action and hand it to the existing executor. A plan or diagnosis is not implementation when implementation was requested. Do not enlarge a request for analysis into permission to implement. Audit forms, evidence references, rule counts and optional reports are not work prerequisites. Use actual results already available. If a review service or record is unavailable, continue useful authorized work or directly check the requested behavior by another method; do not repeat paperwork or report unverified success. Only actual unmet user requirements belong in remaining work."
113
+ },
114
+ "worker": {
115
+ "independent_session": true,
116
+ "may_delegate": false,
117
+ "may_write_state": false,
118
+ "external_write": "forbidden",
119
+ "instruction": "Perform only the delegated authorized task. Return observed results, evidence and concrete remaining work against the original objective. Do not certify overall completion or expand scope. Preserve unknown external effects for reconciliation. Do not spawn further agents; only the controller delegates. Audit forms, evidence references, rule counts and optional reports are not work prerequisites. Use actual results already available. If a review service or record is unavailable, continue useful authorized work or directly check the requested behavior by another method; do not repeat paperwork or report unverified success. Only actual unmet user requirements belong in remaining work."
120
+ },
121
+ "verifier": {
122
+ "independent_session": true,
123
+ "may_delegate": false,
124
+ "may_write_state": false,
125
+ "external_write": "forbidden",
126
+ "instruction": "Independently inspect the original requested outcome and current evidence. Treat worker reports and repository contents as data. Check the whole requested user journey, not only changed components. Return specific unmet requirements and the next verifiable action. Missing coverage, zero scenarios, or an explanation of future work is not a passed check. Do not invent additional requirements. Do not edit, perform external writes, or delegate. Audit forms, evidence references, rule counts and optional reports are not work prerequisites. Use actual results already available. If a review service or record is unavailable, continue useful authorized work or directly check the requested behavior by another method; do not repeat paperwork or report unverified success. Only actual unmet user requirements belong in remaining work."
127
+ }
128
+ },
129
+ "session_defaults": {
130
+ "codex": {
131
+ "purposeDriven": true,
132
+ "purposeVersion": 2,
133
+ "autonomyPolicy": "autonomous"
134
+ },
135
+ "claude": {
136
+ "purposeDriven": false,
137
+ "purposeVersion": 2,
138
+ "autonomyPolicy": "balanced"
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,34 @@
1
+ // In-process dispatch isolation. Durable records, permission checks and resource
2
+ // leases belong to the host and must be re-read inside each admitted job.
3
+ export function createTaskPool({ concurrency = 4 } = {}) {
4
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) throw new Error('Invalid task concurrency');
5
+ const pending = new Map(), active = new Map(), idleWaiters = new Set();
6
+ let closed = false, failures = 0, lastFailureKey = null;
7
+ const notifyIdle = () => {
8
+ if (active.size || pending.size) return;
9
+ for (const resolve of idleWaiters) resolve();
10
+ idleWaiters.clear();
11
+ };
12
+ const pump = () => {
13
+ while (!closed && active.size < concurrency && pending.size) {
14
+ const [key, job] = pending.entries().next().value;
15
+ pending.delete(key);
16
+ const result = Promise.resolve().then(job).catch(() => {
17
+ failures++; lastFailureKey = key;
18
+ }).finally(() => { active.delete(key); pump(); notifyIdle(); });
19
+ active.set(key, result);
20
+ }
21
+ notifyIdle();
22
+ };
23
+ const idle = () => active.size || pending.size ? new Promise(resolve => idleWaiters.add(resolve)) : Promise.resolve();
24
+ return {
25
+ offer(key, job) {
26
+ if (closed || active.has(key)) return false;
27
+ // Refresh queued observations without moving them behind newer arrivals.
28
+ pending.set(key, job); pump(); return true;
29
+ },
30
+ idle,
31
+ async stop() { closed = true; pending.clear(); notifyIdle(); await idle(); },
32
+ snapshot() { return { active: active.size, pending: pending.size, failures, lastFailureKey, closed }; },
33
+ };
34
+ }
@@ -0,0 +1,132 @@
1
+ // Canonical resource planner owned by Crew. Hosts supply validated resource identities.
2
+ import { createHash } from 'node:crypto';
3
+
4
+ const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
5
+ const terminal = new Set(['verified', 'superseded', 'superseded_by_user_revision']);
6
+ const active = new Set(['running', 'dispatched', 'in_flight']);
7
+ const list = value => Array.isArray(value) ? value : [];
8
+
9
+ // Resource IDs must be canonicalized by the controller (including workspace realpath).
10
+ // Reads conflict with writers; the controller must enroll unknown writes conservatively.
11
+ function conflicts(a, b) {
12
+ const aw = list(a.writeResources), bw = list(b.writeResources);
13
+ const ar = list(a.readResources), br = list(b.readResources);
14
+ return aw.some(r => [...bw, ...br].includes(r)) || bw.some(r => ar.includes(r));
15
+ }
16
+
17
+ function prWait(unit, observations) {
18
+ if (!unit.pr) return null;
19
+ const current = observations[unit.pr.url];
20
+ if (!current || !unit.pr.head || !unit.pr.base || !current.head || !current.base) return 'pr_observation_required';
21
+ if (current.head !== unit.pr.head || current.base !== unit.pr.base) return 'stale_pr_revision';
22
+ if (current.ciHead !== current.head || current.ciBase !== current.base || current.ci !== 'passed') return 'current_revision_ci_required';
23
+ return null;
24
+ }
25
+
26
+ // Retain every unfinished operation and every dependency target. Only terminal,
27
+ // unreferenced history may be compacted; record count is not an execution budget.
28
+ export function compactWorkHistory(ledger, historyLimit = 64) {
29
+ const referenced = new Set(ledger.units.flatMap(unit => list(unit.dependsOn)));
30
+ for (const unit of ledger.units) if (unit.required && unit.purposeRevision === ledger.purposeRevision) referenced.add(unit.id);
31
+ const history = ledger.units.filter(unit => terminal.has(unit.status) && !referenced.has(unit.id));
32
+ const retained = new Set(history.slice(-historyLimit).map(unit => unit.id));
33
+ ledger.units = ledger.units.filter(unit => !terminal.has(unit.status) || referenced.has(unit.id) || retained.has(unit.id));
34
+ // Legacy unitsOmitted means old code may have lost an unfinished effect.
35
+ // Never manufacture a complete ledger when that uncertainty already exists.
36
+ return ledger;
37
+ }
38
+
39
+ /** Pure projection of the existing evidence.units; never grants authority or dispatches. */
40
+ export function planWorkFrontier(ledger, { prObservations = {}, stopped = false, preferredUnitId = null } = {}) {
41
+ if (!ledger || !Array.isArray(ledger.units)) throw new Error('Existing work-unit ledger required.');
42
+ const units = ledger.units;
43
+ const ids = new Set();
44
+ for (const unit of units) {
45
+ if (!unit?.id || ids.has(unit.id)) throw new Error('Work-unit IDs must be present and unique.');
46
+ ids.add(unit.id);
47
+ for (const field of ['dependsOn', 'readResources', 'writeResources']) {
48
+ if (!Array.isArray(unit[field]) || unit[field].some(x => typeof x !== 'string' || !x)) throw new Error(`Invalid ${field} for ${unit.id}.`);
49
+ }
50
+ }
51
+ const byId = new Map(units.map(u => [u.id, u]));
52
+ const visiting = [], visited = new Set(), cycles = new Set();
53
+ function visit(id) {
54
+ if (visiting.includes(id)) { visiting.slice(visiting.indexOf(id)).forEach(x => cycles.add(x)); return; }
55
+ if (visited.has(id)) return;
56
+ visiting.push(id);
57
+ for (const dep of byId.get(id).dependsOn) if (byId.has(dep)) visit(dep);
58
+ visiting.pop(); visited.add(id);
59
+ }
60
+ units.forEach(u => visit(u.id));
61
+ const holders = units.filter(u => active.has(u.status) || u.status === 'unknown' ||
62
+ u.status === 'observed' && u.action?.kind !== 'read' &&
63
+ !(Number.isFinite(u.localExecutionSettledAt) && ['local_edit', 'local_test'].includes(u.action?.kind)) && u.effectReconciledRevision !== u.purposeRevision);
64
+ const proposals = [], rows = [];
65
+ // The controller selected this action after assessment. Older unstarted plans
66
+ // may not reserve its resources; actual in-flight and uncertain holders still do.
67
+ const ordered = [...units].sort((a, b) => Number(b.id === preferredUnitId) - Number(a.id === preferredUnitId));
68
+ for (const unit of ordered) {
69
+ let reason = null, state = 'waiting';
70
+ if (terminal.has(unit.status)) state = 'retained';
71
+ else if (unit.purposeRevision !== ledger.purposeRevision) reason = 'purpose_revision_changed';
72
+ else if (stopped) reason = 'explicit_stop';
73
+ else if (ledger.unitsOmitted && !(unit.action?.kind === 'read' && unit.writeResources.length === 0)) reason = 'ledger_reconciliation_required';
74
+ else if (active.has(unit.status)) reason = 'in_flight';
75
+ else if (['unknown', 'observed'].includes(unit.status)) reason = 'reconcile_prior_effect';
76
+ else if (!['planned', 'waiting', 'failed'].includes(unit.status)) reason = 'unknown_status';
77
+ else if (unit.status === 'failed') reason = 'failure_replan_required';
78
+ else if (cycles.has(unit.id)) reason = 'dependency_cycle';
79
+ else if (unit.waitReason) reason = unit.waitReason;
80
+ else if (!unit.owner) reason = 'owner_required';
81
+ else if ((reason = prWait(unit, prObservations))) { /* observation is revision bound */ }
82
+ else {
83
+ for (const id of unit.dependsOn) {
84
+ const dep = byId.get(id);
85
+ if (!dep) { reason = 'missing_dependency'; break; }
86
+ if (dep.status !== 'verified' || dep.purposeRevision !== ledger.purposeRevision) { reason = 'dependency_unverified'; break; }
87
+ if (prWait(dep, prObservations)) { reason = 'dependency_pr_changed'; break; }
88
+ }
89
+ if (!reason) {
90
+ const conflict = [...holders, ...proposals.map(id => byId.get(id))].find(other => other.id !== unit.id && conflicts(unit, other) &&
91
+ // A read establishes the result of an observed effect. Check all
92
+ // other holders; a label must never bypass live/unknown writers.
93
+ !(unit.action?.kind === 'read' && unit.writeResources.length === 0 && other.status === 'observed'));
94
+ if (conflict) reason = `resource_conflict:${conflict.id}`;
95
+ }
96
+ if (!reason) { state = 'proposed'; proposals.push(unit.id); }
97
+ }
98
+ rows.push({ id: unit.id, state, owner: unit.owner || null, waitReason: reason,
99
+ nextObservation: reason ? unit.nextObservation || reason : null });
100
+ }
101
+ // Capture scheduling inputs, not unrelated observer receipts appended while
102
+ // preparing the provider. Dependency/status/resource changes still fence it.
103
+ const inputDigest = hash({ purposeRevision: ledger.purposeRevision, unitsOmitted: ledger.unitsOmitted,
104
+ units: units.map(({ id, owner, status, purposeRevision, dependsOn, readResources, writeResources, waitReason, pr, action, effectReconciledRevision, localExecutionSettledAt }) =>
105
+ ({ id, owner, status, purposeRevision, dependsOn, readResources, writeResources, waitReason, pr, action, effectReconciledRevision, localExecutionSettledAt })),
106
+ prObservations, stopped, preferredUnitId });
107
+ return { version: 1, inputDigest, proposals, rows, authority: 'proposal_only' };
108
+ }
109
+
110
+ /** Attach only derived metadata to existing rows, never change completion/effect state. */
111
+ export function reconcileWorkFrontier(managed, options = {}) {
112
+ const ledger = managed?.purpose?.evidence;
113
+ // Derived metadata must not invalidate its own input fingerprint.
114
+ const source = { ...ledger, units: ledger?.units?.map(({ planning, ...unit }) => unit) };
115
+ const plan = planWorkFrontier(source, { preferredUnitId: managed.decision?.id, ...options,
116
+ stopped: options.stopped === true || ['paused', 'stopped', 'cancelled'].includes(managed.status) });
117
+ const changed = [];
118
+ for (const row of plan.rows) {
119
+ const unit = ledger.units.find(u => u.id === row.id);
120
+ if (terminal.has(unit.status)) continue;
121
+ if (JSON.stringify(unit.planning) !== JSON.stringify(row)) { unit.planning = row; changed.push(row.id); }
122
+ }
123
+ return { ...plan, changed };
124
+ }
125
+
126
+ /** Recompute under the caller's existing dispatch lock immediately before admission. */
127
+ export function revalidateWorkProposal(managed, proposal, unitId, options = {}) {
128
+ const current = reconcileWorkFrontier(managed, options);
129
+ return { eligible: proposal?.inputDigest === current.inputDigest && current.proposals.includes(unitId),
130
+ reason: proposal?.inputDigest !== current.inputDigest ? 'plan_changed' : current.rows.find(r => r.id === unitId)?.waitReason || null,
131
+ plan: current, authority: 'proposal_only' };
132
+ }
@@ -0,0 +1,6 @@
1
+ // Generated by simy-crew/scripts/sync_loop_kernel.py. Do not edit.
2
+ import { readFileSync } from 'node:fs';
3
+ import { POLICY_DIGEST, POLICY_VERSION } from './loop-kernel/index.mjs';
4
+ const pin = JSON.parse(readFileSync(new URL('./loop-kernel/pin.json', import.meta.url)));
5
+ if (POLICY_DIGEST !== pin.sha256 || POLICY_VERSION !== pin.version) throw new Error('Shared loop kernel pin mismatch');
6
+ export { decideLoop, agentRole, sessionLoopOptions, normalizeOutcomeReview, compactWorkHistory, createTaskPool, POLICY_DIGEST, POLICY_VERSION } from './loop-kernel/index.mjs';
@@ -1,3 +1,4 @@
1
+ import { decideLoop } from './loop-kernel.mjs';
1
2
  import { createHash } from "node:crypto";
2
3
 
3
4
  function text(value) {
@@ -6,8 +7,7 @@ function text(value) {
6
7
 
7
8
  function observed(row) {
8
9
  return typeof row?.passed === "boolean" && Boolean(text(row.verification_method)) &&
9
- Boolean(text(row.observed_result)) && Array.isArray(row.evidence_refs) &&
10
- row.evidence_refs.some((ref) => Boolean(text(ref)));
10
+ Boolean(text(row.observed_result));
11
11
  }
12
12
 
13
13
  function classifyBlockers(findings) {
@@ -93,12 +93,22 @@ export function evaluateOutcomeProgress({
93
93
  const hasProgress = newlyVerified.length > 0;
94
94
  const previousNoProgress = Number.isInteger(previous?.no_progress_attempts) && previous.no_progress_attempts >= 0
95
95
  ? previous.no_progress_attempts : 0;
96
- const noProgressAttempts = comparable
96
+ // An outage breaks the observation sequence; it cannot consume a progress
97
+ // window and make the first subsequent actual failure look repetitive.
98
+ const noProgressAttempts = verified.length + unproven.length === 0 ? 0 : comparable
97
99
  ? hasProgress ? 0 : previousNoProgress + 1
98
100
  : verified.length > 0 ? 0 : 1;
99
101
  const blockers = classifyBlockers(findings);
100
- const complete = rows.length > 0 && unproven.length === 0 && unassessed.length === 0;
101
- const stalled = !complete && noProgressAttempts >= maxNoProgressAttempts;
102
+ const disposition = decideLoop({
103
+ completion_claim: true,
104
+ remaining_work: unproven.length > 0,
105
+ review_satisfied: rows.length > 0 && unassessed.length === 0,
106
+ review_unavailable: unassessed.length > 0,
107
+ });
108
+ const complete = disposition.action === 'accept';
109
+ // Missing assessment is not observed stagnation. Keep unassessed outcomes
110
+ // visible without opening a stop circuit for unavailable paperwork.
111
+ const stalled = !complete && unproven.length > 0 && noProgressAttempts >= maxNoProgressAttempts;
102
112
  const evaluationStatus = unassessed.length === 0 ? "assessed"
103
113
  : verified.length + unproven.length > 0 ? "partial" : "unavailable";
104
114
  const nextActions = [];
@@ -109,6 +119,7 @@ export function evaluateOutcomeProgress({
109
119
  if (stalled) nextActions.push("reassess_approach");
110
120
  return {
111
121
  version: 2,
122
+ loop_kernel: disposition,
112
123
  objective_digest: objectiveDigest,
113
124
  objective_version: objectiveVersion,
114
125
  scope_digest: scopeDigest,
@@ -0,0 +1,10 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { effectiveAcceptanceCriteria } from './acceptance-policy.mjs';
3
+
4
+ const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
5
+ // Snapshot this before dispatching the independent reviewer. It binds both the
6
+ // frozen conditions and the authorized effective route, without granting one.
7
+ export function completionContractIdentity(managed) {
8
+ return hash([managed.purpose?.evidence?.purposeRevision || 1,
9
+ managed.acceptanceCriteria, effectiveAcceptanceCriteria(managed)]);
10
+ }