apsimo 1.3.0__py3-none-any.whl

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 (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. colony_sidecar/__init__.py +4 -0
@@ -0,0 +1,1681 @@
1
+ """ProjectEngine -- sustained multi-tick pursuit of durable objectives.
2
+
3
+ Pursued from the autonomy loop's ``_phase_projects`` (after
4
+ ``_phase_execute``). Each tick: adopt qualifying project-type initiatives,
5
+ plan any project still in ``planning`` (one LLM pass, deterministically
6
+ validated), then advance due active projects by one ready step each.
7
+
8
+ Safety posture:
9
+ - Every step is boundary-checked (DirectiveGuard) before dispatch; a blocked
10
+ step blocks the whole project (visible, never silent).
11
+ - Step dispatch routes through the sub-path that already gates that action
12
+ kind: reasoning turn (internal tools) for analyze/research/internal,
13
+ DirectedActionService (approval tiering + dry_run) for directed, the
14
+ guarded proposal path for deliver. This engine adds NO new outbound or
15
+ mutating primitive of its own; it is orchestration over existing gates.
16
+ - COLONY_PROJECTS_MODE=shadow (default): plans for real, logs the exact
17
+ intended step action with its boundary verdict, simulates advancement, and
18
+ stores milestone proposals with status "shadow" WITHOUT routing them to
19
+ delivery. Nothing leaves the machine.
20
+ - Uses the self-model (item 4) to defer pursuit under load, and skills memory
21
+ (item 3) to inform planning and distill procedures from completions.
22
+
23
+ For step EXECUTION beyond the sidecar, deployments point the directed
24
+ pipeline's env-configured delegate endpoint at their host framework's job
25
+ surface (e.g. a kanban/runs API); this engine deliberately reuses that seam
26
+ instead of growing a private execution runner.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import asyncio
32
+ from datetime import datetime, timezone
33
+ import hashlib
34
+ import json
35
+ import logging
36
+ import os
37
+ import re
38
+ import time
39
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Set, Tuple
40
+ import uuid
41
+
42
+ from apsimo.projects.models import (
43
+ Project, Step, projects_max_replans, projects_mode, projects_review_secs,
44
+ )
45
+ from apsimo.projects.store import ProjectStore
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ # Canonical ``internal`` WorkOrders may quote a standalone owner safety clause,
50
+ # e.g. ``; do not contact anyone``. Rechecking that quote as affirmative
51
+ # action intent self-blocks the safe WorkOrder. Split only at hard clause
52
+ # boundaries and remove only a plainly negated whole clause. Reversal and
53
+ # double-negation language remains fail-closed.
54
+ _BOUNDARY_CLAUSE_SPLIT = re.compile(r"(?:[;\n]+|(?<=[.!?])\s+)")
55
+ _NEGATED_CONSTRAINT = re.compile(
56
+ r"^\s*(?:and\s+)?"
57
+ r"(?:do\s+not|don['’]t|never|must\s+not|shall\s+not|no)\b",
58
+ re.IGNORECASE,
59
+ )
60
+ _NEGATION_REVERSAL = re.compile(
61
+ r"\b(?:not|avoid\w*|refrain\w*|fail\w*|refus\w*|declin\w*|stop\w*)\b",
62
+ re.IGNORECASE,
63
+ )
64
+
65
+
66
+ def _boundary_intent_text(*parts: object) -> str:
67
+ """Return executable clauses from an internal action description."""
68
+
69
+ kept: List[str] = []
70
+ for part in parts:
71
+ for clause in _BOUNDARY_CLAUSE_SPLIT.split(str(part or "")):
72
+ clause = clause.strip()
73
+ if not clause:
74
+ continue
75
+ match = _NEGATED_CONSTRAINT.match(clause)
76
+ if match and not _NEGATION_REVERSAL.search(clause[match.end():]):
77
+ continue
78
+ kept.append(clause)
79
+ return " ".join(kept)
80
+
81
+
82
+ def _step_boundary_action(project: Project, step: Step):
83
+ """Build the boundary action without weakening effectful step checks."""
84
+
85
+ from apsimo.directives import Action
86
+ from apsimo.work_orders import action_authority
87
+
88
+ risk = action_authority(step.action_kind)[2]
89
+ if risk == "internal":
90
+ return Action(
91
+ kind=step.action_kind,
92
+ text=_boundary_intent_text(
93
+ step.description, project.title, project.objective,
94
+ ),
95
+ target=step.boundary_subject,
96
+ high_risk=True,
97
+ )
98
+ return Action(
99
+ kind=step.action_kind,
100
+ text=f"{step.description} {step.boundary_subject}",
101
+ target=project.subject_text(),
102
+ high_risk=True,
103
+ )
104
+
105
+ # Step execution composes through the shared cognition charter (role
106
+ # "executor"); this block scopes the turn to ONE project step.
107
+ _STEP_SCOPE_CONTEXT = """\
108
+ You are executing ONE STEP of a long-running project. Complete THIS STEP
109
+ ONLY, then summarize the outcome in 2-4 sentences; later steps are separate
110
+ work sessions. If the step cannot be completed, say precisely what is
111
+ missing."""
112
+
113
+ _MAX_TOOL_ROUNDS = 4
114
+ _AUTHORITY_BOUND_PROJECT_SOURCES = frozenset({
115
+ "cognition_spine", "governed_action",
116
+ })
117
+ _GOVERNED_HOLD_REASONS = frozenset({
118
+ "governed_action_requires_live_projects_mode",
119
+ "governed_action_requires_canonical_work_order_adapter",
120
+ })
121
+ _TURN_CONCERN_HOLD_REASON = "turn_concerns_current_mode_not_live"
122
+ _PROJECT_HOLD_REASON_UNAVAILABLE = "project_hold_reason_unavailable"
123
+ _TRANSIENT_PROJECT_HOLD_REASONS = frozenset({
124
+ _TURN_CONCERN_HOLD_REASON,
125
+ _PROJECT_HOLD_REASON_UNAVAILABLE,
126
+ })
127
+
128
+
129
+ def _int_env(name: str, default: int) -> int:
130
+ try:
131
+ return int(os.environ.get(name, str(default)))
132
+ except (TypeError, ValueError):
133
+ return default
134
+
135
+
136
+ class ProjectEngine:
137
+ def __init__(
138
+ self,
139
+ store: ProjectStore,
140
+ *,
141
+ directive_manager: Any = None,
142
+ llm_router: Any = None, # planning + distillation passes
143
+ reasoning_loop: Any = None, # analyze/research/internal steps
144
+ tool_executor: Any = None,
145
+ directed_service: Any = None, # directed steps
146
+ proposal_store: Any = None,
147
+ feedback_store: Any = None,
148
+ self_model: Any = None,
149
+ skill_store: Any = None,
150
+ delivery_router: Any = None, # async callable(payload) -> bool
151
+ initiative_store: Any = None, # adoption of project-type initiatives
152
+ work_order_adapter: Any = None, # canonical external execution bridge
153
+ project_hold_reason: Optional[Callable[[Project], str]] = None,
154
+ ) -> None:
155
+ self.store = store
156
+ self._directives = directive_manager
157
+ self._router = llm_router
158
+ self._reasoning = reasoning_loop
159
+ self._tools = tool_executor
160
+ self._directed = directed_service
161
+ self._proposals = proposal_store
162
+ self._feedback = feedback_store
163
+ self._self_model = self_model
164
+ self._skills = skill_store
165
+ self._deliver = delivery_router
166
+ self._initiatives = initiative_store
167
+ self._work_orders = work_order_adapter
168
+ self._project_hold_reason = project_hold_reason
169
+
170
+ def _apply_project_hold(self, project: Project) -> str:
171
+ reason = ""
172
+ if self._project_hold_reason is not None:
173
+ try:
174
+ reason = str(self._project_hold_reason(project) or "")[:200]
175
+ except Exception:
176
+ logger.exception("project hold callback failed closed")
177
+ reason = _PROJECT_HOLD_REASON_UNAVAILABLE
178
+ if reason in _TRANSIENT_PROJECT_HOLD_REASONS:
179
+ # Transient concern-mode holds are visible when they do not mask a
180
+ # pre-existing, unrelated project reason. The returned reason is
181
+ # still authoritative for eligibility either way.
182
+ if (
183
+ project.reason in _TRANSIENT_PROJECT_HOLD_REASONS
184
+ or not project.reason
185
+ ) and project.reason != reason:
186
+ project.reason = reason
187
+ self.store.save_project(project)
188
+ return reason
189
+ if project.reason in _TRANSIENT_PROJECT_HOLD_REASONS:
190
+ project.reason = ""
191
+ self.store.save_project(project)
192
+ return reason
193
+
194
+ def open_capacity_used(self) -> int:
195
+ """Count open work after reconciling exact turn-mode holds.
196
+
197
+ Only the exact, known turn-mode hold releases a capacity slot. A
198
+ callback outage and every ordinary/external/governed project remain
199
+ capacity-bearing and therefore fail conservatively.
200
+ """
201
+
202
+ used = 0
203
+ for status in ("planning", "active"):
204
+ offset = 0
205
+ while True:
206
+ page = self.store.list_projects(
207
+ status=status, limit=100, offset=offset,
208
+ )
209
+ if not page:
210
+ break
211
+ offset += len(page)
212
+ for project in page:
213
+ hold = self._apply_project_hold(project)
214
+ if hold != _TURN_CONCERN_HOLD_REASON:
215
+ used += 1
216
+ if len(page) < 100:
217
+ break
218
+ return used
219
+
220
+ def _eligible_projects(
221
+ self,
222
+ *,
223
+ status: str,
224
+ limit: int,
225
+ held_ids: Optional[Set[str]] = None,
226
+ due: bool = False,
227
+ ) -> List[Project]:
228
+ """Scan past held rows so they cannot starve ordinary work."""
229
+
230
+ selected: List[Project] = []
231
+ offset = 0
232
+ page_size = max(25, limit)
233
+ while len(selected) < limit:
234
+ page = (
235
+ self.store.due_for_review(limit=page_size, offset=offset)
236
+ if due else
237
+ self.store.list_projects(
238
+ status=status, limit=page_size, offset=offset,
239
+ )
240
+ )
241
+ if not page:
242
+ break
243
+ offset += len(page)
244
+ for project in page:
245
+ if self._apply_project_hold(project):
246
+ if held_ids is not None:
247
+ held_ids.add(project.id)
248
+ continue
249
+ selected.append(project)
250
+ if len(selected) >= limit:
251
+ break
252
+ if len(page) < page_size:
253
+ break
254
+ return selected
255
+
256
+ def _has_canonical_work_order_adapter(self) -> bool:
257
+ try:
258
+ from apsimo.work_orders import QueueWorkOrderAdapter
259
+ except Exception:
260
+ return False
261
+ return (
262
+ isinstance(self._work_orders, QueueWorkOrderAdapter)
263
+ and self._work_orders.project_store is self.store
264
+ )
265
+
266
+ def _governed_hold_reason(self, mode: str) -> str:
267
+ if mode != "live" or projects_mode() != "live":
268
+ return "governed_action_requires_live_projects_mode"
269
+ if not self._has_canonical_work_order_adapter():
270
+ return "governed_action_requires_canonical_work_order_adapter"
271
+ return ""
272
+
273
+ @staticmethod
274
+ def _governed_identity(project: Project) -> str:
275
+ source_refs = list(project.source_event_refs)
276
+ evidence_refs = list(project.evidence_refs)
277
+ if (
278
+ len(source_refs) != 2
279
+ or len(evidence_refs) != 4
280
+ or not source_refs[0].startswith("governed-action:")
281
+ or not source_refs[1].startswith("governed-intent:")
282
+ or not evidence_refs[0].startswith("action-digest:")
283
+ or not evidence_refs[1].startswith("intent-digest:")
284
+ or not evidence_refs[2].startswith("args-digest:")
285
+ or not evidence_refs[3].startswith("execution-digest:")
286
+ ):
287
+ raise ValueError("governed research provenance is incomplete")
288
+ action_id = source_refs[0].split(":", 1)[1]
289
+ intent_id = source_refs[1].split(":", 1)[1]
290
+ try:
291
+ parsed_action_id = uuid.UUID(action_id)
292
+ except (AttributeError, ValueError) as exc:
293
+ raise ValueError("governed research action ID is invalid") from exc
294
+ if (
295
+ parsed_action_id.version != 4
296
+ or str(parsed_action_id) != action_id
297
+ or not re.fullmatch(r"hti_[0-9a-f]{32}", intent_id)
298
+ ):
299
+ raise ValueError("governed research identity is invalid")
300
+ digests = []
301
+ for index, prefix in enumerate((
302
+ "action-digest:", "intent-digest:", "args-digest:",
303
+ "execution-digest:",
304
+ )):
305
+ if not evidence_refs[index].startswith(prefix):
306
+ raise ValueError("governed research digest provenance is invalid")
307
+ digests.append(evidence_refs[index].split(":", 1)[1])
308
+ if any(
309
+ len(value) != 64
310
+ or any(character not in "0123456789abcdef" for character in value)
311
+ for value in digests
312
+ ):
313
+ raise ValueError("governed research identity is invalid")
314
+ action_digest, _intent_digest, _args_digest, execution_digest = digests
315
+ material = {
316
+ "schema": "ColonyGovernedResearchProjectIdentityV1",
317
+ "version": 1,
318
+ "owner_person_id": project.subject_person_id,
319
+ "action_id": action_id,
320
+ "action_digest": action_digest,
321
+ "execution_digest": execution_digest,
322
+ }
323
+ encoded = json.dumps(
324
+ material, sort_keys=True, separators=(",", ":"), ensure_ascii=True,
325
+ ).encode("utf-8")
326
+ return hashlib.sha256(encoded).hexdigest()
327
+
328
+ def prepare_governed_research(self, project: Project) -> None:
329
+ """Validate one owner-approved, read-only durable project handoff."""
330
+
331
+ if not isinstance(project, Project) or project.source != "governed_action":
332
+ raise RuntimeError("governed research requires a governed Project")
333
+ if projects_mode() != "live":
334
+ raise RuntimeError("governed research requires ProjectEngine live mode")
335
+ if not self._has_canonical_work_order_adapter():
336
+ raise RuntimeError(
337
+ "governed research requires the canonical WorkOrder adapter"
338
+ )
339
+ try:
340
+ from apsimo.work_orders import action_authority
341
+ capabilities = list(action_authority("research")[1])
342
+ except Exception as exc:
343
+ raise RuntimeError("governed research WorkOrder authority is unavailable") from exc
344
+ identity = self._governed_identity(project)
345
+ expected_id = "proj-governed-" + identity[:20]
346
+ expected_title = "Governed research " + identity[:8]
347
+ if (
348
+ project.id != expected_id
349
+ or project.goal_fingerprint != identity
350
+ or project.title != expected_title
351
+ or project.status != "planning"
352
+ or project.outcome != "pending"
353
+ or project.reason
354
+ or project.replans != 0
355
+ or project.next_review_at != 0.0
356
+ or project.entity_ids
357
+ or not project.subject_person_id
358
+ or project.viewer_scope != "owner"
359
+ or project.shareability != "owner_private"
360
+ or project.capability_allowlist != capabilities
361
+ ):
362
+ raise RuntimeError("governed research Project authority is invalid")
363
+ prefix = "Research topic:\n"
364
+ if not project.objective.startswith(prefix) or "\nDepth: " not in project.objective:
365
+ raise RuntimeError("governed research objective is invalid")
366
+ topic, depth = project.objective[len(prefix):].rsplit("\nDepth: ", 1)
367
+ if not topic or len(topic) > 1400 or depth not in {"quick", "standard", "deep"}:
368
+ raise RuntimeError("governed research objective is outside its bound")
369
+ policy_refs = list(project.policy_decision_refs)
370
+ if (
371
+ len(policy_refs) != 4
372
+ or not policy_refs[0].startswith("approval:")
373
+ or not policy_refs[1].startswith("decision:")
374
+ or not policy_refs[2].startswith("approval-revision:")
375
+ or not policy_refs[3].startswith("authorization-receipt-digest:")
376
+ ):
377
+ raise RuntimeError("governed research decision provenance is incomplete")
378
+ approval_id = policy_refs[0].split(":", 1)[1]
379
+ decision_id = policy_refs[1].split(":", 1)[1]
380
+ revision = policy_refs[2].split(":", 1)[1]
381
+ receipt_digest = policy_refs[3].split(":", 1)[1]
382
+ if (
383
+ not re.fullmatch(r"APR-[A-Z0-9]{12}", approval_id)
384
+ or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}", decision_id)
385
+ or revision != "1"
386
+ or not re.fullmatch(r"[0-9a-f]{64}", receipt_digest)
387
+ ):
388
+ raise RuntimeError("governed research decision provenance is invalid")
389
+ refs = project.provenance_refs()
390
+ if (
391
+ len(refs) != 10
392
+ or len(set(refs)) != 10
393
+ or any(
394
+ not isinstance(ref, str)
395
+ or not ref
396
+ or len(ref) > 256
397
+ or any(ord(character) < 0x20 for character in ref)
398
+ for ref in refs
399
+ )
400
+ ):
401
+ raise RuntimeError("governed research provenance is invalid")
402
+
403
+ # Existing exact replays already passed their boundary before insert.
404
+ # A new row must still pass the current standing project boundary.
405
+ if self.store.get_project(project.id) is None and self._directives is not None:
406
+ try:
407
+ from apsimo.directives import Action
408
+ verdict = self._directives.check(Action(
409
+ kind="project",
410
+ text=project.objective,
411
+ target=project.subject_person_id,
412
+ high_risk=True,
413
+ ))
414
+ except Exception as exc:
415
+ raise RuntimeError(
416
+ "governed research boundary check failed closed"
417
+ ) from exc
418
+ if getattr(verdict, "allowed", None) is not True:
419
+ raise RuntimeError("governed research boundary refused the project")
420
+
421
+ def enqueue_governed_research(self, project: Project) -> Project:
422
+ """Durably enqueue or exactly replay one governed research Project."""
423
+
424
+ self.prepare_governed_research(project)
425
+ stored, _created = self.store.insert_authority_bound_project(project)
426
+ return stored
427
+
428
+ @staticmethod
429
+ def _authority_gaps(
430
+ project: Project, steps: List[Step],
431
+ ) -> Dict[int, List[str]]:
432
+ if project.source not in _AUTHORITY_BOUND_PROJECT_SOURCES:
433
+ return {}
434
+ from apsimo.work_orders import action_authority
435
+ allowed = set(project.capability_allowlist)
436
+ missing: Dict[int, List[str]] = {}
437
+ for step in steps:
438
+ if project.source == "cognition_spine" and step.action_kind == "deliver":
439
+ missing[step.ordinal] = [
440
+ "p3_deliver_held_missing_attested_recipient_artifact_envelope"
441
+ ]
442
+ continue
443
+ required = set(action_authority(step.action_kind)[1])
444
+ excess = sorted(required - allowed)
445
+ if excess:
446
+ missing[step.ordinal] = excess
447
+ return missing
448
+
449
+ # ------------------------------------------------------------------
450
+ # Creation / adoption / abandonment
451
+ # ------------------------------------------------------------------
452
+
453
+ def create_project(self, objective: str, *, title: str = "",
454
+ source: str = "owner",
455
+ entity_ids: Optional[List[str]] = None,
456
+ ) -> Tuple[Optional[Project], str]:
457
+ """Boundary-gated project creation (planning only; steps gate at
458
+ dispatch). Returns (project, reason)."""
459
+ objective = (objective or "").strip()
460
+ if not objective:
461
+ return None, "objective_required"
462
+ try:
463
+ from apsimo.cognition.goal_spine import cognition_spine_exclusive
464
+ if cognition_spine_exclusive():
465
+ if source == "cognition_spine":
466
+ return None, "typed_goal_proposal_required"
467
+ if source not in {"owner", "directive"}:
468
+ return None, "legacy_autonomous_project_writer_read_only"
469
+ except Exception:
470
+ pass
471
+ if self._directives is not None:
472
+ try:
473
+ from apsimo.directives import Action
474
+ verdict = self._directives.check(Action(
475
+ kind="project", text=objective, target=title or objective,
476
+ high_risk=True))
477
+ if not verdict.allowed:
478
+ logger.warning("Project creation REFUSED by boundary: %s",
479
+ verdict.reason)
480
+ return None, verdict.reason
481
+ except Exception:
482
+ logger.warning(
483
+ "Project creation blocked: boundary check failed closed",
484
+ exc_info=True,
485
+ )
486
+ return None, "boundary_check_failed_closed"
487
+ title = (title or objective.split(".")[0]).strip()[:120]
488
+ project = Project(title=title, objective=objective, source=source,
489
+ entity_ids=list(entity_ids or []))
490
+ self.store.save_project(project)
491
+ logger.info("Project created: %s %r (source=%s, mode=%s)",
492
+ project.id, title, source, projects_mode())
493
+ return project, "ok"
494
+
495
+ async def create_owner_goal_work_order(
496
+ self,
497
+ objective: str,
498
+ *,
499
+ external_event_id: str,
500
+ external_event_digest: str,
501
+ intake_receipt: Mapping[str, Any],
502
+ subject_person_id: str,
503
+ viewer_scope: str,
504
+ shareability: str,
505
+ occurred_at: str,
506
+ ) -> Dict[str, Any]:
507
+ """Create one deterministic owner Project and initial WorkOrder.
508
+
509
+ This is the narrow durable target for a transport adapter that has
510
+ already authenticated an owner ``Goal:`` request. Intent recognition
511
+ happens before this method; no model decides whether the Project
512
+ exists. The first bounded ``analyze`` step lets the normal canonical
513
+ WorkOrder/Action Plane continue the goal without granting an external
514
+ effect at ingestion time.
515
+
516
+ Every identity and authority-bearing timestamp is derived from the
517
+ immutable external event. Replaying the event therefore validates and
518
+ returns the same Project, Step, and WorkOrder instead of creating work
519
+ twice. The external intake receipt and journal identity are reference
520
+ linked into the WorkOrder; external producer text never becomes an
521
+ authority or an execution receipt by itself.
522
+ """
523
+
524
+ from apsimo.work_orders import WorkOrderV1
525
+
526
+ normalized = " ".join(str(objective or "").split()).strip()
527
+ event_id = str(external_event_id or "").strip()
528
+ event_digest = str(external_event_digest or "").strip()
529
+ subject = str(subject_person_id or "").strip()
530
+ scope = str(viewer_scope or "").strip()
531
+ sharing = str(shareability or "").strip()
532
+ if not normalized or len(normalized) > 500:
533
+ raise ValueError("owner goal objective is outside bounds")
534
+ if not event_id or len(event_id) > 192:
535
+ raise ValueError("owner goal external event ID is outside bounds")
536
+ if not re.fullmatch(r"[0-9a-f]{64}", event_digest):
537
+ raise ValueError("owner goal external event digest is invalid")
538
+ if not subject or not scope or sharing != "owner_private":
539
+ raise ValueError("owner goal scope is invalid")
540
+ try:
541
+ issued = datetime.fromisoformat(
542
+ str(occurred_at or "").replace("Z", "+00:00")
543
+ )
544
+ except ValueError as exc:
545
+ raise ValueError("owner goal occurrence time is invalid") from exc
546
+ if issued.tzinfo is None:
547
+ raise ValueError("owner goal occurrence time requires a timezone")
548
+ issued = issued.astimezone(timezone.utc)
549
+ issued_at = issued.timestamp()
550
+
551
+ receipt = dict(intake_receipt or {})
552
+ receipt_ref = str(receipt.get("receipt_ref") or "").strip()
553
+ journal_event_id = str(receipt.get("journal_event_id") or "").strip()
554
+ journal_seq = receipt.get("journal_seq")
555
+ if (
556
+ receipt.get("status") != "projected"
557
+ or receipt.get("event_id") != event_id
558
+ or not receipt_ref.startswith("external-event-receipt:")
559
+ or not journal_event_id
560
+ or isinstance(journal_seq, bool)
561
+ or not isinstance(journal_seq, int)
562
+ or journal_seq < 1
563
+ ):
564
+ raise ValueError("owner goal intake receipt is incomplete")
565
+
566
+ identity = hashlib.sha256(
567
+ ("owner-goal-v1\0" + event_id).encode("utf-8")
568
+ ).hexdigest()
569
+ project_id = f"proj-owner-goal-{identity[:20]}"
570
+ step_id = f"step-owner-goal-{identity[:20]}"
571
+ title = normalized.split(".", 1)[0].strip()[:120] or "Owner goal"
572
+ goal_fingerprint = hashlib.sha256(json.dumps(
573
+ {
574
+ "title": title.casefold(),
575
+ "objective": normalized.casefold(),
576
+ "subject_person_id": subject,
577
+ "viewer_scope": scope,
578
+ },
579
+ ensure_ascii=False,
580
+ sort_keys=True,
581
+ separators=(",", ":"),
582
+ ).encode("utf-8")).hexdigest()
583
+ event_ref = f"xevent:{event_id}"
584
+ host_event_ref = f"event:{journal_event_id}"
585
+ evidence_refs = [
586
+ receipt_ref,
587
+ f"xdigest:{event_digest}",
588
+ f"journal:{journal_seq}:{journal_event_id}",
589
+ ]
590
+ expected_project = Project(
591
+ id=project_id,
592
+ title=title,
593
+ objective=normalized,
594
+ source="owner",
595
+ status="active",
596
+ outcome="pending",
597
+ # ``xevent`` is the immutable producer identity. The cognition
598
+ # ThoughtJob intentionally carries only the server journal event
599
+ # identity, so retaining both lets its duplicate gate meet this
600
+ # Project without copying producer text into either authority.
601
+ source_event_refs=[event_ref, host_event_ref],
602
+ evidence_refs=evidence_refs,
603
+ subject_person_id=subject,
604
+ viewer_scope=scope,
605
+ shareability=sharing,
606
+ goal_fingerprint=goal_fingerprint,
607
+ created_at=issued_at,
608
+ updated_at=issued_at,
609
+ )
610
+ expected_step = Step(
611
+ id=step_id,
612
+ project_id=project_id,
613
+ ordinal=1,
614
+ description=(
615
+ "Analyze and advance the owner-authored goal, returning a "
616
+ f"receipt-backed result: {normalized}"
617
+ ),
618
+ action_kind="analyze",
619
+ status="pending",
620
+ boundary_subject=subject,
621
+ confidence=1.0,
622
+ work_order_issued_at=issued_at,
623
+ created_at=issued_at,
624
+ updated_at=issued_at,
625
+ )
626
+
627
+ project = self.store.get_project(project_id)
628
+ if project is None:
629
+ project = expected_project
630
+ else:
631
+ immutable = (
632
+ "title", "objective", "source", "source_event_refs",
633
+ "evidence_refs", "subject_person_id", "viewer_scope",
634
+ "shareability", "goal_fingerprint",
635
+ )
636
+ if any(
637
+ getattr(project, field) != getattr(expected_project, field)
638
+ for field in immutable
639
+ ):
640
+ raise ValueError("deterministic owner goal Project collision")
641
+
642
+ steps = self.store.steps_for(project_id)
643
+ step = next((item for item in steps if item.id == step_id), None)
644
+ if step is None:
645
+ if steps:
646
+ raise ValueError("deterministic owner goal Step collision")
647
+ step = expected_step
648
+ else:
649
+ authority_fields = (
650
+ "project_id", "ordinal", "description", "action_kind",
651
+ "depends_on", "boundary_subject", "confidence",
652
+ )
653
+ if any(
654
+ getattr(step, field) != getattr(expected_step, field)
655
+ for field in authority_fields
656
+ ):
657
+ raise ValueError("deterministic owner goal Step collision")
658
+ if (
659
+ step.work_order_issued_at
660
+ and abs(step.work_order_issued_at - issued_at) > 0.001
661
+ ):
662
+ raise ValueError("owner goal WorkOrder issue time drifted")
663
+ if not step.work_order_issued_at:
664
+ step.work_order_issued_at = issued_at
665
+
666
+ refs = tuple(project.provenance_refs())
667
+ order = WorkOrderV1.for_project_step(
668
+ project, step, context_refs=refs, now=issued,
669
+ )
670
+ existing_order = self.store.get_work_order(order.work_order_id)
671
+ if existing_order is not None and (
672
+ existing_order["work_order_digest"] != order.work_order_digest
673
+ or existing_order["payload"] != order.payload()
674
+ ):
675
+ raise ValueError("deterministic owner goal WorkOrder collision")
676
+
677
+ terminal = project.status in {"completed", "abandoned"} or (
678
+ step.status in {"done", "failed", "skipped"}
679
+ )
680
+ if terminal:
681
+ if existing_order is None:
682
+ raise ValueError("terminal owner goal is missing its WorkOrder")
683
+ return {
684
+ "schema": "OwnerGoalPromotionReceiptV1",
685
+ "status": "existing_terminal",
686
+ "external_event_id": event_id,
687
+ "external_receipt_ref": receipt_ref,
688
+ "project_id": project_id,
689
+ "step_id": step_id,
690
+ "work_order_ref": f"work-order:{order.work_order_id}",
691
+ "work_order_id": order.work_order_id,
692
+ "work_order_digest": order.work_order_digest,
693
+ }
694
+
695
+ blocked_reason = ""
696
+ if project.status == "blocked":
697
+ blocked_reason = project.reason or "owner_goal_blocked"
698
+ elif self._directives is not None:
699
+ try:
700
+ from apsimo.directives import Action
701
+ for action in (
702
+ Action(
703
+ kind="project",
704
+ text=_boundary_intent_text(normalized, title),
705
+ target="",
706
+ high_risk=True,
707
+ ),
708
+ _step_boundary_action(project, step),
709
+ ):
710
+ verdict = self._directives.check(action)
711
+ if not verdict.allowed:
712
+ blocked_reason = str(
713
+ verdict.reason or "owner_goal_boundary_refused"
714
+ )[:500]
715
+ break
716
+ except Exception:
717
+ blocked_reason = "boundary_check_failed_closed"
718
+
719
+ if blocked_reason:
720
+ if self.store.get_project(project_id) is None:
721
+ project.status = "blocked"
722
+ project.reason = blocked_reason
723
+ self.store.prepare_work_order(project, step, order)
724
+ return {
725
+ "schema": "OwnerGoalPromotionReceiptV1",
726
+ "status": "blocked",
727
+ "reason": blocked_reason,
728
+ "external_event_id": event_id,
729
+ "external_receipt_ref": receipt_ref,
730
+ "project_id": project_id,
731
+ "step_id": step_id,
732
+ "work_order_ref": f"work-order:{order.work_order_id}",
733
+ "work_order_id": order.work_order_id,
734
+ "work_order_digest": order.work_order_digest,
735
+ }
736
+
737
+ if not self._has_canonical_work_order_adapter():
738
+ raise RuntimeError("canonical WorkOrder adapter is unavailable")
739
+ ok, result = await self._work_orders.execute(
740
+ project, step, context_refs=refs,
741
+ )
742
+ if ok is False:
743
+ raise RuntimeError(result or "owner goal WorkOrder issue failed")
744
+ persisted = self.store.get_work_order(order.work_order_id)
745
+ if persisted is None:
746
+ raise RuntimeError("owner goal WorkOrder did not persist")
747
+ return {
748
+ "schema": "OwnerGoalPromotionReceiptV1",
749
+ "status": "issued",
750
+ "queue_state": str(result or "").rsplit(":", 1)[-1],
751
+ "external_event_id": event_id,
752
+ "external_receipt_ref": receipt_ref,
753
+ "project_id": project_id,
754
+ "step_id": step_id,
755
+ "work_order_ref": persisted["work_order_ref"],
756
+ "work_order_id": order.work_order_id,
757
+ "work_order_digest": order.work_order_digest,
758
+ }
759
+
760
+ def abandon(self, project_id: str, reason: str = "owner_request",
761
+ ) -> Optional[Project]:
762
+ project = self.store.get_project(project_id)
763
+ if project is None or project.status in ("completed", "abandoned"):
764
+ return project
765
+ project.status = "abandoned"
766
+ project.outcome = "failed"
767
+ project.reason = reason
768
+ self.store.save_project(project)
769
+ self._record_outcome("failure")
770
+ if self._feedback is not None:
771
+ try:
772
+ self._feedback.record("project", "dismissed")
773
+ except Exception:
774
+ pass
775
+ logger.info("Project %s abandoned: %s", project_id, reason)
776
+ return project
777
+
778
+ def project_status(self, project_id: str) -> Optional[Dict[str, Any]]:
779
+ project = self.store.get_project(project_id)
780
+ if project is None:
781
+ return None
782
+ steps = self.store.steps_for(project_id)
783
+ return {
784
+ "project": project.to_row(),
785
+ "steps": [s.to_row() for s in steps],
786
+ "done": sum(1 for s in steps if s.status == "done"),
787
+ "total": len(steps),
788
+ }
789
+
790
+ # ------------------------------------------------------------------
791
+ # Tick
792
+ # ------------------------------------------------------------------
793
+
794
+ def _effective_mode(self) -> str:
795
+ """Env mode, graduated by the trust engine (Amendment 1.2).
796
+
797
+ "off" and "live" are owner overrides. "shadow" is the calibration
798
+ stage: once the "project" trust domain graduates (clean calibration
799
+ runs), pursuit goes live THROUGH the sub-gates, which carry their own
800
+ ask/approval semantics for anything outbound or mutating.
801
+ """
802
+ mode = projects_mode()
803
+ if mode in ("off", "live"):
804
+ return mode
805
+ trust = getattr(self._self_model, "trust", None)
806
+ if trust is None:
807
+ return mode
808
+ try:
809
+ stage = trust.stage("project", default="shadow")
810
+ except Exception:
811
+ return mode
812
+ return "shadow" if stage == "shadow" else "live"
813
+
814
+ async def tick(self) -> Dict[str, Any]:
815
+ mode = self._effective_mode()
816
+ report: Dict[str, Any] = {"mode": mode, "adopted": 0, "planned": 0,
817
+ "steps_dispatched": 0, "deferred": False}
818
+ if mode == "off":
819
+ return report
820
+ report["held"] = 0
821
+
822
+ # Pursue-vs-defer via the self-model: under heavy load, hold off.
823
+ if self._self_model is not None:
824
+ try:
825
+ load = self._self_model.load()
826
+ if int(load.get("total") or 0) >= _int_env(
827
+ "COLONY_PROJECTS_DEFER_LOAD", 10):
828
+ logger.info("Project pursuit deferred (load=%s)", load)
829
+ report["deferred"] = True
830
+ return report
831
+ except Exception:
832
+ pass
833
+
834
+ try:
835
+ report["adopted"] = await self._adopt_initiatives()
836
+ except Exception:
837
+ logger.debug("project adoption failed", exc_info=True)
838
+ try:
839
+ held_ids: Set[str] = set()
840
+ report["planned"] = await self._plan_pending(mode, held_ids)
841
+ except Exception:
842
+ logger.debug("project planning failed", exc_info=True)
843
+ try:
844
+ report["steps_dispatched"] = await self._pursue_active(
845
+ mode, held_ids,
846
+ )
847
+ except Exception:
848
+ logger.debug("project pursuit failed", exc_info=True)
849
+ report["held"] = len(held_ids)
850
+ return report
851
+
852
+ async def reconcile_terminal_results(
853
+ self, *, limit: int = 25,
854
+ ) -> Dict[str, Any]:
855
+ """Reconcile durable queue outcomes without planning or pursuit.
856
+
857
+ This is intentionally separate from ``tick``: result projection is
858
+ bookkeeping for work that already ran, so it must not wait behind an
859
+ LLM planning pass, self-model deferral, review cadence, or a Project
860
+ boundary that changed after dispatch.
861
+ """
862
+
863
+ reconcile = getattr(
864
+ self._work_orders, "reconcile_terminal_results", None,
865
+ )
866
+ if not callable(reconcile):
867
+ return {
868
+ "checked": 0,
869
+ "terminal": 0,
870
+ "projected": 0,
871
+ "errors": 0,
872
+ }
873
+ return await reconcile(limit=limit)
874
+
875
+ # ------------------------------------------------------------------
876
+ # Adoption: project-type initiatives become durable projects
877
+ # ------------------------------------------------------------------
878
+
879
+ async def _adopt_initiatives(self) -> int:
880
+ try:
881
+ from apsimo.cognition.goal_spine import cognition_spine_exclusive
882
+ if cognition_spine_exclusive():
883
+ return 0
884
+ except Exception:
885
+ pass
886
+ if self._initiatives is None:
887
+ return 0
888
+ open_projects = self.open_capacity_used()
889
+ if open_projects >= _int_env("COLONY_PROJECTS_MAX_CONCURRENT", 3):
890
+ return 0
891
+ try:
892
+ loop = asyncio.get_event_loop()
893
+ pending = await loop.run_in_executor(
894
+ None, lambda: self._initiatives.list(
895
+ status=["pending"], type="project", limit=5))
896
+ except Exception:
897
+ return 0
898
+ existing_titles = {p.title.strip().lower()
899
+ for p in self.store.list_projects(limit=200)}
900
+ for init in pending or []:
901
+ desc = (getattr(init, "description", "") or "").strip()
902
+ if not desc or desc.split(".")[0].strip()[:120].lower() in existing_titles:
903
+ continue
904
+ rationale = (getattr(init, "rationale", "") or "").strip()
905
+ objective = desc if not rationale else f"{desc}\n\nWhy: {rationale}"
906
+ project, reason = self.create_project(
907
+ objective, title=desc.split(".")[0][:120], source="thinker")
908
+ if project is None:
909
+ continue
910
+ try:
911
+ iid = getattr(init, "id", "")
912
+ await asyncio.get_event_loop().run_in_executor(
913
+ None, lambda: self._initiatives.complete(
914
+ iid, agent_id="project-engine",
915
+ result=f"adopted as project {project.id}"))
916
+ except Exception:
917
+ logger.debug("initiative adoption closure failed", exc_info=True)
918
+ logger.info("Adopted initiative as project %s: %r",
919
+ project.id, project.title)
920
+ return 1
921
+ return 0
922
+
923
+ # ------------------------------------------------------------------
924
+ # Planning
925
+ # ------------------------------------------------------------------
926
+
927
+ async def _plan_pending(
928
+ self, mode: str, held_ids: Optional[Set[str]] = None,
929
+ ) -> int:
930
+ from apsimo.projects.planner import plan_project
931
+ planned = 0
932
+ now = time.time()
933
+ for project in self._eligible_projects(
934
+ status="planning", limit=10, held_ids=held_ids,
935
+ ):
936
+ if project.next_review_at and project.next_review_at > now:
937
+ continue
938
+ if project.source == "governed_action":
939
+ hold_reason = self._governed_hold_reason(mode)
940
+ if hold_reason:
941
+ project.reason = hold_reason
942
+ project.next_review_at = now + projects_review_secs()
943
+ self.store.save_project(project)
944
+ continue
945
+ if project.reason in _GOVERNED_HOLD_REASONS:
946
+ project.reason = ""
947
+ skills_block = self._skills_block(project.objective)
948
+ brief = self._self_brief()
949
+ planning_context = ""
950
+ if project.source == "cognition_spine":
951
+ planning_context = (
952
+ "This autonomous goal is bound to these exact capabilities: "
953
+ + ", ".join(project.capability_allowlist)
954
+ + ". Do not create a deliver step: P3 delivery is held until "
955
+ "WorkOrder carries a transport-attested recipient and a "
956
+ "bounded message/artifact reference in its authority digest. "
957
+ "End with an internal receipt-backed artifact instead."
958
+ )
959
+ elif project.source == "governed_action":
960
+ planning_context = (
961
+ "This owner-governed research goal is bound to these exact "
962
+ "read-only capabilities: "
963
+ + ", ".join(project.capability_allowlist)
964
+ + ". Do not create directed or deliver steps, or any step "
965
+ "requiring capabilities outside this list. End with a "
966
+ "receipt-backed internal artifact."
967
+ )
968
+ steps = await plan_project(
969
+ self._router, project.objective, project_id=project.id,
970
+ context=planning_context,
971
+ skills_block=skills_block, self_brief=brief,
972
+ boundaries=self._boundaries_brief())
973
+ if self._apply_project_hold(project):
974
+ if held_ids is not None:
975
+ held_ids.add(project.id)
976
+ continue
977
+ if not steps:
978
+ project.replans += 1
979
+ if project.replans > projects_max_replans():
980
+ project.status = "abandoned"
981
+ project.outcome = "failed"
982
+ project.reason = "planning_failed"
983
+ self.store.save_project(project)
984
+ self._record_outcome("failure")
985
+ logger.warning("Project %s abandoned: planning failed %d times",
986
+ project.id, project.replans)
987
+ else:
988
+ project.next_review_at = now + projects_review_secs()
989
+ self.store.save_project(project)
990
+ continue
991
+ if project.source in _AUTHORITY_BOUND_PROJECT_SOURCES:
992
+ try:
993
+ missing = self._authority_gaps(project, steps)
994
+ except Exception as exc:
995
+ project.status = "blocked"
996
+ project.reason = f"capability_validation_failed:{exc}"
997
+ self.store.save_project(project)
998
+ continue
999
+ if missing:
1000
+ project.status = "blocked"
1001
+ project.reason = "goal_authority_missing:" + json.dumps(
1002
+ missing, sort_keys=True, separators=(",", ":"),
1003
+ )
1004
+ self.store.save_project(project)
1005
+ continue
1006
+ for s in steps:
1007
+ self.store.save_step(s)
1008
+ project.status = "active"
1009
+ project.next_review_at = 0.0
1010
+ self.store.save_project(project)
1011
+ planned += 1
1012
+ logger.info(
1013
+ "Project %s planned[%s]: %d step(s): %s",
1014
+ project.id, mode, len(steps),
1015
+ " | ".join(f"{s.ordinal}.{s.action_kind}:{s.description[:60]}"
1016
+ for s in steps))
1017
+ return planned
1018
+
1019
+ # ------------------------------------------------------------------
1020
+ # Pursuit
1021
+ # ------------------------------------------------------------------
1022
+
1023
+ async def _pursue_active(
1024
+ self, mode: str, held_ids: Optional[Set[str]] = None,
1025
+ ) -> int:
1026
+ dispatched = 0
1027
+ for project in self._eligible_projects(
1028
+ status="active", limit=3, held_ids=held_ids, due=True,
1029
+ ):
1030
+ try:
1031
+ advanced = await self._advance_project(
1032
+ project, mode, held_ids=held_ids,
1033
+ )
1034
+ dispatched += 1 if advanced else 0
1035
+ except Exception:
1036
+ logger.error("project %s advance failed", project.id,
1037
+ exc_info=True)
1038
+ return dispatched
1039
+
1040
+ async def _advance_project(
1041
+ self, project: Project, mode: str,
1042
+ held_ids: Optional[Set[str]] = None,
1043
+ ) -> bool:
1044
+ if self._apply_project_hold(project):
1045
+ if held_ids is not None:
1046
+ held_ids.add(project.id)
1047
+ return False
1048
+ if project.source == "governed_action":
1049
+ hold_reason = self._governed_hold_reason(mode)
1050
+ if hold_reason:
1051
+ project.reason = hold_reason
1052
+ project.next_review_at = time.time() + projects_review_secs()
1053
+ self.store.save_project(project)
1054
+ return False
1055
+ if project.reason in _GOVERNED_HOLD_REASONS:
1056
+ project.reason = ""
1057
+ steps = self.store.steps_for(project.id)
1058
+ if not steps:
1059
+ # active project with no steps: send back to planning
1060
+ project.status = "planning"
1061
+ self.store.save_project(project)
1062
+ return False
1063
+
1064
+ # Terminal check: everything done/skipped -> complete.
1065
+ if all(s.status in ("done", "skipped") for s in steps):
1066
+ await self._complete_project(project, steps, mode)
1067
+ return False
1068
+
1069
+ # A failed step triggers a bounded replan of the remaining work.
1070
+ if any(s.status == "failed" for s in steps):
1071
+ await self._replan_remaining(project, steps, mode)
1072
+ return False
1073
+
1074
+ done_ordinals = {s.ordinal for s in steps if s.status in ("done", "skipped")}
1075
+ ready = [s for s in steps if s.status == "pending"
1076
+ and all(d in done_ordinals for d in s.depends_on)]
1077
+ if not ready:
1078
+ # nothing ready (steps active or deps unmet) -- check again later
1079
+ project.next_review_at = time.time() + projects_review_secs()
1080
+ self.store.save_project(project)
1081
+ return False
1082
+ step = ready[0]
1083
+
1084
+ # A cognition-spine project is bound to the GoalProposal authority.
1085
+ # Recheck at dispatch so a hand-edited or stale plan cannot widen it.
1086
+ if project.source in _AUTHORITY_BOUND_PROJECT_SOURCES:
1087
+ try:
1088
+ gaps = self._authority_gaps(project, [step])
1089
+ missing = gaps.get(step.ordinal, [])
1090
+ except Exception as exc:
1091
+ missing = [f"validation_error:{exc}"]
1092
+ if missing:
1093
+ project.status = "blocked"
1094
+ project.reason = "goal_authority_missing:" + ",".join(missing)
1095
+ self.store.save_project(project)
1096
+ return False
1097
+
1098
+ # Boundary gate on the concrete step.
1099
+ if self._directives is not None:
1100
+ try:
1101
+ verdict = self._directives.check(
1102
+ _step_boundary_action(project, step)
1103
+ )
1104
+ if not verdict.allowed:
1105
+ project.status = "blocked"
1106
+ project.reason = verdict.reason
1107
+ self.store.save_project(project)
1108
+ logger.warning(
1109
+ "Project %s BLOCKED at step %d by boundary: %s",
1110
+ project.id, step.ordinal, verdict.reason)
1111
+ await self._milestone(
1112
+ project, "blocked",
1113
+ f"Step {step.ordinal} ({step.description[:120]}) hit a "
1114
+ f"standing boundary: {verdict.reason}", mode)
1115
+ return False
1116
+ except Exception:
1117
+ project.status = "blocked"
1118
+ project.reason = "boundary_check_failed_closed"
1119
+ self.store.save_project(project)
1120
+ logger.warning(
1121
+ "Project %s BLOCKED at step %d: boundary check failed closed",
1122
+ project.id,
1123
+ step.ordinal,
1124
+ exc_info=True,
1125
+ )
1126
+ return False
1127
+
1128
+ # A boundary check may itself observe or trigger a concurrent mode
1129
+ # transition. Re-evaluate before either shadow bookkeeping or a live
1130
+ # dispatch so the ready step remains pending and exactly resumable.
1131
+ if self._apply_project_hold(project):
1132
+ if held_ids is not None:
1133
+ held_ids.add(project.id)
1134
+ return False
1135
+
1136
+ if mode == "shadow":
1137
+ logger.info(
1138
+ "SHADOW-PROJECT %s step %d/%d [%s]: would dispatch %r "
1139
+ "(boundary=allowed)",
1140
+ project.id, step.ordinal, len(steps), step.action_kind,
1141
+ step.description[:160])
1142
+ step.status = "skipped"
1143
+ step.result = "SKIPPED: SHADOW simulation (no action taken)"
1144
+ self.store.save_step(step)
1145
+ project.next_review_at = time.time() + projects_review_secs()
1146
+ self.store.save_project(project)
1147
+ # re-check terminal state so a finished shadow run completes
1148
+ steps = self.store.steps_for(project.id)
1149
+ if all(s.status in ("done", "skipped") for s in steps):
1150
+ await self._complete_project(project, steps, mode)
1151
+ return True
1152
+
1153
+ # LIVE dispatch through the kind's own gated sub-path.
1154
+ if self._apply_project_hold(project):
1155
+ if held_ids is not None:
1156
+ held_ids.add(project.id)
1157
+ return False
1158
+ step.status = "active"
1159
+ step.attempts += 1
1160
+ self.store.save_step(step)
1161
+ started = time.monotonic()
1162
+ ok, result = await self._dispatch_step(project, step)
1163
+ latency = time.monotonic() - started
1164
+
1165
+ if ok is None:
1166
+ # waiting on an external gate (e.g. directed approval): not an
1167
+ # attempt, re-check at next review.
1168
+ step.status = "pending"
1169
+ step.attempts = max(0, step.attempts - 1)
1170
+ step.result = result[:1000]
1171
+ self.store.save_step(step)
1172
+ elif ok:
1173
+ skipped = result.startswith("SKIPPED:")
1174
+ step.status = "skipped" if skipped else "done"
1175
+ step.result = result[:2000]
1176
+ self.store.save_step(step)
1177
+ if not skipped:
1178
+ receipt = (
1179
+ self.store.get_execution_result(step.result_ref)
1180
+ if step.result_ref else None
1181
+ )
1182
+ if (
1183
+ receipt
1184
+ and receipt.get("terminal_outcome") == "succeeded"
1185
+ and receipt.get("verification_result") == "verified"
1186
+ ):
1187
+ self._record_outcome(
1188
+ "success", latency,
1189
+ stated_confidence=step.confidence,
1190
+ )
1191
+ else:
1192
+ self._record_outcome(
1193
+ "timeout" if "timeout" in (result or "").lower() else "failure",
1194
+ latency, stated_confidence=step.confidence)
1195
+ if step.attempts >= 2:
1196
+ step.status = "failed"
1197
+ step.result = result[:1000]
1198
+ self.store.save_step(step)
1199
+ if self._skills is not None:
1200
+ try:
1201
+ self._skills.record_failure_note(
1202
+ "project",
1203
+ f"step '{step.description[:80]}' failed: {result[:120]}")
1204
+ except Exception:
1205
+ pass
1206
+ else:
1207
+ step.status = "pending"
1208
+ step.result = f"attempt {step.attempts} failed: {result[:500]}"
1209
+ self.store.save_step(step)
1210
+
1211
+ project.next_review_at = time.time() + projects_review_secs()
1212
+ self.store.save_project(project)
1213
+
1214
+ steps = self.store.steps_for(project.id)
1215
+ if all(s.status in ("done", "skipped") for s in steps):
1216
+ await self._complete_project(project, steps, mode)
1217
+ return True
1218
+
1219
+ # ------------------------------------------------------------------
1220
+ # Step dispatch by kind (LIVE mode only)
1221
+ # ------------------------------------------------------------------
1222
+
1223
+ async def _dispatch_step(self, project: Project, step: Step,
1224
+ ) -> Tuple[Optional[bool], str]:
1225
+ if (
1226
+ project.source == "governed_action"
1227
+ and not self._has_canonical_work_order_adapter()
1228
+ ):
1229
+ return None, (
1230
+ "governed_action_requires_canonical_work_order_adapter"
1231
+ )
1232
+ if self._work_orders is not None:
1233
+ completed_refs = tuple(project.provenance_refs()) + tuple(
1234
+ "project-step:%s:%s" % (project.id, item.id)
1235
+ for item in self.store.steps_for(project.id)
1236
+ if item.status == "done"
1237
+ )
1238
+ return await self._work_orders.execute(
1239
+ project, step, context_refs=completed_refs
1240
+ )
1241
+ kind = step.action_kind
1242
+ try:
1243
+ if kind in ("analyze", "research", "internal"):
1244
+ return await self._run_reasoning_step(project, step)
1245
+ if kind == "directed":
1246
+ return await self._run_directed_step(project, step)
1247
+ if kind == "deliver":
1248
+ return await self._run_deliver_step(project, step)
1249
+ except Exception as exc:
1250
+ return False, f"dispatch error: {exc}"
1251
+ return False, f"unknown action_kind {kind}"
1252
+
1253
+ async def _run_reasoning_step(self, project: Project, step: Step,
1254
+ ) -> Tuple[Optional[bool], str]:
1255
+ if self._reasoning is None:
1256
+ return False, "no reasoning loop wired"
1257
+ done = [s for s in self.store.steps_for(project.id)
1258
+ if s.status == "done" and s.result]
1259
+ context = "\n".join(
1260
+ f"- step {s.ordinal}: {s.result[:200]}" for s in done[-5:])
1261
+ prompt = (f"## Project: {project.title}\n"
1262
+ f"Objective: {project.objective[:800]}\n\n"
1263
+ + (f"## Completed so far\n{context}\n\n" if context else "")
1264
+ + f"## Current step ({step.action_kind})\n{step.description}")
1265
+ try:
1266
+ from apsimo.cognition.charter import build_system_prompt
1267
+ system = build_system_prompt(
1268
+ "executor",
1269
+ self_brief=self._self_brief() or None,
1270
+ boundaries=self._boundaries_brief() or None,
1271
+ skills=self._skills_block(step.description) or None,
1272
+ extra=_STEP_SCOPE_CONTEXT)
1273
+ except Exception:
1274
+ logger.debug("charter compose failed; minimal fallback",
1275
+ exc_info=True)
1276
+ system = _STEP_SCOPE_CONTEXT
1277
+ db = self._boundaries_brief()
1278
+ if db:
1279
+ system += ("\n## Standing boundaries from the owner "
1280
+ "(obey without exception)\n" + db)
1281
+
1282
+ working: List[Dict[str, Any]] = [{"role": "user", "content": prompt}]
1283
+ tier = os.environ.get("COLONY_PROJECTS_MODEL_TIER", "small")
1284
+ for _round in range(_MAX_TOOL_ROUNDS):
1285
+ result = await self._reasoning.run_turn(
1286
+ session_id=f"project-{project.id}-s{step.ordinal}",
1287
+ messages=working, system_prompt=system, model_override=tier)
1288
+ if result.status == "completed":
1289
+ text = (result.message or {}).get("content", "") or ""
1290
+ return True, text
1291
+ if result.status == "error":
1292
+ return False, result.error or "reasoning error"
1293
+ if result.status == "needs_tool":
1294
+ pending = list(result.tool_calls or [])
1295
+ if not pending or self._tools is None:
1296
+ return False, "needs_tool with no executable tool path"
1297
+ pending = self._boundary_filter_tools(pending)
1298
+ if not pending:
1299
+ working.append({
1300
+ "role": "user",
1301
+ "content": "Those actions violate a standing boundary "
1302
+ "and were refused. Summarise what you can "
1303
+ "and stop."})
1304
+ continue
1305
+ results = await self._tools.execute_batch(
1306
+ pending, session_id=f"project-{project.id}-s{step.ordinal}")
1307
+ working.append(_assistant_msg(
1308
+ (result.message or {}).get("content", ""), pending))
1309
+ for tr in results:
1310
+ working.append({"role": "tool",
1311
+ "tool_call_id": tr.get("tool_call_id", ""),
1312
+ "content": tr.get("content", "")})
1313
+ continue
1314
+ return False, f"unexpected reasoning status {result.status}"
1315
+ return False, f"tool-loop cap reached ({_MAX_TOOL_ROUNDS} rounds)"
1316
+
1317
+ def _boundary_filter_tools(self, pending: List[dict]) -> List[dict]:
1318
+ if self._directives is None:
1319
+ return pending
1320
+ try:
1321
+ from apsimo.directives import Action
1322
+ except Exception:
1323
+ logger.warning(
1324
+ "Project tool batch blocked: boundary action contract unavailable",
1325
+ exc_info=True,
1326
+ )
1327
+ return []
1328
+ survivors = []
1329
+ for tc in pending:
1330
+ name = tc.get("name", "")
1331
+ args = tc.get("arguments", {}) if isinstance(
1332
+ tc.get("arguments"), dict) else {}
1333
+ try:
1334
+ verdict = self._directives.check(Action(
1335
+ kind="execute_tool", tool_name=name, args=args, text=name,
1336
+ high_risk=True))
1337
+ except Exception:
1338
+ logger.warning(
1339
+ "Project tool call %s REFUSED: boundary check failed closed",
1340
+ name,
1341
+ exc_info=True,
1342
+ )
1343
+ continue
1344
+ if getattr(verdict, "allowed", None) is not True:
1345
+ logger.warning("Project tool call %s REFUSED by boundary: %s",
1346
+ name, getattr(verdict, "reason", "invalid_verdict"))
1347
+ continue
1348
+ survivors.append(tc)
1349
+ return survivors
1350
+
1351
+ async def _run_directed_step(self, project: Project, step: Step,
1352
+ ) -> Tuple[Optional[bool], str]:
1353
+ if self._directed is None:
1354
+ return False, "no directed service wired"
1355
+ # An existing awaiting-approval task for this step: keep waiting.
1356
+ prior = (step.result or "")
1357
+ if prior.startswith("awaiting_approval:"):
1358
+ task_id = prior.split(":", 1)[1].strip()
1359
+ task = self._directed.store.get(task_id)
1360
+ if task is not None:
1361
+ if task.status == "awaiting_approval":
1362
+ return None, prior
1363
+ if task.status in ("approved",):
1364
+ out = await self._directed.dispatch(task.id)
1365
+ return True, f"directed task {task.id} dispatched: {json.dumps(out, default=str)[:300]}"
1366
+ if task.status in ("dispatched", "dispatched_dry", "completed"):
1367
+ return True, f"directed task {task.id} {task.status}"
1368
+ if task.status in ("refused", "violated", "failed", "expired"):
1369
+ return False, f"directed task {task.id} {task.status}: {task.refusal_reason}"
1370
+ task = await self._directed.intake(step.description)
1371
+ if task.status == "refused":
1372
+ return False, f"directed intake refused: {task.refusal_reason}"
1373
+ if task.status == "awaiting_approval":
1374
+ return None, f"awaiting_approval:{task.id}"
1375
+ out = await self._directed.dispatch(task.id)
1376
+ status = "dry_run" if out.get("dry_run") else (
1377
+ "dispatched" if out.get("dispatched") else
1378
+ f"not dispatched ({out.get('reason', '?')})")
1379
+ ok = bool(out.get("dispatched") or out.get("dry_run"))
1380
+ return (True, f"directed task {task.id}: {status}") if ok else (
1381
+ False, f"directed task {task.id}: {status}")
1382
+
1383
+ async def _run_deliver_step(self, project: Project, step: Step,
1384
+ ) -> Tuple[Optional[bool], str]:
1385
+ done = [s for s in self.store.steps_for(project.id)
1386
+ if s.status == "done" and s.result
1387
+ and not s.result.startswith("SHADOW")]
1388
+ finding = "\n".join(
1389
+ f"- {s.result[:300]}" for s in done[-4:]) or project.objective[:300]
1390
+ try:
1391
+ from apsimo.proposals import Proposal, proposal_to_payload
1392
+ prop = Proposal(
1393
+ title=f"Project update: {project.title[:70]}",
1394
+ finding=f"{step.description[:200]}\n{finding}"[:1200],
1395
+ why_it_helps=f"progress on your project: {project.title[:100]}",
1396
+ suggested_action="Tell me to continue, adjust, or stop this project.",
1397
+ source=project.id, initiative_type="proposal", confidence=0.7)
1398
+ if self._proposals is not None:
1399
+ self._proposals.add(prop)
1400
+ if self._deliver is not None:
1401
+ ok = bool(await self._deliver(proposal_to_payload(prop)))
1402
+ return True, ("delivered" if ok else
1403
+ "routed to guarded delivery (held/gated)")
1404
+ return True, "proposal stored (no delivery router)"
1405
+ except Exception as exc:
1406
+ return False, f"deliver failed: {exc}"
1407
+
1408
+ # ------------------------------------------------------------------
1409
+ # Replan / complete / milestones
1410
+ # ------------------------------------------------------------------
1411
+
1412
+ async def _replan_remaining(self, project: Project, steps: List[Step],
1413
+ mode: str) -> None:
1414
+ from apsimo.projects.planner import plan_project
1415
+ next_replans = project.replans + 1
1416
+ if next_replans > projects_max_replans():
1417
+ project.replans = next_replans
1418
+ project.status = "abandoned"
1419
+ project.reason = "replan_limit"
1420
+ self.store.save_project(project)
1421
+ self._record_outcome("failure")
1422
+ if self._feedback is not None:
1423
+ try:
1424
+ self._feedback.record("project", "dismissed")
1425
+ except Exception:
1426
+ pass
1427
+ await self._milestone(
1428
+ project, "abandoned",
1429
+ f"replan limit reached after {project.replans - 1} replans", mode)
1430
+ return
1431
+
1432
+ done = [s for s in steps if s.status == "done"]
1433
+ failed = [s for s in steps if s.status == "failed"]
1434
+ context = ""
1435
+ if done:
1436
+ context += "Completed steps:\n" + "\n".join(
1437
+ f"- {s.description[:120]}: {s.result[:150]}" for s in done[-5:])
1438
+ if failed:
1439
+ context += "\nFailed steps (plan around these failures):\n" + "\n".join(
1440
+ f"- {s.description[:120]}: {s.result[:150]}" for s in failed[-3:])
1441
+
1442
+ new_steps = await plan_project(
1443
+ self._router,
1444
+ f"{project.objective}\n\nRe-plan ONLY the remaining work.",
1445
+ project_id=project.id, context=context,
1446
+ skills_block=self._skills_block(project.objective),
1447
+ self_brief=self._self_brief(),
1448
+ boundaries=self._boundaries_brief())
1449
+ if self._apply_project_hold(project):
1450
+ return
1451
+ project.replans = next_replans
1452
+ if project.source in _AUTHORITY_BOUND_PROJECT_SOURCES:
1453
+ try:
1454
+ missing = self._authority_gaps(project, new_steps)
1455
+ except Exception as exc:
1456
+ project.status = "blocked"
1457
+ project.reason = f"capability_validation_failed:{exc}"
1458
+ self.store.save_project(project)
1459
+ return
1460
+ if missing:
1461
+ project.status = "blocked"
1462
+ project.reason = "goal_authority_missing:" + json.dumps(
1463
+ missing, sort_keys=True, separators=(",", ":"),
1464
+ )
1465
+ self.store.save_project(project)
1466
+ return
1467
+ self.store.delete_steps(project.id, statuses=["pending", "failed", "active"])
1468
+ base = max((s.ordinal for s in done), default=0)
1469
+ for s in new_steps:
1470
+ s.ordinal += base
1471
+ s.depends_on = [d + base for d in s.depends_on]
1472
+ self.store.save_step(s)
1473
+ project.next_review_at = time.time() + projects_review_secs()
1474
+ if not new_steps and not done:
1475
+ project.status = "abandoned"
1476
+ project.reason = "replan_produced_no_steps"
1477
+ self._record_outcome("failure")
1478
+ elif not new_steps:
1479
+ # nothing left to do beyond what completed
1480
+ self.store.save_project(project)
1481
+ await self._complete_project(
1482
+ project, self.store.steps_for(project.id), mode)
1483
+ return
1484
+ self.store.save_project(project)
1485
+ logger.info("Project %s replanned (replan %d/%d): %d new step(s)",
1486
+ project.id, project.replans, projects_max_replans(),
1487
+ len(new_steps))
1488
+
1489
+ async def _complete_project(self, project: Project, steps: List[Step],
1490
+ mode: str) -> None:
1491
+ if project.status == "completed":
1492
+ return
1493
+ project.status = "completed"
1494
+ done = [step for step in steps if step.status == "done"]
1495
+ skipped = [step for step in steps if step.status == "skipped"]
1496
+ all_steps_done = bool(steps) and len(done) == len(steps)
1497
+ receipts_verified = all_steps_done
1498
+ if receipts_verified:
1499
+ for step in done:
1500
+ result = (
1501
+ self.store.get_execution_result(step.result_ref)
1502
+ if step.result_ref else None
1503
+ )
1504
+ payload = result.get("payload", {}) if result else {}
1505
+ if (
1506
+ not result
1507
+ or result.get("terminal_outcome") != "succeeded"
1508
+ or result.get("verification_result") != "verified"
1509
+ or not payload.get("receipt_refs")
1510
+ ):
1511
+ receipts_verified = False
1512
+ break
1513
+ if all_steps_done:
1514
+ project.reason = "all_steps_done"
1515
+ elif done:
1516
+ project.reason = "completed_with_skips"
1517
+ else:
1518
+ project.reason = "all_steps_skipped"
1519
+ if mode == "shadow" or not all_steps_done:
1520
+ project.outcome = "neutral"
1521
+ elif receipts_verified:
1522
+ project.outcome = "succeeded"
1523
+ else:
1524
+ project.outcome = "unverified"
1525
+ self.store.save_project(project)
1526
+ # A skip means no outcome was observed. It is neither success nor
1527
+ # failure and must never graduate competence/trust. In particular,
1528
+ # shadow simulation is audit evidence, not a successful action.
1529
+ if project.outcome == "succeeded":
1530
+ self._record_outcome("success", shadow=(mode == "shadow"))
1531
+ if project.outcome == "succeeded" and self._feedback is not None:
1532
+ try:
1533
+ self._feedback.record("project", "actioned")
1534
+ except Exception:
1535
+ pass
1536
+ summary = "; ".join(
1537
+ s.result[:100] for s in steps if s.status == "done" and s.result)[:800]
1538
+ event = (
1539
+ "completed" if project.outcome == "succeeded"
1540
+ else "unverified" if all_steps_done and mode != "shadow"
1541
+ else "skipped"
1542
+ )
1543
+ neutral_summary = (
1544
+ f"{len(done)} completed step(s); {len(skipped)} skipped; "
1545
+ "no receipt-backed overall success recorded"
1546
+ )
1547
+ await self._milestone(
1548
+ project, event,
1549
+ summary if project.outcome == "succeeded" and summary else neutral_summary,
1550
+ mode,
1551
+ )
1552
+ # Skill hook (item 3): non-trivial completion -> distill a procedure.
1553
+ if project.outcome == "succeeded" and len(steps) >= 3 and mode == "live":
1554
+ try:
1555
+ from apsimo.skills_memory import distill_from_completion
1556
+ transcript = "\n".join(
1557
+ f"step {s.ordinal} ({s.action_kind}): {s.description[:150]} "
1558
+ f"-> {s.result[:200]}" for s in steps)
1559
+ await distill_from_completion(
1560
+ self._router, self._skills, domain="project",
1561
+ task_text=project.objective, result_text=transcript,
1562
+ source_ref=project.id)
1563
+ except Exception:
1564
+ logger.debug("project skill distillation failed", exc_info=True)
1565
+ logger.info(
1566
+ "Project %s %s[%s]: %s (done=%d skipped=%d)",
1567
+ project.id,
1568
+ "COMPLETED" if project.outcome == "succeeded" else "NEUTRAL",
1569
+ mode,
1570
+ project.title,
1571
+ len(done),
1572
+ len(skipped),
1573
+ )
1574
+
1575
+ async def _milestone(self, project: Project, event: str, detail: str,
1576
+ mode: str) -> None:
1577
+ """Status-change report as a Proposal. Shadow: stored + logged only."""
1578
+ try:
1579
+ from apsimo.proposals import Proposal, proposal_to_payload
1580
+ prop = Proposal(
1581
+ title=f"Project {event}: {project.title[:60]}",
1582
+ finding=detail[:1000],
1583
+ why_it_helps=("keeps you in control of work I am pursuing "
1584
+ "on your behalf"),
1585
+ suggested_action=(
1586
+ "Review and tell me whether to proceed differently"
1587
+ if event in ("blocked", "abandoned")
1588
+ else "Review the outcome; tell me any follow-ups"),
1589
+ source=project.id, initiative_type="proposal",
1590
+ confidence=0.85 if event == "blocked" else 0.7)
1591
+ if self._proposals is not None:
1592
+ self._proposals.add(prop)
1593
+ if mode == "shadow" or self._deliver is None:
1594
+ logger.info(
1595
+ "SHADOW-PROJECT-MILESTONE %s [%s]: %s -- %s",
1596
+ project.id, event, prop.title, detail[:200])
1597
+ return
1598
+ await self._deliver(proposal_to_payload(prop))
1599
+ except Exception:
1600
+ logger.debug("project milestone failed", exc_info=True)
1601
+
1602
+ # ------------------------------------------------------------------
1603
+ # Helpers
1604
+ # ------------------------------------------------------------------
1605
+
1606
+ def _skills_block(self, situation: str) -> str:
1607
+ if self._skills is None:
1608
+ return ""
1609
+ try:
1610
+ from apsimo.skills_memory import (
1611
+ format_block, relevant_skills, skills_enabled,
1612
+ )
1613
+ if not skills_enabled():
1614
+ return ""
1615
+ return format_block(
1616
+ relevant_skills(self._skills, situation, k=3, domain="project"),
1617
+ strategy_note=self._skills.get_note("project"))
1618
+ except Exception:
1619
+ return ""
1620
+
1621
+ def _boundaries_brief(self) -> str:
1622
+ if self._directives is None:
1623
+ return ""
1624
+ try:
1625
+ return self._directives.context_brief() or ""
1626
+ except Exception:
1627
+ return ""
1628
+
1629
+ def _self_brief(self) -> str:
1630
+ if self._self_model is None:
1631
+ return ""
1632
+ try:
1633
+ return self._self_model.brief()
1634
+ except Exception:
1635
+ return ""
1636
+
1637
+ def _record_outcome(self, outcome: str,
1638
+ latency: Optional[float] = None,
1639
+ shadow: bool = False,
1640
+ stated_confidence: Optional[float] = None) -> None:
1641
+ if self._self_model is None:
1642
+ return
1643
+ try:
1644
+ from apsimo.cognition.evidence_pipeline import (
1645
+ cognition_evidence_mode,
1646
+ )
1647
+ if cognition_evidence_mode() in {"shadow", "live"}:
1648
+ # The receipt-bound reducer is the sole project-competence
1649
+ # writer in live mode, while shadow must remain observation-
1650
+ # only. Direct in-process status is not durable evidence.
1651
+ return
1652
+ except Exception:
1653
+ # Import/configuration failure must not manufacture a new direct
1654
+ # success while the evidence path is requested live.
1655
+ if os.environ.get(
1656
+ "COLONY_COGNITION_EVIDENCE", "off"
1657
+ ).strip().lower() in {"shadow", "live"}:
1658
+ return
1659
+ try:
1660
+ self._self_model.record("project", outcome, latency_secs=latency,
1661
+ shadow=shadow,
1662
+ stated_confidence=stated_confidence)
1663
+ except Exception:
1664
+ pass
1665
+
1666
+
1667
+ def _assistant_msg(content: str, tool_calls: List[dict]) -> Dict[str, Any]:
1668
+ """OpenAI-shaped assistant message carrying tool calls (mirrors the
1669
+ initiative executor's continuation shape)."""
1670
+ return {
1671
+ "role": "assistant",
1672
+ "content": content or None,
1673
+ "tool_calls": [
1674
+ {"id": tc.get("id", ""), "type": "function",
1675
+ "function": {"name": tc.get("name", ""),
1676
+ "arguments": (tc["arguments"]
1677
+ if isinstance(tc.get("arguments"), str)
1678
+ else json.dumps(tc.get("arguments", {})))}}
1679
+ for tc in tool_calls
1680
+ ],
1681
+ }