@penclipai/server 2026.704.0 → 2026.714.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/process/execute.d.ts.map +1 -1
- package/dist/adapters/process/execute.js +1 -0
- package/dist/adapters/process/execute.js.map +1 -1
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +79 -51
- package/dist/adapters/registry.js.map +1 -1
- package/dist/adapters/utils.d.ts +5 -0
- package/dist/adapters/utils.d.ts.map +1 -1
- package/dist/adapters/utils.js.map +1 -1
- package/dist/agent-auth-jwt.d.ts +5 -1
- package/dist/agent-auth-jwt.d.ts.map +1 -1
- package/dist/agent-auth-jwt.js +72 -20
- package/dist/agent-auth-jwt.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +10 -0
- package/dist/app.js.map +1 -1
- package/dist/built-ins/agents/reflection-coach/AGENTS.md +47 -0
- package/dist/built-ins/agents/reflection-coach/routines/recent-agent-reflection.md +77 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +208 -116
- package/dist/index.js.map +1 -1
- package/dist/middleware/api-compression.d.ts +8 -0
- package/dist/middleware/api-compression.d.ts.map +1 -0
- package/dist/middleware/api-compression.js +207 -0
- package/dist/middleware/api-compression.js.map +1 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +130 -3
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/error-handler.d.ts.map +1 -1
- package/dist/middleware/error-handler.js +27 -0
- package/dist/middleware/error-handler.js.map +1 -1
- package/dist/onboarding-assets/default/AGENTS.md +1 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +11 -0
- package/dist/redaction.js.map +1 -1
- package/dist/routes/access.d.ts.map +1 -1
- package/dist/routes/access.js +4 -3
- package/dist/routes/access.js.map +1 -1
- package/dist/routes/adapters.d.ts.map +1 -1
- package/dist/routes/adapters.js +2 -0
- package/dist/routes/adapters.js.map +1 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +222 -87
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/attention.d.ts +3 -0
- package/dist/routes/attention.d.ts.map +1 -0
- package/dist/routes/attention.js +24 -0
- package/dist/routes/attention.js.map +1 -0
- package/dist/routes/authz.d.ts.map +1 -1
- package/dist/routes/authz.js +29 -1
- package/dist/routes/authz.js.map +1 -1
- package/dist/routes/board-chat.d.ts.map +1 -1
- package/dist/routes/board-chat.js +4 -1
- package/dist/routes/board-chat.js.map +1 -1
- package/dist/routes/built-in-agents.d.ts +3 -0
- package/dist/routes/built-in-agents.d.ts.map +1 -0
- package/dist/routes/built-in-agents.js +267 -0
- package/dist/routes/built-in-agents.js.map +1 -0
- package/dist/routes/cases.d.ts +46 -0
- package/dist/routes/cases.d.ts.map +1 -0
- package/dist/routes/cases.js +1395 -0
- package/dist/routes/cases.js.map +1 -0
- package/dist/routes/companies.d.ts.map +1 -1
- package/dist/routes/companies.js +4 -1
- package/dist/routes/companies.js.map +1 -1
- package/dist/routes/company-skills.d.ts.map +1 -1
- package/dist/routes/company-skills.js +453 -33
- package/dist/routes/company-skills.js.map +1 -1
- package/dist/routes/environments.d.ts.map +1 -1
- package/dist/routes/environments.js +71 -8
- package/dist/routes/environments.js.map +1 -1
- package/dist/routes/execution-workspaces.d.ts.map +1 -1
- package/dist/routes/execution-workspaces.js +89 -3
- package/dist/routes/execution-workspaces.js.map +1 -1
- package/dist/routes/health.d.ts +2 -0
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +30 -0
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/inbox-dismissals.d.ts.map +1 -1
- package/dist/routes/inbox-dismissals.js +80 -19
- package/dist/routes/inbox-dismissals.js.map +1 -1
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/issues.d.ts +25 -0
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +1455 -46
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/openapi.d.ts.map +1 -1
- package/dist/routes/openapi.js +448 -11
- package/dist/routes/openapi.js.map +1 -1
- package/dist/routes/pipelines.js +2 -2
- package/dist/routes/pipelines.js.map +1 -1
- package/dist/routes/resource-memberships.d.ts.map +1 -1
- package/dist/routes/resource-memberships.js +13 -5
- package/dist/routes/resource-memberships.js.map +1 -1
- package/dist/routes/secrets.d.ts.map +1 -1
- package/dist/routes/secrets.js +261 -1
- package/dist/routes/secrets.js.map +1 -1
- package/dist/routes/sidebar-badges.d.ts.map +1 -1
- package/dist/routes/sidebar-badges.js +18 -2
- package/dist/routes/sidebar-badges.js.map +1 -1
- package/dist/routes/user-profiles.d.ts.map +1 -1
- package/dist/routes/user-profiles.js +5 -4
- package/dist/routes/user-profiles.js.map +1 -1
- package/dist/server-info.d.ts +2 -0
- package/dist/server-info.d.ts.map +1 -1
- package/dist/server-info.js +20 -5
- package/dist/server-info.js.map +1 -1
- package/dist/services/access.d.ts +4 -4
- package/dist/services/access.d.ts.map +1 -1
- package/dist/services/activity.d.ts +3 -3
- package/dist/services/activity.d.ts.map +1 -1
- package/dist/services/activity.js +5 -4
- package/dist/services/activity.js.map +1 -1
- package/dist/services/adapter-registry-bootstrap.reconcile.test.js +1 -0
- package/dist/services/adapter-registry-bootstrap.reconcile.test.js.map +1 -1
- package/dist/services/agent-secret-bindings.d.ts +15 -0
- package/dist/services/agent-secret-bindings.d.ts.map +1 -1
- package/dist/services/agent-secret-bindings.js +43 -0
- package/dist/services/agent-secret-bindings.js.map +1 -1
- package/dist/services/agents.d.ts +194 -133
- package/dist/services/agents.d.ts.map +1 -1
- package/dist/services/agents.js +107 -4
- package/dist/services/agents.js.map +1 -1
- package/dist/services/approvals.d.ts +10 -10
- package/dist/services/approvals.d.ts.map +1 -1
- package/dist/services/approvals.js +9 -1
- package/dist/services/approvals.js.map +1 -1
- package/dist/services/assets.d.ts +5 -5
- package/dist/services/attention.d.ts +11 -0
- package/dist/services/attention.d.ts.map +1 -0
- package/dist/services/attention.js +1024 -0
- package/dist/services/attention.js.map +1 -0
- package/dist/services/authorization.d.ts +15 -2
- package/dist/services/authorization.d.ts.map +1 -1
- package/dist/services/authorization.js +324 -8
- package/dist/services/authorization.js.map +1 -1
- package/dist/services/board-auth.d.ts +2 -2
- package/dist/services/budgets.d.ts.map +1 -1
- package/dist/services/budgets.js +8 -7
- package/dist/services/budgets.js.map +1 -1
- package/dist/services/built-in-agent-metadata.d.ts +9 -0
- package/dist/services/built-in-agent-metadata.d.ts.map +1 -0
- package/dist/services/built-in-agent-metadata.js +39 -0
- package/dist/services/built-in-agent-metadata.js.map +1 -0
- package/dist/services/built-in-agents.d.ts +394 -0
- package/dist/services/built-in-agents.d.ts.map +1 -0
- package/dist/services/built-in-agents.js +1426 -0
- package/dist/services/built-in-agents.js.map +1 -0
- package/dist/services/change-consent-gate.d.ts +18 -0
- package/dist/services/change-consent-gate.d.ts.map +1 -0
- package/dist/services/change-consent-gate.js +170 -0
- package/dist/services/change-consent-gate.js.map +1 -0
- package/dist/services/companies.d.ts +14 -8
- package/dist/services/companies.d.ts.map +1 -1
- package/dist/services/companies.js +6 -1
- package/dist/services/companies.js.map +1 -1
- package/dist/services/company-artifacts.d.ts +4 -1
- package/dist/services/company-artifacts.d.ts.map +1 -1
- package/dist/services/company-artifacts.js +9 -1
- package/dist/services/company-artifacts.js.map +1 -1
- package/dist/services/company-member-roles.d.ts.map +1 -1
- package/dist/services/company-member-roles.js +2 -0
- package/dist/services/company-member-roles.js.map +1 -1
- package/dist/services/company-portability.d.ts +1 -1
- package/dist/services/company-portability.d.ts.map +1 -1
- package/dist/services/company-portability.js +113 -8
- package/dist/services/company-portability.js.map +1 -1
- package/dist/services/company-search.d.ts.map +1 -1
- package/dist/services/company-search.js +843 -294
- package/dist/services/company-search.js.map +1 -1
- package/dist/services/company-skills.d.ts +64 -3
- package/dist/services/company-skills.d.ts.map +1 -1
- package/dist/services/company-skills.js +1205 -20
- package/dist/services/company-skills.js.map +1 -1
- package/dist/services/costs.d.ts +9 -8
- package/dist/services/costs.d.ts.map +1 -1
- package/dist/services/costs.js +9 -2
- package/dist/services/costs.js.map +1 -1
- package/dist/services/dashboard.d.ts +2 -0
- package/dist/services/dashboard.d.ts.map +1 -1
- package/dist/services/dashboard.js +60 -17
- package/dist/services/dashboard.js.map +1 -1
- package/dist/services/database-backup-health.d.ts +34 -0
- package/dist/services/database-backup-health.d.ts.map +1 -0
- package/dist/services/database-backup-health.js +104 -0
- package/dist/services/database-backup-health.js.map +1 -0
- package/dist/services/document-annotations.d.ts +169 -1
- package/dist/services/document-annotations.d.ts.map +1 -1
- package/dist/services/document-annotations.js +272 -1
- package/dist/services/document-annotations.js.map +1 -1
- package/dist/services/documents.d.ts +368 -0
- package/dist/services/documents.d.ts.map +1 -1
- package/dist/services/documents.js +2 -2
- package/dist/services/documents.js.map +1 -1
- package/dist/services/environment-config.d.ts.map +1 -1
- package/dist/services/environment-config.js +6 -0
- package/dist/services/environment-config.js.map +1 -1
- package/dist/services/environment-custom-image-runtime.d.ts +28 -0
- package/dist/services/environment-custom-image-runtime.d.ts.map +1 -1
- package/dist/services/environment-custom-image-runtime.js +106 -7
- package/dist/services/environment-custom-image-runtime.js.map +1 -1
- package/dist/services/environment-custom-images.d.ts +31 -1
- package/dist/services/environment-custom-images.d.ts.map +1 -1
- package/dist/services/environment-custom-images.js +110 -4
- package/dist/services/environment-custom-images.js.map +1 -1
- package/dist/services/environment-execution-target.d.ts.map +1 -1
- package/dist/services/environment-execution-target.js +1 -3
- package/dist/services/environment-execution-target.js.map +1 -1
- package/dist/services/environment-probe.d.ts +1 -0
- package/dist/services/environment-probe.d.ts.map +1 -1
- package/dist/services/environment-probe.js +99 -5
- package/dist/services/environment-probe.js.map +1 -1
- package/dist/services/environments.d.ts +3 -1
- package/dist/services/environments.d.ts.map +1 -1
- package/dist/services/environments.js +98 -3
- package/dist/services/environments.js.map +1 -1
- package/dist/services/execution-workspace-policy.d.ts +29 -3
- package/dist/services/execution-workspace-policy.d.ts.map +1 -1
- package/dist/services/execution-workspace-policy.js +44 -1
- package/dist/services/execution-workspace-policy.js.map +1 -1
- package/dist/services/execution-workspaces.d.ts +64 -1
- package/dist/services/execution-workspaces.d.ts.map +1 -1
- package/dist/services/execution-workspaces.js +634 -3
- package/dist/services/execution-workspaces.js.map +1 -1
- package/dist/services/external-objects.d.ts +26 -26
- package/dist/services/feedback.d.ts +2 -2
- package/dist/services/finance.d.ts +5 -5
- package/dist/services/goals.d.ts +8 -8
- package/dist/services/heartbeat-stop-metadata.d.ts +2 -2
- package/dist/services/heartbeat-stop-metadata.d.ts.map +1 -1
- package/dist/services/heartbeat-stop-metadata.js +4 -0
- package/dist/services/heartbeat-stop-metadata.js.map +1 -1
- package/dist/services/heartbeat-stop-metadata.test.js +9 -0
- package/dist/services/heartbeat-stop-metadata.test.js.map +1 -1
- package/dist/services/heartbeat.d.ts +133 -28
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +1963 -239
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/inbox-dismissals.d.ts +26 -0
- package/dist/services/inbox-dismissals.d.ts.map +1 -1
- package/dist/services/inbox-dismissals.js +33 -18
- package/dist/services/inbox-dismissals.js.map +1 -1
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/instance-settings.d.ts +25 -1
- package/dist/services/instance-settings.d.ts.map +1 -1
- package/dist/services/instance-settings.js +103 -5
- package/dist/services/instance-settings.js.map +1 -1
- package/dist/services/issue-continuation-summary.js +1 -1
- package/dist/services/issue-continuation-summary.js.map +1 -1
- package/dist/services/issue-recovery-actions.d.ts +3 -0
- package/dist/services/issue-recovery-actions.d.ts.map +1 -1
- package/dist/services/issue-recovery-actions.js +9 -0
- package/dist/services/issue-recovery-actions.js.map +1 -1
- package/dist/services/issue-rewake-throttle.d.ts +92 -0
- package/dist/services/issue-rewake-throttle.d.ts.map +1 -0
- package/dist/services/issue-rewake-throttle.js +139 -0
- package/dist/services/issue-rewake-throttle.js.map +1 -0
- package/dist/services/issue-thread-interactions.d.ts +8 -1
- package/dist/services/issue-thread-interactions.d.ts.map +1 -1
- package/dist/services/issue-thread-interactions.js +231 -16
- package/dist/services/issue-thread-interactions.js.map +1 -1
- package/dist/services/issue-visibility.d.ts +4 -0
- package/dist/services/issue-visibility.d.ts.map +1 -0
- package/dist/services/issue-visibility.js +9 -0
- package/dist/services/issue-visibility.js.map +1 -0
- package/dist/services/issues.d.ts +225 -105
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +540 -18
- package/dist/services/issues.js.map +1 -1
- package/dist/services/local-service-supervisor.d.ts +3 -0
- package/dist/services/local-service-supervisor.d.ts.map +1 -1
- package/dist/services/local-service-supervisor.js +51 -0
- package/dist/services/local-service-supervisor.js.map +1 -1
- package/dist/services/pipeline-case-outputs.d.ts.map +1 -1
- package/dist/services/pipeline-case-outputs.js +2 -1
- package/dist/services/pipeline-case-outputs.js.map +1 -1
- package/dist/services/pipelines-aggregation.d.ts.map +1 -1
- package/dist/services/pipelines-aggregation.js +3 -2
- package/dist/services/pipelines-aggregation.js.map +1 -1
- package/dist/services/pipelines.d.ts +165 -165
- package/dist/services/pipelines.d.ts.map +1 -1
- package/dist/services/pipelines.js +6 -4
- package/dist/services/pipelines.js.map +1 -1
- package/dist/services/plugin-database.d.ts +2 -2
- package/dist/services/plugin-environment-driver.d.ts +1 -1
- package/dist/services/plugin-host-services.d.ts.map +1 -1
- package/dist/services/plugin-host-services.js +3 -1
- package/dist/services/plugin-host-services.js.map +1 -1
- package/dist/services/plugin-managed-routines.d.ts +1 -0
- package/dist/services/plugin-managed-routines.d.ts.map +1 -1
- package/dist/services/plugin-registry.d.ts +21 -21
- package/dist/services/productivity-review.d.ts +1 -0
- package/dist/services/productivity-review.d.ts.map +1 -1
- package/dist/services/productivity-review.js +6 -5
- package/dist/services/productivity-review.js.map +1 -1
- package/dist/services/projects.d.ts +9 -9
- package/dist/services/recovery/service.d.ts +82 -54
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +200 -35
- package/dist/services/recovery/service.js.map +1 -1
- package/dist/services/recovery/successful-run-handoff.d.ts +1 -0
- package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
- package/dist/services/recovery/successful-run-handoff.js +2 -0
- package/dist/services/recovery/successful-run-handoff.js.map +1 -1
- package/dist/services/recovery/successful-run-handoff.test.js +20 -0
- package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
- package/dist/services/resource-memberships.d.ts +13 -10
- package/dist/services/resource-memberships.d.ts.map +1 -1
- package/dist/services/resource-memberships.js +89 -20
- package/dist/services/resource-memberships.js.map +1 -1
- package/dist/services/responsible-user-denial-run-outcomes.d.ts +60 -0
- package/dist/services/responsible-user-denial-run-outcomes.d.ts.map +1 -0
- package/dist/services/responsible-user-denial-run-outcomes.js +56 -0
- package/dist/services/responsible-user-denial-run-outcomes.js.map +1 -0
- package/dist/services/responsible-user-denial-run-outcomes.test.d.ts +2 -0
- package/dist/services/responsible-user-denial-run-outcomes.test.d.ts.map +1 -0
- package/dist/services/responsible-user-denial-run-outcomes.test.js +75 -0
- package/dist/services/responsible-user-denial-run-outcomes.test.js.map +1 -0
- package/dist/services/routines.d.ts +30 -8
- package/dist/services/routines.d.ts.map +1 -1
- package/dist/services/routines.js +241 -18
- package/dist/services/routines.js.map +1 -1
- package/dist/services/run-liveness.d.ts.map +1 -1
- package/dist/services/run-liveness.js +21 -0
- package/dist/services/run-liveness.js.map +1 -1
- package/dist/services/run-log-store.d.ts +1 -0
- package/dist/services/run-log-store.d.ts.map +1 -1
- package/dist/services/run-log-store.js +4 -0
- package/dist/services/run-log-store.js.map +1 -1
- package/dist/services/run-scratch.d.ts +42 -0
- package/dist/services/run-scratch.d.ts.map +1 -0
- package/dist/services/run-scratch.js +111 -0
- package/dist/services/run-scratch.js.map +1 -0
- package/dist/services/run-scratch.test.d.ts +2 -0
- package/dist/services/run-scratch.test.d.ts.map +1 -0
- package/dist/services/run-scratch.test.js +98 -0
- package/dist/services/run-scratch.test.js.map +1 -0
- package/dist/services/secrets.d.ts +1261 -64
- package/dist/services/secrets.d.ts.map +1 -1
- package/dist/services/secrets.js +1089 -96
- package/dist/services/secrets.js.map +1 -1
- package/dist/services/task-watchdogs.d.ts +1 -0
- package/dist/services/task-watchdogs.d.ts.map +1 -1
- package/dist/services/task-watchdogs.js +78 -9
- package/dist/services/task-watchdogs.js.map +1 -1
- package/dist/services/work-timeline.d.ts.map +1 -1
- package/dist/services/work-timeline.js +34 -2
- package/dist/services/work-timeline.js.map +1 -1
- package/dist/services/workspace-runtime-read-model.d.ts +30 -29
- package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
- package/dist/services/workspace-runtime-read-model.js.map +1 -1
- package/dist/services/workspace-runtime.d.ts +59 -13
- package/dist/services/workspace-runtime.d.ts.map +1 -1
- package/dist/services/workspace-runtime.js +1121 -102
- package/dist/services/workspace-runtime.js.map +1 -1
- package/dist/skills-catalog/catalog/bundled/paperclip-operations/reflection-coach/SKILL.md +202 -0
- package/dist/skills-catalog/catalog/bundled/product/paperclip-capsules/SKILL.md +1 -1
- package/dist/skills-catalog/catalog/bundled/product/wireframe/SKILL.md +1 -1
- package/dist/skills-catalog/catalog/optional/content/release-announcement/SKILL.md +90 -0
- package/dist/skills-catalog/catalog/optional/finance/ramp/SKILL.md +98 -0
- package/dist/skills-catalog/generated/catalog.json +84 -12
- package/dist/ui-branding.d.ts +7 -0
- package/dist/ui-branding.d.ts.map +1 -1
- package/dist/ui-branding.js +8 -2
- package/dist/ui-branding.js.map +1 -1
- package/dist/version.d.ts +14 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +114 -3
- package/dist/version.js.map +1 -1
- package/package.json +19 -20
- package/skills/paperclip/SKILL.md +45 -10
- package/skills/paperclip/references/api-reference.md +246 -1
- package/skills/paperclip/references/cases.md +295 -0
- package/skills/paperclip/references/company-skills.md +1 -1
- package/skills/paperclip-board/SKILL.md +3 -4
- package/skills/paperclip-converting-plans-to-tasks/SKILL.md +3 -7
- package/skills/para-memory-files/SKILL.md +3 -7
- package/ui-dist/assets/{abnfDiagram-VRR7QNED-BsFz2IiG.js → abnfDiagram-VRR7QNED-Dtgfy1PK.js} +1 -1
- package/ui-dist/assets/{arc-HZhAwbOs.js → arc-BGdvNvs8.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-BteMjuOn.js → architectureDiagram-ZJ3FMSHR-CK19SQDx.js} +1 -1
- package/ui-dist/assets/{blockDiagram-677ZJIJ3-Iq66uZjf.js → blockDiagram-677ZJIJ3-Kh4N3XG4.js} +1 -1
- package/ui-dist/assets/{browser-ponyfill-BBsuyZJt.js → browser-ponyfill-B3hXtEFW.js} +1 -1
- package/ui-dist/assets/{c4Diagram-LMCZKHZV-_nlglj7I.js → c4Diagram-LMCZKHZV-CBHpGrgU.js} +1 -1
- package/ui-dist/assets/channel-4fjB8e8N.js +1 -0
- package/ui-dist/assets/{chunk-2Q5K7J3B-Cvz7WwSQ.js → chunk-2Q5K7J3B-BdHbqi7k.js} +1 -1
- package/ui-dist/assets/{chunk-32BRIVSS-LLubniCS.js → chunk-32BRIVSS-DxtzZzpt.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-BOst5hcr.js → chunk-5VM5RSS4-DMKxBbgu.js} +1 -1
- package/ui-dist/assets/{chunk-EX3LRPZG-D5WNShVm.js → chunk-EX3LRPZG-BaIOPi4v.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-CA07x4av.js → chunk-JWPE2WC7-DGo3MPmr.js} +1 -1
- package/ui-dist/assets/{chunk-MOJQB5TN-Clfi83rK.js → chunk-MOJQB5TN-BEhQg0uD.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-BhZ6p7YJ.js → chunk-RYQCIY6F-HPJ6zRpJ.js} +1 -1
- package/ui-dist/assets/{chunk-V7JOEXUC-CdexJGoV.js → chunk-V7JOEXUC-Bl_6cAJD.js} +1 -1
- package/ui-dist/assets/{chunk-VR4S4FIN-8vjmBhr1.js → chunk-VR4S4FIN-KMjgGCsw.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-CqmCaplj.js → chunk-XXDRQBXY-oWGAIKjr.js} +1 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-CTXxO5l9.js +1 -0
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CTXxO5l9.js +1 -0
- package/ui-dist/assets/{cose-bilkent-JH36ORCC-CY-zAygY.js → cose-bilkent-JH36ORCC-Ckqse1pH.js} +1 -1
- package/ui-dist/assets/{cynefin-VYW2F7L2-BQSQ1HKr.js → cynefin-VYW2F7L2-DfZ8Hzqi.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-NJCuFrZL.js → cynefinDiagram-TSTJHNR4-BkDQo9oi.js} +1 -1
- package/ui-dist/assets/{dagre-VKFMJZFB-DAUPIc8g.js → dagre-VKFMJZFB-ClghXQ6E.js} +1 -1
- package/ui-dist/assets/{diagram-FQU43EPY-DvMwJlCi.js → diagram-FQU43EPY-D5a0CwpV.js} +1 -1
- package/ui-dist/assets/{diagram-G47NLZAW-DPY0Jz_y.js → diagram-G47NLZAW-hVjYR_xl.js} +1 -1
- package/ui-dist/assets/{diagram-NH7WQ7WH-BpopeEhp.js → diagram-NH7WQ7WH-BlDPgD5Z.js} +1 -1
- package/ui-dist/assets/{diagram-OA4YK3LP-Qpu05z9X.js → diagram-OA4YK3LP-D0iZKIS3.js} +1 -1
- package/ui-dist/assets/{diagram-WEI45ONY-DHnvw9XU.js → diagram-WEI45ONY-m6hrfD0J.js} +1 -1
- package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-D61S54_7.js → ebnfDiagram-CCIWWBDH-D4SQYLod.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q63AITRT-pplAN_x2.js → erDiagram-Q63AITRT-Byzb6-I5.js} +1 -1
- package/ui-dist/assets/{flowDiagram-23GEKE2U-BaBcbyDq.js → flowDiagram-23GEKE2U-CaTUkFO6.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-NO4QXBWP-BOJSII2b.js → ganttDiagram-NO4QXBWP-DmdMQ3ES.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-aYAaO4a2.js → gitGraphDiagram-IHSO6WYX-Bt-1g2CS.js} +1 -1
- package/ui-dist/assets/{index-CZxYajxF.js → index-2Dk9yO0W.js} +1 -1
- package/ui-dist/assets/{index-B8H0SHw1.js → index-8jLpgFNU.js} +1 -1
- package/ui-dist/assets/{index-3EKATXgc.js → index-AP_iimxs.js} +1 -1
- package/ui-dist/assets/{index-C2JgdjPa.js → index-BNJasQY9.js} +1 -1
- package/ui-dist/assets/{index-BaiskB3o.js → index-BO-aNmyX.js} +1 -1
- package/ui-dist/assets/{index-B5YenxAQ.js → index-BPCzCn1Y.js} +1 -1
- package/ui-dist/assets/{index-Da2kqT1M.js → index-BSLnLTb2.js} +1 -1
- package/ui-dist/assets/{index-By0oeEvz.js → index-CCZ2qgHN.js} +1 -1
- package/ui-dist/assets/{index-bUFCSEgv.js → index-CLZXR1k9.js} +1 -1
- package/ui-dist/assets/{index-CJlw4l9L.js → index-CTNfrE1s.js} +1 -1
- package/ui-dist/assets/{index-TmpY6Xh_.js → index-ChNd25s7.js} +1 -1
- package/ui-dist/assets/{index-BRgdHUNR.js → index-CjHiMOEO.js} +1 -1
- package/ui-dist/assets/{index-DrL6pHSJ.js → index-CmeJQpXZ.js} +1 -1
- package/ui-dist/assets/index-CyuYtn7U.css +1 -0
- package/ui-dist/assets/{index-CKKytnOd.js → index-CzZjM8eh.js} +1 -1
- package/ui-dist/assets/{index-Dceamza_.js → index-D-6T0Aft.js} +1 -1
- package/ui-dist/assets/{index-CjDqB1SX.js → index-DG_7V1pG.js} +1 -1
- package/ui-dist/assets/index-DIKcgxCt.js +676 -0
- package/ui-dist/assets/{index-BKVeJ3Jh.js → index-DiUerHmi.js} +1 -1
- package/ui-dist/assets/{index-C-nKjFZh.js → index-NZ7v5Fc2.js} +1 -1
- package/ui-dist/assets/{index-CvPPuNs1.js → index-Py70EvBg.js} +1 -1
- package/ui-dist/assets/{index-LHkZdu0h.js → index-R3aYn0qM.js} +1 -1
- package/ui-dist/assets/{index-BT8b79Bd.js → index-YXpm7gW-.js} +1 -1
- package/ui-dist/assets/{index-CemenVOf.js → index-n4q3gzki.js} +1 -1
- package/ui-dist/assets/{index-D3ODiUup.js → index-vlZ3GoYY.js} +1 -1
- package/ui-dist/assets/{infoDiagram-FWYZ7A6U-C7_90qGq.js → infoDiagram-FWYZ7A6U-jdi5ciVg.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-T-nPPdTt.js → ishikawaDiagram-FXEZZL3T-CMz5VvYp.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-5HDEW3XC-C3hqQN0R.js → journeyDiagram-5HDEW3XC-DK6dk2R-.js} +1 -1
- package/ui-dist/assets/{kanban-definition-HUTT4EX6-CPoD9DUm.js → kanban-definition-HUTT4EX6-DX5cZL-D.js} +1 -1
- package/ui-dist/assets/{linear-D-S8yr3Z.js → linear-CI96ABKp.js} +1 -1
- package/ui-dist/assets/{mermaid.core-DhvH8shK.js → mermaid.core-DnsEVfbd.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-LN4V7U3C-CxCIKrKo.js → mindmap-definition-LN4V7U3C-BdDCY7hf.js} +1 -1
- package/ui-dist/assets/{pegDiagram-2B236MQR-D4G-PC6N.js → pegDiagram-2B236MQR-DuY4TF-f.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ENE6RG2P-N4X8UvqY.js → pieDiagram-ENE6RG2P-bgFw7dm3.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CZohoDln.js → quadrantDiagram-ABIIQ3AL-BCZVWyWS.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-RFXS5EU6-fMGQVhwF.js → railroadDiagram-RFXS5EU6-D9V5_Cgc.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-TGXJPOKE-CK0IuUmk.js → requirementDiagram-TGXJPOKE-CItsya2r.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-pSfb3CqG.js → sankeyDiagram-HTMAVEWB-Cz9IHsZO.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CbhkqIeq.js → sequenceDiagram-DBY2YBRQ-hMSqTiwK.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-B6vqqPx-.js → sizeCapture-X5ZJPWSS-0g9M5wMf.js} +1 -1
- package/ui-dist/assets/{stateDiagram-2N3HPSRC-DIJf9h5C.js → stateDiagram-2N3HPSRC-F8_YhgWl.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-fZImUBtq.js +1 -0
- package/ui-dist/assets/{swimlanes-5IMT3BWC-DIX_LV8O.js → swimlanes-5IMT3BWC-CCZqPbpE.js} +2 -2
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-9C1UT51R.js +8 -0
- package/ui-dist/assets/{timeline-definition-FHXFAJF6-CD-Pxb_g.js → timeline-definition-FHXFAJF6-BeUIr18x.js} +1 -1
- package/ui-dist/assets/{vennDiagram-L72KCM5P-BAZsWL0R.js → vennDiagram-L72KCM5P-D3d5h_G4.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-EHGQE667-CnNyfwX5.js → wardleyDiagram-EHGQE667-Bzt-As9A.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-FW5EYKEG-j9YgsD3Y.js → xychartDiagram-FW5EYKEG-C2mly0xf.js} +1 -1
- package/ui-dist/fonts/InterVariable-Italic.woff2 +0 -0
- package/ui-dist/fonts/InterVariable.woff2 +0 -0
- package/ui-dist/fonts/NOTICE.md +17 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/locales/en/common.json +1142 -5
- package/ui-dist/locales/zh-CN/common.json +1140 -3
- package/ui-dist/assets/channel-CsiI2LYw.js +0 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-CBn1Jqo8.js +0 -1
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CBn1Jqo8.js +0 -1
- package/ui-dist/assets/index-BsL8K1vL.css +0 -1
- package/ui-dist/assets/index-D__fTsb2.js +0 -643
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DpwLD7YL.js +0 -1
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-Cklauutg.js +0 -8
|
@@ -5,13 +5,14 @@ import net from "node:net";
|
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { setTimeout as delay } from "node:timers/promises";
|
|
8
|
-
import { executionWorkspaces, projectWorkspaces, workspaceRuntimeServices } from "@penclipai/db";
|
|
8
|
+
import { executionWorkspaces, issueComments, issues, projectWorkspaces, workspaceRuntimeServices } from "@penclipai/db";
|
|
9
9
|
import { listWorkspaceServiceCommandDefinitions, } from "@penclipai/shared";
|
|
10
|
-
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
10
|
+
import { and, desc, eq, inArray, ne } from "drizzle-orm";
|
|
11
11
|
import { asNumber, asString, parseObject, renderTemplate } from "../adapters/utils.js";
|
|
12
12
|
import { resolveHomeAwarePath } from "../home-paths.js";
|
|
13
|
-
import { createLocalServiceKey, findLocalServiceRegistryRecordByRuntimeServiceId, findAdoptableLocalService, readLocalServicePortOwner, removeLocalServiceRegistryRecord, terminateLocalService, touchLocalServiceRegistryRecord, writeLocalServiceRegistryRecord, } from "./local-service-supervisor.js";
|
|
14
|
-
import { readExecutionWorkspaceConfig } from "./execution-workspaces.js";
|
|
13
|
+
import { createLocalServiceKey, findLocalServiceRegistryRecordByRuntimeServiceId, findAdoptableLocalService, isLocalServiceProcessInWorkspace, readLocalServiceProcessCwd, readLocalServicePortOwner, removeLocalServiceRegistryRecord, terminateLocalService, touchLocalServiceRegistryRecord, writeLocalServiceRegistryRecord, } from "./local-service-supervisor.js";
|
|
14
|
+
import { executionWorkspaceService, readExecutionWorkspaceConfig } from "./execution-workspaces.js";
|
|
15
|
+
import { logActivity } from "./activity-log.js";
|
|
15
16
|
import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js";
|
|
16
17
|
export function resolveShell() {
|
|
17
18
|
const windowsGitBash = "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
@@ -466,6 +467,150 @@ const GIT_WORKTREE_BRANCH_INCOHERENCE_REASON = "git_worktree_branch_incoherence"
|
|
|
466
467
|
function formatBranchForMessage(branch) {
|
|
467
468
|
return branch && branch.length > 0 ? branch : "<detached>";
|
|
468
469
|
}
|
|
470
|
+
const GIT_IN_PROGRESS_OPERATION_MARKERS = [
|
|
471
|
+
{ operation: "rebase", marker: "rebase-merge" },
|
|
472
|
+
{ operation: "rebase", marker: "rebase-apply" },
|
|
473
|
+
{ operation: "merge", marker: "MERGE_HEAD" },
|
|
474
|
+
{ operation: "cherry_pick", marker: "CHERRY_PICK_HEAD" },
|
|
475
|
+
{ operation: "revert", marker: "REVERT_HEAD" },
|
|
476
|
+
{ operation: "bisect", marker: "BISECT_LOG" },
|
|
477
|
+
];
|
|
478
|
+
const GIT_IN_PROGRESS_OPERATION_LABELS = {
|
|
479
|
+
rebase: "rebase",
|
|
480
|
+
merge: "merge",
|
|
481
|
+
cherry_pick: "cherry-pick",
|
|
482
|
+
revert: "revert",
|
|
483
|
+
bisect: "bisect",
|
|
484
|
+
};
|
|
485
|
+
// `--quit` clears the interrupted operation's state directory without touching
|
|
486
|
+
// the working tree or moving HEAD, unlike `--abort` which resets both.
|
|
487
|
+
const GIT_IN_PROGRESS_OPERATION_QUIT_ARGS = {
|
|
488
|
+
rebase: ["rebase", "--quit"],
|
|
489
|
+
merge: ["merge", "--quit"],
|
|
490
|
+
cherry_pick: ["cherry-pick", "--quit"],
|
|
491
|
+
revert: ["revert", "--quit"],
|
|
492
|
+
bisect: ["bisect", "reset", "HEAD"],
|
|
493
|
+
};
|
|
494
|
+
async function detectGitWorktreeInProgressOperation(worktreePath) {
|
|
495
|
+
for (const { operation, marker } of GIT_IN_PROGRESS_OPERATION_MARKERS) {
|
|
496
|
+
const markerPath = await runGit(["rev-parse", "--git-path", marker], worktreePath).catch(() => null);
|
|
497
|
+
if (!markerPath)
|
|
498
|
+
continue;
|
|
499
|
+
if (existsSync(path.resolve(worktreePath, markerPath)))
|
|
500
|
+
return operation;
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const DIRTY_PATH_SAMPLE_LIMIT = 5;
|
|
505
|
+
function parseGitPorcelainPath(line) {
|
|
506
|
+
const raw = line.trimEnd();
|
|
507
|
+
if (raw.trim().length <= 3)
|
|
508
|
+
return raw.trim();
|
|
509
|
+
if (raw[1] === " " && raw[2] !== " ")
|
|
510
|
+
return raw.slice(2).trim();
|
|
511
|
+
return raw.slice(3).trim();
|
|
512
|
+
}
|
|
513
|
+
function sampleDirtyStatusPaths(statusLines) {
|
|
514
|
+
return (statusLines ?? [])
|
|
515
|
+
.map(parseGitPorcelainPath)
|
|
516
|
+
.filter((value) => value.length > 0)
|
|
517
|
+
.slice(0, DIRTY_PATH_SAMPLE_LIMIT);
|
|
518
|
+
}
|
|
519
|
+
function formatUtcBranchTimestamp(date = new Date()) {
|
|
520
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
521
|
+
}
|
|
522
|
+
function buildDirtyQuarantineRescueBranch(sourceIssue) {
|
|
523
|
+
const issueComponent = sanitizeBranchName(sourceIssue?.identifier ?? sourceIssue?.id ?? "issue");
|
|
524
|
+
return sanitizeBranchName(`paperclip/rescue/${issueComponent}/${formatUtcBranchTimestamp()}`);
|
|
525
|
+
}
|
|
526
|
+
function formatIssueReference(issueId, identifier) {
|
|
527
|
+
if (!identifier)
|
|
528
|
+
return issueId ? `\`${issueId}\`` : "`unknown`";
|
|
529
|
+
const match = identifier.match(/^([A-Z]+)-\d+$/);
|
|
530
|
+
if (!match)
|
|
531
|
+
return `\`${identifier}\``;
|
|
532
|
+
return `[${identifier}](/${match[1]}/issues/${identifier})`;
|
|
533
|
+
}
|
|
534
|
+
async function readIssueCompanyId(db, issueId) {
|
|
535
|
+
if (!issueId)
|
|
536
|
+
return null;
|
|
537
|
+
return db
|
|
538
|
+
.select({ companyId: issues.companyId })
|
|
539
|
+
.from(issues)
|
|
540
|
+
.where(eq(issues.id, issueId))
|
|
541
|
+
.then((rows) => rows[0]?.companyId ?? null);
|
|
542
|
+
}
|
|
543
|
+
async function findGitWorktreeBranchContention(input) {
|
|
544
|
+
if (!input.db)
|
|
545
|
+
return null;
|
|
546
|
+
const companyId = await readIssueCompanyId(input.db, input.sourceIssue?.id);
|
|
547
|
+
if (!companyId)
|
|
548
|
+
return null;
|
|
549
|
+
return executionWorkspaceService(input.db).findGitWorktreeContention({
|
|
550
|
+
companyId,
|
|
551
|
+
worktreePath: input.worktreePath,
|
|
552
|
+
liveBranchName: input.actualBranchName,
|
|
553
|
+
excludingExecutionWorkspaceId: input.executionWorkspaceId,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function executionWorkspaceUsesInheritedProjectRuntimeServices(row) {
|
|
557
|
+
if (row.mode !== "shared_workspace" || !row.projectWorkspaceId)
|
|
558
|
+
return false;
|
|
559
|
+
return !readExecutionWorkspaceConfig(row.metadata ?? null)?.workspaceRuntime;
|
|
560
|
+
}
|
|
561
|
+
async function findActiveRuntimeServiceBlockingDirtyQuarantine(input) {
|
|
562
|
+
const inheritedProjectWorkspaceId = executionWorkspaceUsesInheritedProjectRuntimeServices(input.workspace)
|
|
563
|
+
? input.workspace.projectWorkspaceId
|
|
564
|
+
: null;
|
|
565
|
+
const serviceScopeCondition = inheritedProjectWorkspaceId
|
|
566
|
+
? and(eq(workspaceRuntimeServices.companyId, input.workspace.companyId), eq(workspaceRuntimeServices.projectWorkspaceId, inheritedProjectWorkspaceId), eq(workspaceRuntimeServices.scopeType, "project_workspace"))
|
|
567
|
+
: and(eq(workspaceRuntimeServices.companyId, input.workspace.companyId), eq(workspaceRuntimeServices.executionWorkspaceId, input.workspace.id));
|
|
568
|
+
const [service] = await input.db
|
|
569
|
+
.select({
|
|
570
|
+
id: workspaceRuntimeServices.id,
|
|
571
|
+
serviceName: workspaceRuntimeServices.serviceName,
|
|
572
|
+
status: workspaceRuntimeServices.status,
|
|
573
|
+
scopeType: workspaceRuntimeServices.scopeType,
|
|
574
|
+
})
|
|
575
|
+
.from(workspaceRuntimeServices)
|
|
576
|
+
.where(and(serviceScopeCondition, ne(workspaceRuntimeServices.status, "stopped")))
|
|
577
|
+
.orderBy(desc(workspaceRuntimeServices.updatedAt), desc(workspaceRuntimeServices.createdAt))
|
|
578
|
+
.limit(1);
|
|
579
|
+
return service ?? null;
|
|
580
|
+
}
|
|
581
|
+
async function assertDirtyQuarantineRuntimeServicesStopped(input) {
|
|
582
|
+
if (!input.executionWorkspaceId) {
|
|
583
|
+
input.evidence.safeRepair.eligible = false;
|
|
584
|
+
input.evidence.safeRepair.reason = "dirty quarantine repair requires an execution workspace id for runtime-service checks";
|
|
585
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
586
|
+
}
|
|
587
|
+
const [workspace] = await input.db
|
|
588
|
+
.select()
|
|
589
|
+
.from(executionWorkspaces)
|
|
590
|
+
.where(eq(executionWorkspaces.id, input.executionWorkspaceId));
|
|
591
|
+
if (!workspace) {
|
|
592
|
+
input.evidence.safeRepair.eligible = false;
|
|
593
|
+
input.evidence.safeRepair.reason = "dirty quarantine repair requires a persisted execution workspace for runtime-service checks";
|
|
594
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
595
|
+
}
|
|
596
|
+
const activeService = await findActiveRuntimeServiceBlockingDirtyQuarantine({
|
|
597
|
+
db: input.db,
|
|
598
|
+
workspace,
|
|
599
|
+
});
|
|
600
|
+
if (!activeService)
|
|
601
|
+
return;
|
|
602
|
+
input.evidence.safeRepair.eligible = false;
|
|
603
|
+
input.evidence.safeRepair.reason =
|
|
604
|
+
`dirty quarantine repair requires runtime service "${activeService.serviceName}" (${activeService.id}) to be stopped; current status is ${activeService.status}`;
|
|
605
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
606
|
+
}
|
|
607
|
+
async function assertGitIndexIsUnlocked(worktreePath) {
|
|
608
|
+
const indexLockPath = await runGit(["rev-parse", "--git-path", "index.lock"], worktreePath)
|
|
609
|
+
.catch(() => null);
|
|
610
|
+
if (indexLockPath && existsSync(indexLockPath)) {
|
|
611
|
+
throw new Error(`git index lock exists at ${indexLockPath}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
469
614
|
function fingerprintWorkspaceBranchIncoherence(input) {
|
|
470
615
|
const digest = createHash("sha256")
|
|
471
616
|
.update(stableStringify({
|
|
@@ -483,12 +628,46 @@ function fingerprintWorkspaceBranchIncoherence(input) {
|
|
|
483
628
|
.digest("hex");
|
|
484
629
|
return `workspace_incoherence:v1:sha256:${digest}`;
|
|
485
630
|
}
|
|
631
|
+
async function getGitWorktreeBranchAncestryVerdict(input) {
|
|
632
|
+
if (!input.expectedHeadSha || !input.actualHeadSha)
|
|
633
|
+
return "unknown";
|
|
634
|
+
const proc = await executeProcess({
|
|
635
|
+
command: "git",
|
|
636
|
+
args: ["merge-base", "--is-ancestor", input.expectedHeadSha, input.actualHeadSha],
|
|
637
|
+
cwd: input.repoRoot,
|
|
638
|
+
}).catch(() => null);
|
|
639
|
+
if (!proc)
|
|
640
|
+
return "unknown";
|
|
641
|
+
if (proc.code === 0)
|
|
642
|
+
return "ancestor";
|
|
643
|
+
if (proc.code === 1)
|
|
644
|
+
return "diverged";
|
|
645
|
+
return "unknown";
|
|
646
|
+
}
|
|
647
|
+
function explainGitWorktreeBranchIncoherence(input) {
|
|
648
|
+
const actualBranch = formatBranchForMessage(input.actualBranchName);
|
|
649
|
+
if (!input.expectedHeadSha || !input.actualHeadSha) {
|
|
650
|
+
return `Paperclip could not determine branch ancestry because the recorded branch "${input.expectedBranchName}" or checked-out branch "${actualBranch}" is missing a resolvable HEAD commit.`;
|
|
651
|
+
}
|
|
652
|
+
if (input.sameHead) {
|
|
653
|
+
return `The recorded branch "${input.expectedBranchName}" and checked-out branch "${actualBranch}" resolve to the same commit, so the mismatch is branch metadata rather than commit divergence.`;
|
|
654
|
+
}
|
|
655
|
+
if (input.ancestryVerdict === "ancestor") {
|
|
656
|
+
return `The recorded branch "${input.expectedBranchName}" is an ancestor of the checked-out branch "${actualBranch}", so the checked-out branch is forward of the recorded branch.`;
|
|
657
|
+
}
|
|
658
|
+
if (input.ancestryVerdict === "diverged") {
|
|
659
|
+
return `The recorded branch "${input.expectedBranchName}" is not an ancestor of the checked-out branch "${actualBranch}", so Paperclip cannot prove a forward-only reconciliation.`;
|
|
660
|
+
}
|
|
661
|
+
return `Paperclip could not determine whether the checked-out branch "${actualBranch}" is forward of the recorded branch "${input.expectedBranchName}".`;
|
|
662
|
+
}
|
|
486
663
|
async function inspectGitWorktreeBranchIncoherence(input) {
|
|
487
664
|
const status = await runGit(["status", "--porcelain", "--untracked-files=all"], input.worktreePath).catch(() => null);
|
|
488
665
|
const statusLines = status === null
|
|
489
666
|
? null
|
|
490
|
-
: status.split(/\r?\n/).map((line) => line.
|
|
667
|
+
: status.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
668
|
+
const dirtyPathSample = sampleDirtyStatusPaths(statusLines);
|
|
491
669
|
const cleanliness = status === null ? "unknown" : status.trim().length > 0 ? "dirty" : "clean";
|
|
670
|
+
const inProgressOperation = await detectGitWorktreeInProgressOperation(input.worktreePath);
|
|
492
671
|
const expectedHeadSha = await runGit(["rev-parse", "--verify", `refs/heads/${input.expectedBranchName}^{commit}`], input.repoRoot).catch(() => null);
|
|
493
672
|
const actualHeadSha = await runGit(["rev-parse", "HEAD"], input.worktreePath).catch(() => null);
|
|
494
673
|
const actualBranchExists = input.actualBranchName
|
|
@@ -500,11 +679,46 @@ async function inspectGitWorktreeBranchIncoherence(input) {
|
|
|
500
679
|
const registeredBranchMatchesHead = Boolean(registered && registeredBranchRef === actualBranchRef);
|
|
501
680
|
const sameHead = Boolean(expectedHeadSha && actualHeadSha && expectedHeadSha === actualHeadSha);
|
|
502
681
|
const expectedBranchExists = Boolean(expectedHeadSha);
|
|
503
|
-
const
|
|
682
|
+
const ancestryVerdict = await getGitWorktreeBranchAncestryVerdict({
|
|
683
|
+
repoRoot: input.repoRoot,
|
|
684
|
+
expectedHeadSha,
|
|
685
|
+
actualHeadSha,
|
|
686
|
+
});
|
|
687
|
+
const basePlainLanguageReason = explainGitWorktreeBranchIncoherence({
|
|
688
|
+
expectedBranchName: input.expectedBranchName,
|
|
689
|
+
actualBranchName: input.actualBranchName,
|
|
690
|
+
expectedHeadSha,
|
|
691
|
+
actualHeadSha,
|
|
692
|
+
sameHead,
|
|
693
|
+
ancestryVerdict,
|
|
694
|
+
});
|
|
695
|
+
const plainLanguageReason = inProgressOperation
|
|
696
|
+
? `${basePlainLanguageReason} An interrupted git ${GIT_IN_PROGRESS_OPERATION_LABELS[inProgressOperation]} is still in progress in this worktree.`
|
|
697
|
+
: basePlainLanguageReason;
|
|
698
|
+
const canCheckoutRecordedBranch = cleanliness === "clean" && expectedBranchExists && sameHead && registeredBranchMatchesHead;
|
|
699
|
+
const canAdoptForwardActualBranch = cleanliness === "clean" &&
|
|
700
|
+
expectedBranchExists &&
|
|
701
|
+
actualBranchExists === true &&
|
|
702
|
+
ancestryVerdict === "ancestor" &&
|
|
703
|
+
!sameHead &&
|
|
704
|
+
registeredBranchMatchesHead;
|
|
705
|
+
const canAttachRecordedBranchToDetachedHead = cleanliness === "clean" &&
|
|
706
|
+
expectedBranchExists &&
|
|
707
|
+
input.actualBranchName === null &&
|
|
708
|
+
ancestryVerdict === "ancestor" &&
|
|
709
|
+
!sameHead &&
|
|
710
|
+
registeredBranchMatchesHead;
|
|
711
|
+
const eligible = canCheckoutRecordedBranch || canAdoptForwardActualBranch || canAttachRecordedBranchToDetachedHead;
|
|
504
712
|
const safeRepairReason = eligible
|
|
505
|
-
?
|
|
713
|
+
? canCheckoutRecordedBranch
|
|
714
|
+
? "clean worktree and expected branch points at the current HEAD"
|
|
715
|
+
: canAdoptForwardActualBranch
|
|
716
|
+
? "clean worktree and checked-out branch is forward of the recorded branch"
|
|
717
|
+
: "clean detached worktree HEAD is forward of the recorded branch"
|
|
506
718
|
: cleanliness !== "clean"
|
|
507
|
-
?
|
|
719
|
+
? inProgressOperation
|
|
720
|
+
? `worktree is not clean and a git ${GIT_IN_PROGRESS_OPERATION_LABELS[inProgressOperation]} is in progress`
|
|
721
|
+
: "worktree is not clean"
|
|
508
722
|
: !registered
|
|
509
723
|
? "worktree path is not registered"
|
|
510
724
|
: !registeredBranchMatchesHead
|
|
@@ -524,6 +738,13 @@ async function inspectGitWorktreeBranchIncoherence(input) {
|
|
|
524
738
|
expectedHeadSha,
|
|
525
739
|
actualHeadSha,
|
|
526
740
|
});
|
|
741
|
+
const contention = await findGitWorktreeBranchContention({
|
|
742
|
+
db: input.db ?? null,
|
|
743
|
+
sourceIssue: input.sourceIssue,
|
|
744
|
+
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
|
745
|
+
worktreePath: input.worktreePath,
|
|
746
|
+
actualBranchName: input.actualBranchName,
|
|
747
|
+
});
|
|
527
748
|
return {
|
|
528
749
|
reason: GIT_WORKTREE_BRANCH_INCOHERENCE_REASON,
|
|
529
750
|
fingerprint,
|
|
@@ -535,7 +756,10 @@ async function inspectGitWorktreeBranchIncoherence(input) {
|
|
|
535
756
|
expectedBranch: input.expectedBranchName,
|
|
536
757
|
actualBranch: input.actualBranchName,
|
|
537
758
|
cleanliness,
|
|
759
|
+
inProgressOperation,
|
|
538
760
|
statusEntryCount: statusLines?.length ?? null,
|
|
761
|
+
dirtyPathSample,
|
|
762
|
+
contention,
|
|
539
763
|
provenance: {
|
|
540
764
|
expectedBranchRef: `refs/heads/${input.expectedBranchName}`,
|
|
541
765
|
actualBranchRef,
|
|
@@ -547,6 +771,8 @@ async function inspectGitWorktreeBranchIncoherence(input) {
|
|
|
547
771
|
expectedHeadSha,
|
|
548
772
|
actualHeadSha,
|
|
549
773
|
sameHead,
|
|
774
|
+
ancestryVerdict,
|
|
775
|
+
plainLanguageReason,
|
|
550
776
|
},
|
|
551
777
|
safeRepair: {
|
|
552
778
|
eligible,
|
|
@@ -561,16 +787,465 @@ function branchIncoherenceValidationFailure(evidence) {
|
|
|
561
787
|
workspaceValidation: evidence,
|
|
562
788
|
});
|
|
563
789
|
}
|
|
790
|
+
function formatDirtyQuarantineContentionRefusal(contention) {
|
|
791
|
+
const activeRunText = contention.activeRun
|
|
792
|
+
? ` with active run ${contention.activeRun.id}`
|
|
793
|
+
: " with no active run";
|
|
794
|
+
return `dirty quarantine repair refused because workspace ${contention.claimedByWorkspaceId} already claims the live branch${activeRunText}`;
|
|
795
|
+
}
|
|
796
|
+
function formatDirtyQuarantineFailure(error) {
|
|
797
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
798
|
+
if (gitErrorIncludes(error, "index.lock") ||
|
|
799
|
+
gitErrorIncludes(error, "index lock") ||
|
|
800
|
+
gitErrorIncludes(error, "another git process") ||
|
|
801
|
+
gitErrorIncludes(error, "Unable to create")) {
|
|
802
|
+
return `dirty quarantine repair aborted because git reported index contention: ${message}`;
|
|
803
|
+
}
|
|
804
|
+
return `dirty quarantine repair failed: ${message}`;
|
|
805
|
+
}
|
|
806
|
+
function formatDirtyQuarantineAuditComment(input) {
|
|
807
|
+
const dirtySample = input.evidence.dirtyPathSample.length > 0
|
|
808
|
+
? input.evidence.dirtyPathSample.map((entry) => `\`${entry}\``).join(", ")
|
|
809
|
+
: "`none captured`";
|
|
810
|
+
return [
|
|
811
|
+
"Execution workspace dirty worktree quarantined before restore.",
|
|
812
|
+
"",
|
|
813
|
+
`- Source issue: ${formatIssueReference(input.evidence.sourceIssueId, input.evidence.sourceIdentifier ?? input.sourceIssue?.identifier ?? null)}`,
|
|
814
|
+
`- Workspace: \`${input.evidence.executionWorkspaceId ?? "unpersisted"}\``,
|
|
815
|
+
`- Worktree: \`${input.evidence.worktreePath}\``,
|
|
816
|
+
`- Recorded branch: \`${input.evidence.expectedBranch}\``,
|
|
817
|
+
`- Live branch: \`${formatBranchForMessage(input.evidence.actualBranch)}\``,
|
|
818
|
+
`- Rescue branch: \`${input.rescueBranch}\``,
|
|
819
|
+
`- Rescue commit: \`${input.rescueCommitSha}\``,
|
|
820
|
+
`- Dirty file count: \`${input.fileCount}\``,
|
|
821
|
+
`- Dirty path sample: ${dirtySample}`,
|
|
822
|
+
...(input.evidence.inProgressOperation
|
|
823
|
+
? [`- Interrupted operation: \`git ${GIT_IN_PROGRESS_OPERATION_LABELS[input.evidence.inProgressOperation]}\` (state cleared after rescue; resolution preserved on the rescue branch)`]
|
|
824
|
+
: []),
|
|
825
|
+
`- Fingerprint: \`${input.evidence.fingerprint}\``,
|
|
826
|
+
input.claimant
|
|
827
|
+
? `- Claimant: workspace \`${input.claimant.claimedByWorkspaceId}\` on issue ${formatIssueReference(input.claimant.claimedByIssueId, input.claimant.claimedByIssueIdentifier)}${input.claimant.activeRun ? ` with active run \`${input.claimant.activeRun.id}\`` : " with no active run"}`
|
|
828
|
+
: "- Claimant: none",
|
|
829
|
+
].join("\n");
|
|
830
|
+
}
|
|
831
|
+
async function writeDirtyQuarantineAuditComments(input) {
|
|
832
|
+
const body = formatDirtyQuarantineAuditComment({
|
|
833
|
+
evidence: input.evidence,
|
|
834
|
+
rescueBranch: input.rescueBranch,
|
|
835
|
+
rescueCommitSha: input.rescueCommitSha,
|
|
836
|
+
fileCount: input.fileCount,
|
|
837
|
+
sourceIssue: input.sourceIssue,
|
|
838
|
+
claimant: input.evidence.contention,
|
|
839
|
+
});
|
|
840
|
+
let sourceAuditCommentId = null;
|
|
841
|
+
let claimantAuditCommentId = null;
|
|
842
|
+
const now = new Date();
|
|
843
|
+
if (input.evidence.sourceIssueId) {
|
|
844
|
+
const [sourceComment] = await input.db
|
|
845
|
+
.insert(issueComments)
|
|
846
|
+
.values({
|
|
847
|
+
companyId: input.companyId,
|
|
848
|
+
issueId: input.evidence.sourceIssueId,
|
|
849
|
+
authorAgentId: null,
|
|
850
|
+
authorUserId: null,
|
|
851
|
+
authorType: "system",
|
|
852
|
+
createdByRunId: input.heartbeatRunId,
|
|
853
|
+
body,
|
|
854
|
+
})
|
|
855
|
+
.returning({ id: issueComments.id });
|
|
856
|
+
sourceAuditCommentId = sourceComment?.id ?? null;
|
|
857
|
+
await input.db
|
|
858
|
+
.update(issues)
|
|
859
|
+
.set({ updatedAt: now })
|
|
860
|
+
.where(eq(issues.id, input.evidence.sourceIssueId));
|
|
861
|
+
}
|
|
862
|
+
const claimantIssueId = input.evidence.contention?.claimedByIssueId ?? null;
|
|
863
|
+
if (claimantIssueId && claimantIssueId !== input.evidence.sourceIssueId) {
|
|
864
|
+
const [claimantComment] = await input.db
|
|
865
|
+
.insert(issueComments)
|
|
866
|
+
.values({
|
|
867
|
+
companyId: input.companyId,
|
|
868
|
+
issueId: claimantIssueId,
|
|
869
|
+
authorAgentId: null,
|
|
870
|
+
authorUserId: null,
|
|
871
|
+
authorType: "system",
|
|
872
|
+
createdByRunId: input.heartbeatRunId,
|
|
873
|
+
body,
|
|
874
|
+
})
|
|
875
|
+
.returning({ id: issueComments.id });
|
|
876
|
+
claimantAuditCommentId = claimantComment?.id ?? null;
|
|
877
|
+
await input.db
|
|
878
|
+
.update(issues)
|
|
879
|
+
.set({ updatedAt: now })
|
|
880
|
+
.where(eq(issues.id, claimantIssueId));
|
|
881
|
+
}
|
|
882
|
+
return { sourceAuditCommentId, claimantAuditCommentId };
|
|
883
|
+
}
|
|
884
|
+
async function logDirtyQuarantineActivity(input) {
|
|
885
|
+
await logActivity(input.db, {
|
|
886
|
+
companyId: input.companyId,
|
|
887
|
+
actorType: "system",
|
|
888
|
+
actorId: "workspace_runtime",
|
|
889
|
+
runId: input.heartbeatRunId,
|
|
890
|
+
action: "execution_workspace.dirty_worktree_quarantined",
|
|
891
|
+
entityType: input.evidence.executionWorkspaceId ? "execution_workspace" : "issue",
|
|
892
|
+
entityId: input.evidence.executionWorkspaceId ?? input.evidence.sourceIssueId ?? input.companyId,
|
|
893
|
+
details: {
|
|
894
|
+
reason: GIT_WORKTREE_BRANCH_INCOHERENCE_REASON,
|
|
895
|
+
sourceIssueId: input.evidence.sourceIssueId,
|
|
896
|
+
executionWorkspaceId: input.evidence.executionWorkspaceId,
|
|
897
|
+
worktreePath: input.evidence.worktreePath,
|
|
898
|
+
expectedBranch: input.evidence.expectedBranch,
|
|
899
|
+
actualBranch: input.evidence.actualBranch,
|
|
900
|
+
rescueBranch: input.rescueBranch,
|
|
901
|
+
rescueCommitSha: input.rescueCommitSha,
|
|
902
|
+
fileCount: input.fileCount,
|
|
903
|
+
dirtyPathSample: input.evidence.dirtyPathSample,
|
|
904
|
+
fingerprint: input.evidence.fingerprint,
|
|
905
|
+
contention: input.evidence.contention,
|
|
906
|
+
sourceAuditCommentId: input.sourceAuditCommentId,
|
|
907
|
+
claimantAuditCommentId: input.claimantAuditCommentId,
|
|
908
|
+
actor: {
|
|
909
|
+
type: "system",
|
|
910
|
+
id: "workspace_runtime",
|
|
911
|
+
source: "workspace_runtime",
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
async function recordDirtyQuarantineOperation(input) {
|
|
917
|
+
if (!input.recorder)
|
|
918
|
+
return;
|
|
919
|
+
await input.recorder.recordOperation({
|
|
920
|
+
phase: input.phase ?? "worktree_prepare",
|
|
921
|
+
cwd: input.cwd,
|
|
922
|
+
metadata: {
|
|
923
|
+
repoRoot: input.evidence.repoRoot,
|
|
924
|
+
worktreePath: input.evidence.worktreePath,
|
|
925
|
+
expectedBranchName: input.evidence.expectedBranch,
|
|
926
|
+
actualBranchName: input.evidence.actualBranch,
|
|
927
|
+
branchIncoherenceDirtyQuarantineRepair: true,
|
|
928
|
+
rescueBranch: input.rescueBranch,
|
|
929
|
+
rescueCommitSha: input.rescueCommitSha,
|
|
930
|
+
fileCount: input.fileCount,
|
|
931
|
+
dirtyPathSample: input.evidence.dirtyPathSample,
|
|
932
|
+
fingerprint: input.evidence.fingerprint,
|
|
933
|
+
sourceIssueId: input.evidence.sourceIssueId,
|
|
934
|
+
executionWorkspaceId: input.evidence.executionWorkspaceId,
|
|
935
|
+
sourceAuditCommentId: input.sourceAuditCommentId,
|
|
936
|
+
claimantAuditCommentId: input.claimantAuditCommentId,
|
|
937
|
+
},
|
|
938
|
+
run: async () => ({
|
|
939
|
+
status: "succeeded",
|
|
940
|
+
system: `Quarantined dirty git worktree state on ${input.rescueBranch} (${formatShortSha(input.rescueCommitSha)}) and restored recorded branch ${input.evidence.expectedBranch}.\n`,
|
|
941
|
+
}),
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
async function quarantineDirtyWorktreeBranchIncoherence(input) {
|
|
945
|
+
const companyId = await readIssueCompanyId(input.db, input.evidence.sourceIssueId);
|
|
946
|
+
if (!companyId) {
|
|
947
|
+
input.evidence.safeRepair.eligible = false;
|
|
948
|
+
input.evidence.safeRepair.reason = "dirty quarantine repair requires a source issue company for audit";
|
|
949
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
950
|
+
}
|
|
951
|
+
const freshContention = await findGitWorktreeBranchContention({
|
|
952
|
+
db: input.db,
|
|
953
|
+
sourceIssue: input.sourceIssue,
|
|
954
|
+
executionWorkspaceId: input.executionWorkspaceId,
|
|
955
|
+
worktreePath: input.worktreePath,
|
|
956
|
+
actualBranchName: input.evidence.actualBranch,
|
|
957
|
+
});
|
|
958
|
+
input.evidence.contention = freshContention;
|
|
959
|
+
if (freshContention) {
|
|
960
|
+
input.evidence.safeRepair.eligible = false;
|
|
961
|
+
input.evidence.safeRepair.reason = formatDirtyQuarantineContentionRefusal(freshContention);
|
|
962
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
963
|
+
}
|
|
964
|
+
const rescueBranch = buildDirtyQuarantineRescueBranch(input.sourceIssue);
|
|
965
|
+
const fileCount = input.evidence.statusEntryCount ?? input.evidence.dirtyPathSample.length;
|
|
966
|
+
const baseMetadata = {
|
|
967
|
+
repoRoot: input.repoRoot,
|
|
968
|
+
worktreePath: input.worktreePath,
|
|
969
|
+
expectedBranchName: input.expectedBranchName,
|
|
970
|
+
actualBranchName: input.evidence.actualBranch,
|
|
971
|
+
branchIncoherenceDirtyQuarantineRepair: true,
|
|
972
|
+
rescueBranch,
|
|
973
|
+
fingerprint: input.evidence.fingerprint,
|
|
974
|
+
sourceIssueId: input.evidence.sourceIssueId,
|
|
975
|
+
executionWorkspaceId: input.evidence.executionWorkspaceId,
|
|
976
|
+
fileCount,
|
|
977
|
+
dirtyPathSample: input.evidence.dirtyPathSample,
|
|
978
|
+
contention: input.evidence.contention,
|
|
979
|
+
};
|
|
980
|
+
let rescueBranchCreated = false;
|
|
981
|
+
let expectedBranchRestored = false;
|
|
982
|
+
try {
|
|
983
|
+
await assertGitIndexIsUnlocked(input.worktreePath);
|
|
984
|
+
await recordGitOperation(input.recorder, {
|
|
985
|
+
phase: input.phase ?? "worktree_prepare",
|
|
986
|
+
args: ["checkout", "-b", rescueBranch],
|
|
987
|
+
cwd: input.worktreePath,
|
|
988
|
+
metadata: baseMetadata,
|
|
989
|
+
successMessage: `Created rescue branch ${rescueBranch} for dirty git worktree state at ${input.worktreePath}\n`,
|
|
990
|
+
failureLabel: `git checkout -b ${rescueBranch}`,
|
|
991
|
+
});
|
|
992
|
+
rescueBranchCreated = true;
|
|
993
|
+
await recordGitOperation(input.recorder, {
|
|
994
|
+
phase: input.phase ?? "worktree_prepare",
|
|
995
|
+
args: ["add", "-A"],
|
|
996
|
+
cwd: input.worktreePath,
|
|
997
|
+
metadata: baseMetadata,
|
|
998
|
+
successMessage: `Staged dirty git worktree state for rescue branch ${rescueBranch}\n`,
|
|
999
|
+
failureLabel: "git add -A",
|
|
1000
|
+
});
|
|
1001
|
+
await recordGitOperation(input.recorder, {
|
|
1002
|
+
phase: input.phase ?? "worktree_prepare",
|
|
1003
|
+
args: [
|
|
1004
|
+
"commit",
|
|
1005
|
+
"-m",
|
|
1006
|
+
"Paperclip dirty workspace rescue",
|
|
1007
|
+
"-m",
|
|
1008
|
+
[
|
|
1009
|
+
`Source-Issue: ${input.evidence.sourceIdentifier ?? input.evidence.sourceIssueId ?? "unknown"}`,
|
|
1010
|
+
`Run-Id: ${input.heartbeatRunId ?? "unknown"}`,
|
|
1011
|
+
`Recorded-Branch: ${input.expectedBranchName}`,
|
|
1012
|
+
`Live-Branch: ${formatBranchForMessage(input.evidence.actualBranch)}`,
|
|
1013
|
+
`Fingerprint: ${input.evidence.fingerprint}`,
|
|
1014
|
+
].join("\n"),
|
|
1015
|
+
],
|
|
1016
|
+
cwd: input.worktreePath,
|
|
1017
|
+
metadata: baseMetadata,
|
|
1018
|
+
successMessage: `Committed dirty git worktree state to rescue branch ${rescueBranch}\n`,
|
|
1019
|
+
failureLabel: "git commit dirty workspace rescue",
|
|
1020
|
+
});
|
|
1021
|
+
const rescueCommitSha = await runGit(["rev-parse", "HEAD"], input.worktreePath);
|
|
1022
|
+
await recordGitOperation(input.recorder, {
|
|
1023
|
+
phase: input.phase ?? "worktree_prepare",
|
|
1024
|
+
args: ["checkout", input.expectedBranchName],
|
|
1025
|
+
cwd: input.worktreePath,
|
|
1026
|
+
metadata: {
|
|
1027
|
+
...baseMetadata,
|
|
1028
|
+
rescueCommitSha,
|
|
1029
|
+
},
|
|
1030
|
+
successMessage: `Restored recorded branch ${input.expectedBranchName} after dirty workspace rescue ${rescueBranch}\n`,
|
|
1031
|
+
failureLabel: `git checkout ${input.expectedBranchName}`,
|
|
1032
|
+
});
|
|
1033
|
+
expectedBranchRestored = true;
|
|
1034
|
+
// A run that died mid-rebase (or mid-merge/cherry-pick/revert/bisect)
|
|
1035
|
+
// leaves the operation's state directory behind even after the recorded
|
|
1036
|
+
// branch is checked out, which wedges the next git command in the
|
|
1037
|
+
// worktree. The rescue commit above already preserved the in-flight
|
|
1038
|
+
// resolution, so clearing the state metadata here loses nothing.
|
|
1039
|
+
let clearedInProgressOperation = null;
|
|
1040
|
+
const lingeringOperation = await detectGitWorktreeInProgressOperation(input.worktreePath);
|
|
1041
|
+
if (lingeringOperation) {
|
|
1042
|
+
const operationLabel = GIT_IN_PROGRESS_OPERATION_LABELS[lingeringOperation];
|
|
1043
|
+
const quitArgs = GIT_IN_PROGRESS_OPERATION_QUIT_ARGS[lingeringOperation];
|
|
1044
|
+
await recordGitOperation(input.recorder, {
|
|
1045
|
+
phase: input.phase ?? "worktree_prepare",
|
|
1046
|
+
args: quitArgs,
|
|
1047
|
+
cwd: input.worktreePath,
|
|
1048
|
+
metadata: {
|
|
1049
|
+
...baseMetadata,
|
|
1050
|
+
clearedInProgressOperation: lingeringOperation,
|
|
1051
|
+
},
|
|
1052
|
+
successMessage: `Cleared interrupted git ${operationLabel} state after dirty workspace rescue ${rescueBranch}\n`,
|
|
1053
|
+
failureLabel: `git ${quitArgs.join(" ")}`,
|
|
1054
|
+
});
|
|
1055
|
+
const stillInProgress = await detectGitWorktreeInProgressOperation(input.worktreePath);
|
|
1056
|
+
if (stillInProgress) {
|
|
1057
|
+
input.evidence.safeRepair.succeeded = false;
|
|
1058
|
+
input.evidence.safeRepair.reason =
|
|
1059
|
+
`dirty quarantine repair could not clear the interrupted git ${GIT_IN_PROGRESS_OPERATION_LABELS[stillInProgress]} state`;
|
|
1060
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
1061
|
+
}
|
|
1062
|
+
clearedInProgressOperation = lingeringOperation;
|
|
1063
|
+
}
|
|
1064
|
+
const repairedBranch = await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], input.worktreePath)
|
|
1065
|
+
.catch(() => null);
|
|
1066
|
+
if (repairedBranch !== input.expectedBranchName) {
|
|
1067
|
+
input.evidence.safeRepair.succeeded = false;
|
|
1068
|
+
input.evidence.safeRepair.reason =
|
|
1069
|
+
`dirty quarantine repair checked out ${formatBranchForMessage(repairedBranch)} instead of ${input.expectedBranchName}`;
|
|
1070
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
1071
|
+
}
|
|
1072
|
+
const repairedStatus = await runGit(["status", "--porcelain", "--untracked-files=all"], input.worktreePath);
|
|
1073
|
+
if (repairedStatus.trim().length > 0) {
|
|
1074
|
+
input.evidence.safeRepair.succeeded = false;
|
|
1075
|
+
input.evidence.safeRepair.reason = "dirty quarantine repair completed but the worktree is still dirty";
|
|
1076
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
1077
|
+
}
|
|
1078
|
+
const comments = await writeDirtyQuarantineAuditComments({
|
|
1079
|
+
db: input.db,
|
|
1080
|
+
companyId,
|
|
1081
|
+
evidence: input.evidence,
|
|
1082
|
+
sourceIssue: input.sourceIssue,
|
|
1083
|
+
rescueBranch,
|
|
1084
|
+
rescueCommitSha,
|
|
1085
|
+
fileCount,
|
|
1086
|
+
heartbeatRunId: input.heartbeatRunId,
|
|
1087
|
+
});
|
|
1088
|
+
await logDirtyQuarantineActivity({
|
|
1089
|
+
db: input.db,
|
|
1090
|
+
companyId,
|
|
1091
|
+
evidence: input.evidence,
|
|
1092
|
+
rescueBranch,
|
|
1093
|
+
rescueCommitSha,
|
|
1094
|
+
fileCount,
|
|
1095
|
+
heartbeatRunId: input.heartbeatRunId,
|
|
1096
|
+
sourceAuditCommentId: comments.sourceAuditCommentId,
|
|
1097
|
+
claimantAuditCommentId: comments.claimantAuditCommentId,
|
|
1098
|
+
});
|
|
1099
|
+
await recordDirtyQuarantineOperation({
|
|
1100
|
+
recorder: input.recorder,
|
|
1101
|
+
phase: input.phase,
|
|
1102
|
+
cwd: input.worktreePath,
|
|
1103
|
+
evidence: input.evidence,
|
|
1104
|
+
rescueBranch,
|
|
1105
|
+
rescueCommitSha,
|
|
1106
|
+
fileCount,
|
|
1107
|
+
sourceAuditCommentId: comments.sourceAuditCommentId,
|
|
1108
|
+
claimantAuditCommentId: comments.claimantAuditCommentId,
|
|
1109
|
+
});
|
|
1110
|
+
return {
|
|
1111
|
+
rescueBranch,
|
|
1112
|
+
rescueCommitSha,
|
|
1113
|
+
fileCount,
|
|
1114
|
+
clearedInProgressOperation,
|
|
1115
|
+
...comments,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
catch (error) {
|
|
1119
|
+
if (rescueBranchCreated && !expectedBranchRestored) {
|
|
1120
|
+
await runGit(["checkout", input.expectedBranchName], input.worktreePath).catch(() => null);
|
|
1121
|
+
}
|
|
1122
|
+
if (error instanceof WorkspaceRuntimeValidationFailure)
|
|
1123
|
+
throw error;
|
|
1124
|
+
input.evidence.safeRepair.succeeded = false;
|
|
1125
|
+
input.evidence.safeRepair.reason = formatDirtyQuarantineFailure(error);
|
|
1126
|
+
throw branchIncoherenceValidationFailure(input.evidence);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
async function recordForwardBranchReconcileOperation(input) {
|
|
1130
|
+
if (!input.recorder)
|
|
1131
|
+
return;
|
|
1132
|
+
await input.recorder.recordOperation({
|
|
1133
|
+
phase: input.phase ?? "worktree_prepare",
|
|
1134
|
+
cwd: input.cwd,
|
|
1135
|
+
metadata: {
|
|
1136
|
+
repoRoot: input.repoRoot,
|
|
1137
|
+
worktreePath: input.worktreePath,
|
|
1138
|
+
expectedBranchName: input.expectedBranchName,
|
|
1139
|
+
actualBranchName: input.actualBranchName,
|
|
1140
|
+
branchIncoherenceReconcileForward: true,
|
|
1141
|
+
reconcileMode: input.mode,
|
|
1142
|
+
fingerprint: input.fingerprint,
|
|
1143
|
+
sourceIssueId: input.sourceIssueId,
|
|
1144
|
+
executionWorkspaceId: input.executionWorkspaceId,
|
|
1145
|
+
expectedHeadSha: input.expectedHeadSha,
|
|
1146
|
+
actualHeadSha: input.actualHeadSha,
|
|
1147
|
+
ancestryVerdict: input.ancestryVerdict,
|
|
1148
|
+
auditCommentId: input.auditCommentId ?? null,
|
|
1149
|
+
recoveryActionId: input.recoveryActionId ?? null,
|
|
1150
|
+
},
|
|
1151
|
+
run: async () => ({
|
|
1152
|
+
status: "succeeded",
|
|
1153
|
+
system: input.mode === "record_updated"
|
|
1154
|
+
? `Reconciled execution workspace branch record from ${input.expectedBranchName} to ${input.actualBranchName}; worktree left unchanged.\n`
|
|
1155
|
+
: `Adopted live git worktree branch ${input.actualBranchName} for this execution workspace realization; worktree left unchanged.\n`,
|
|
1156
|
+
}),
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
async function logForwardBranchReconcileActivity(input) {
|
|
1160
|
+
await logActivity(input.db, {
|
|
1161
|
+
companyId: input.companyId,
|
|
1162
|
+
actorType: "system",
|
|
1163
|
+
actorId: "workspace_runtime",
|
|
1164
|
+
runId: input.runId,
|
|
1165
|
+
action: "execution_workspace.branch_reconciled",
|
|
1166
|
+
entityType: "execution_workspace",
|
|
1167
|
+
entityId: input.executionWorkspaceId,
|
|
1168
|
+
details: {
|
|
1169
|
+
mode: input.mode,
|
|
1170
|
+
reason: input.reason,
|
|
1171
|
+
fromBranch: input.fromBranch,
|
|
1172
|
+
toBranch: input.toBranch,
|
|
1173
|
+
fromSha: input.fromSha,
|
|
1174
|
+
toSha: input.toSha,
|
|
1175
|
+
ancestryVerdict: input.ancestryVerdict,
|
|
1176
|
+
fingerprint: input.fingerprint,
|
|
1177
|
+
sourceIssueId: input.sourceIssueId,
|
|
1178
|
+
auditCommentId: input.auditCommentId,
|
|
1179
|
+
recoveryActionId: input.recoveryActionId,
|
|
1180
|
+
actor: {
|
|
1181
|
+
type: "system",
|
|
1182
|
+
id: "workspace_runtime",
|
|
1183
|
+
source: "workspace_runtime",
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
export async function reconcilePendingForwardBranchAfterPersistence(input) {
|
|
1189
|
+
const result = await executionWorkspaceService(input.db).reconcileExecutionWorkspaceBranch(input.executionWorkspaceId, {
|
|
1190
|
+
mode: "forward",
|
|
1191
|
+
reason: input.pending.reason,
|
|
1192
|
+
alternateRecoveryFingerprints: [input.pending.prePersistenceFingerprint],
|
|
1193
|
+
actor: {
|
|
1194
|
+
actorType: "system",
|
|
1195
|
+
actorId: "workspace_runtime",
|
|
1196
|
+
agentId: null,
|
|
1197
|
+
runId: input.heartbeatRunId ?? null,
|
|
1198
|
+
},
|
|
1199
|
+
});
|
|
1200
|
+
await logForwardBranchReconcileActivity({
|
|
1201
|
+
db: input.db,
|
|
1202
|
+
companyId: result.workspace.companyId,
|
|
1203
|
+
executionWorkspaceId: result.workspace.id,
|
|
1204
|
+
sourceIssueId: result.workspace.sourceIssueId,
|
|
1205
|
+
runId: input.heartbeatRunId ?? null,
|
|
1206
|
+
mode: "forward",
|
|
1207
|
+
reason: input.pending.reason,
|
|
1208
|
+
fromBranch: result.inspection.fromBranch,
|
|
1209
|
+
toBranch: result.inspection.toBranch,
|
|
1210
|
+
fromSha: result.inspection.fromSha,
|
|
1211
|
+
toSha: result.inspection.toSha,
|
|
1212
|
+
ancestryVerdict: result.inspection.ancestryVerdict,
|
|
1213
|
+
fingerprint: result.inspection.fingerprint,
|
|
1214
|
+
auditCommentId: result.auditCommentId,
|
|
1215
|
+
recoveryActionId: result.recoveryAction?.id ?? null,
|
|
1216
|
+
});
|
|
1217
|
+
await recordForwardBranchReconcileOperation({
|
|
1218
|
+
recorder: input.recorder,
|
|
1219
|
+
phase: input.reconcileOperationPhase,
|
|
1220
|
+
cwd: result.inspection.worktreePath,
|
|
1221
|
+
repoRoot: result.inspection.repoRoot,
|
|
1222
|
+
worktreePath: result.inspection.worktreePath,
|
|
1223
|
+
expectedBranchName: result.inspection.fromBranch,
|
|
1224
|
+
actualBranchName: result.inspection.toBranch,
|
|
1225
|
+
executionWorkspaceId: result.workspace.id,
|
|
1226
|
+
sourceIssueId: result.workspace.sourceIssueId,
|
|
1227
|
+
fingerprint: result.inspection.fingerprint,
|
|
1228
|
+
expectedHeadSha: result.inspection.fromSha,
|
|
1229
|
+
actualHeadSha: result.inspection.toSha,
|
|
1230
|
+
ancestryVerdict: result.inspection.ancestryVerdict,
|
|
1231
|
+
mode: "adopt_for_realize",
|
|
1232
|
+
auditCommentId: result.auditCommentId,
|
|
1233
|
+
recoveryActionId: result.recoveryAction?.id ?? null,
|
|
1234
|
+
});
|
|
1235
|
+
return result;
|
|
1236
|
+
}
|
|
564
1237
|
export async function ensureGitWorktreeBranchCoherent(input) {
|
|
565
1238
|
const expectedBranchName = input.expectedBranchName?.trim();
|
|
566
1239
|
if (!expectedBranchName)
|
|
567
|
-
return;
|
|
1240
|
+
return { branchName: null, reconciledForward: false, warnings: [] };
|
|
568
1241
|
const currentBranch = input.actualBranchName !== undefined
|
|
569
1242
|
? input.actualBranchName
|
|
570
1243
|
: await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], input.worktreePath).catch(() => null);
|
|
571
|
-
if (currentBranch === expectedBranchName)
|
|
572
|
-
return;
|
|
1244
|
+
if (currentBranch === expectedBranchName) {
|
|
1245
|
+
return { branchName: expectedBranchName, reconciledForward: false, warnings: [] };
|
|
1246
|
+
}
|
|
573
1247
|
const evidence = await inspectGitWorktreeBranchIncoherence({
|
|
1248
|
+
db: input.db ?? null,
|
|
574
1249
|
repoRoot: input.repoRoot,
|
|
575
1250
|
worktreePath: input.worktreePath,
|
|
576
1251
|
expectedBranchName,
|
|
@@ -578,10 +1253,203 @@ export async function ensureGitWorktreeBranchCoherent(input) {
|
|
|
578
1253
|
sourceIssue: input.sourceIssue,
|
|
579
1254
|
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
|
580
1255
|
});
|
|
1256
|
+
if (evidence.cleanliness === "dirty" && input.enableWorkspaceDirtyQuarantineRepair === true) {
|
|
1257
|
+
if (!input.db) {
|
|
1258
|
+
evidence.safeRepair.reason = "dirty quarantine repair requires database access for claimant checks and audit";
|
|
1259
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1260
|
+
}
|
|
1261
|
+
if (!evidence.provenance.registeredPathFound) {
|
|
1262
|
+
evidence.safeRepair.reason = "dirty quarantine repair requires a registered git worktree path";
|
|
1263
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1264
|
+
}
|
|
1265
|
+
if (!evidence.provenance.expectedBranchExists) {
|
|
1266
|
+
evidence.safeRepair.reason = "dirty quarantine repair requires the recorded branch to exist";
|
|
1267
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1268
|
+
}
|
|
1269
|
+
if (evidence.contention) {
|
|
1270
|
+
evidence.safeRepair.eligible = false;
|
|
1271
|
+
evidence.safeRepair.reason = formatDirtyQuarantineContentionRefusal(evidence.contention);
|
|
1272
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1273
|
+
}
|
|
1274
|
+
await assertDirtyQuarantineRuntimeServicesStopped({
|
|
1275
|
+
db: input.db,
|
|
1276
|
+
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
|
1277
|
+
evidence,
|
|
1278
|
+
});
|
|
1279
|
+
evidence.safeRepair.eligible = true;
|
|
1280
|
+
evidence.safeRepair.attempted = true;
|
|
1281
|
+
evidence.safeRepair.reason = "dirty worktree can be quarantined on a rescue branch before restoring the recorded branch";
|
|
1282
|
+
const result = await quarantineDirtyWorktreeBranchIncoherence({
|
|
1283
|
+
db: input.db,
|
|
1284
|
+
repoRoot: input.repoRoot,
|
|
1285
|
+
worktreePath: input.worktreePath,
|
|
1286
|
+
expectedBranchName,
|
|
1287
|
+
sourceIssue: input.sourceIssue,
|
|
1288
|
+
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
|
1289
|
+
heartbeatRunId: input.heartbeatRunId ?? null,
|
|
1290
|
+
evidence,
|
|
1291
|
+
phase: input.reconcileOperationPhase,
|
|
1292
|
+
recorder: input.recorder ?? null,
|
|
1293
|
+
});
|
|
1294
|
+
evidence.safeRepair.succeeded = true;
|
|
1295
|
+
evidence.safeRepair.reason = result.clearedInProgressOperation
|
|
1296
|
+
? `dirty worktree quarantined on ${result.rescueBranch} at ${formatShortSha(result.rescueCommitSha)}; interrupted git ${GIT_IN_PROGRESS_OPERATION_LABELS[result.clearedInProgressOperation]} state cleared`
|
|
1297
|
+
: `dirty worktree quarantined on ${result.rescueBranch} at ${formatShortSha(result.rescueCommitSha)}`;
|
|
1298
|
+
return {
|
|
1299
|
+
branchName: expectedBranchName,
|
|
1300
|
+
reconciledForward: false,
|
|
1301
|
+
dirtyQuarantineRepair: result,
|
|
1302
|
+
warnings: [
|
|
1303
|
+
`Execution workspace dirty worktree state was quarantined on rescue branch "${result.rescueBranch}" (${formatShortSha(result.rescueCommitSha)}; ${result.fileCount} ${result.fileCount === 1 ? "file" : "files"}) before restoring recorded branch "${expectedBranchName}".${result.clearedInProgressOperation ? ` An interrupted git ${GIT_IN_PROGRESS_OPERATION_LABELS[result.clearedInProgressOperation]} was also cleared; its in-flight state is preserved on the rescue branch.` : ""}`,
|
|
1304
|
+
],
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
if (input.enableWorkspaceBranchReconcileForward === true &&
|
|
1308
|
+
evidence.provenance.ancestryVerdict === "ancestor" &&
|
|
1309
|
+
!evidence.provenance.sameHead &&
|
|
1310
|
+
evidence.cleanliness === "clean" &&
|
|
1311
|
+
currentBranch) {
|
|
1312
|
+
const reason = "Automatic forward reconciliation: recorded branch is an ancestor of the checked-out branch.";
|
|
1313
|
+
if (input.executionWorkspaceId && input.persistForwardReconcile !== false) {
|
|
1314
|
+
if (!input.db) {
|
|
1315
|
+
evidence.safeRepair.reason = "forward reconciliation requires database access to update the execution workspace record";
|
|
1316
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1317
|
+
}
|
|
1318
|
+
try {
|
|
1319
|
+
const result = await executionWorkspaceService(input.db).reconcileExecutionWorkspaceBranch(input.executionWorkspaceId, {
|
|
1320
|
+
mode: "forward",
|
|
1321
|
+
reason,
|
|
1322
|
+
actor: {
|
|
1323
|
+
actorType: "system",
|
|
1324
|
+
actorId: "workspace_runtime",
|
|
1325
|
+
agentId: null,
|
|
1326
|
+
runId: input.heartbeatRunId ?? null,
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
await logForwardBranchReconcileActivity({
|
|
1330
|
+
db: input.db,
|
|
1331
|
+
companyId: result.workspace.companyId,
|
|
1332
|
+
executionWorkspaceId: result.workspace.id,
|
|
1333
|
+
sourceIssueId: result.workspace.sourceIssueId ?? evidence.sourceIssueId ?? null,
|
|
1334
|
+
runId: input.heartbeatRunId ?? null,
|
|
1335
|
+
mode: "forward",
|
|
1336
|
+
reason,
|
|
1337
|
+
fromBranch: result.inspection.fromBranch,
|
|
1338
|
+
toBranch: result.inspection.toBranch,
|
|
1339
|
+
fromSha: result.inspection.fromSha,
|
|
1340
|
+
toSha: result.inspection.toSha,
|
|
1341
|
+
ancestryVerdict: result.inspection.ancestryVerdict,
|
|
1342
|
+
fingerprint: result.inspection.fingerprint,
|
|
1343
|
+
auditCommentId: result.auditCommentId,
|
|
1344
|
+
recoveryActionId: result.recoveryAction?.id ?? null,
|
|
1345
|
+
});
|
|
1346
|
+
await recordForwardBranchReconcileOperation({
|
|
1347
|
+
recorder: input.recorder,
|
|
1348
|
+
phase: input.reconcileOperationPhase,
|
|
1349
|
+
cwd: input.worktreePath,
|
|
1350
|
+
repoRoot: result.inspection.repoRoot,
|
|
1351
|
+
worktreePath: result.inspection.worktreePath,
|
|
1352
|
+
expectedBranchName: result.inspection.fromBranch,
|
|
1353
|
+
actualBranchName: result.inspection.toBranch,
|
|
1354
|
+
executionWorkspaceId: result.workspace.id,
|
|
1355
|
+
sourceIssueId: result.workspace.sourceIssueId ?? evidence.sourceIssueId ?? null,
|
|
1356
|
+
fingerprint: result.inspection.fingerprint,
|
|
1357
|
+
expectedHeadSha: result.inspection.fromSha,
|
|
1358
|
+
actualHeadSha: result.inspection.toSha,
|
|
1359
|
+
ancestryVerdict: result.inspection.ancestryVerdict,
|
|
1360
|
+
mode: "record_updated",
|
|
1361
|
+
auditCommentId: result.auditCommentId,
|
|
1362
|
+
recoveryActionId: result.recoveryAction?.id ?? null,
|
|
1363
|
+
});
|
|
1364
|
+
return { branchName: result.inspection.toBranch, reconciledForward: true, warnings: [] };
|
|
1365
|
+
}
|
|
1366
|
+
catch (error) {
|
|
1367
|
+
evidence.safeRepair.reason =
|
|
1368
|
+
`forward reconciliation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1369
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (!input.db) {
|
|
1373
|
+
evidence.safeRepair.reason = "forward reconciliation adoption requires database access to audit after workspace realization";
|
|
1374
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
branchName: currentBranch,
|
|
1378
|
+
reconciledForward: true,
|
|
1379
|
+
warnings: [],
|
|
1380
|
+
pendingForwardBranchReconcile: {
|
|
1381
|
+
recordedBranchName: expectedBranchName,
|
|
1382
|
+
adoptedBranchName: currentBranch,
|
|
1383
|
+
prePersistenceFingerprint: evidence.fingerprint,
|
|
1384
|
+
reason,
|
|
1385
|
+
},
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
581
1388
|
if (!evidence.safeRepair.eligible) {
|
|
582
1389
|
throw branchIncoherenceValidationFailure(evidence);
|
|
583
1390
|
}
|
|
584
1391
|
evidence.safeRepair.attempted = true;
|
|
1392
|
+
const warningPrefix = `Execution workspace branch metadata was self-healed from "${expectedBranchName}" to "${formatBranchForMessage(currentBranch)}" at ${input.worktreePath}.`;
|
|
1393
|
+
if (currentBranch &&
|
|
1394
|
+
evidence.provenance.actualBranchExists === true &&
|
|
1395
|
+
evidence.provenance.ancestryVerdict === "ancestor" &&
|
|
1396
|
+
!evidence.provenance.sameHead) {
|
|
1397
|
+
evidence.safeRepair.succeeded = true;
|
|
1398
|
+
evidence.safeRepair.reason = "clean worktree adopted the checked-out branch because it is forward of the recorded branch";
|
|
1399
|
+
return {
|
|
1400
|
+
branchName: currentBranch,
|
|
1401
|
+
reconciledForward: false,
|
|
1402
|
+
warnings: [
|
|
1403
|
+
`${warningPrefix} The checked-out branch contains the recorded branch plus newer commits, so Paperclip adopted it for subsequent runs.`,
|
|
1404
|
+
],
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
if (currentBranch === null &&
|
|
1408
|
+
evidence.provenance.ancestryVerdict === "ancestor" &&
|
|
1409
|
+
!evidence.provenance.sameHead &&
|
|
1410
|
+
evidence.provenance.actualHeadSha) {
|
|
1411
|
+
try {
|
|
1412
|
+
await recordGitOperation(input.recorder, {
|
|
1413
|
+
phase: "worktree_prepare",
|
|
1414
|
+
args: ["checkout", "-B", expectedBranchName, evidence.provenance.actualHeadSha],
|
|
1415
|
+
cwd: input.worktreePath,
|
|
1416
|
+
metadata: {
|
|
1417
|
+
repoRoot: input.repoRoot,
|
|
1418
|
+
worktreePath: input.worktreePath,
|
|
1419
|
+
expectedBranchName,
|
|
1420
|
+
actualBranchName: currentBranch,
|
|
1421
|
+
branchIncoherenceRepair: true,
|
|
1422
|
+
detachedHeadRepair: true,
|
|
1423
|
+
fingerprint: evidence.fingerprint,
|
|
1424
|
+
sourceIssueId: evidence.sourceIssueId,
|
|
1425
|
+
executionWorkspaceId: evidence.executionWorkspaceId,
|
|
1426
|
+
},
|
|
1427
|
+
successMessage: `Reattached detached git worktree HEAD at ${input.worktreePath} to ${expectedBranchName}\n`,
|
|
1428
|
+
failureLabel: `git checkout -B ${expectedBranchName} ${formatShortSha(evidence.provenance.actualHeadSha)}`,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
catch (error) {
|
|
1432
|
+
evidence.safeRepair.succeeded = false;
|
|
1433
|
+
evidence.safeRepair.reason = `safe detached HEAD reattachment failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1434
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1435
|
+
}
|
|
1436
|
+
const repairedBranch = await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], input.worktreePath)
|
|
1437
|
+
.catch(() => null);
|
|
1438
|
+
if (repairedBranch !== expectedBranchName) {
|
|
1439
|
+
evidence.safeRepair.succeeded = false;
|
|
1440
|
+
evidence.safeRepair.reason = `reattach completed but HEAD is ${formatBranchForMessage(repairedBranch)}`;
|
|
1441
|
+
throw branchIncoherenceValidationFailure(evidence);
|
|
1442
|
+
}
|
|
1443
|
+
evidence.safeRepair.succeeded = true;
|
|
1444
|
+
evidence.safeRepair.reason = "clean detached worktree HEAD was reattached to the recorded branch";
|
|
1445
|
+
return {
|
|
1446
|
+
branchName: expectedBranchName,
|
|
1447
|
+
reconciledForward: false,
|
|
1448
|
+
warnings: [
|
|
1449
|
+
`${warningPrefix} The detached HEAD contained the recorded branch plus newer commits, so Paperclip moved the recorded branch to that HEAD.`,
|
|
1450
|
+
],
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
585
1453
|
try {
|
|
586
1454
|
await recordGitOperation(input.recorder, {
|
|
587
1455
|
phase: "worktree_prepare",
|
|
@@ -615,6 +1483,13 @@ export async function ensureGitWorktreeBranchCoherent(input) {
|
|
|
615
1483
|
}
|
|
616
1484
|
evidence.safeRepair.succeeded = true;
|
|
617
1485
|
evidence.safeRepair.reason = "clean worktree checked out the recorded branch";
|
|
1486
|
+
return {
|
|
1487
|
+
branchName: expectedBranchName,
|
|
1488
|
+
reconciledForward: false,
|
|
1489
|
+
warnings: [
|
|
1490
|
+
`Execution workspace branch metadata was self-healed by checking out recorded branch "${expectedBranchName}" at ${input.worktreePath}.`,
|
|
1491
|
+
],
|
|
1492
|
+
};
|
|
618
1493
|
}
|
|
619
1494
|
// Resolve the authoritative base ref for a fresh worktree. A configured local
|
|
620
1495
|
// branch is mapped to its `origin/<branch>` counterpart so unpushed local
|
|
@@ -1165,12 +2040,13 @@ export async function realizeExecutionWorkspace(input) {
|
|
|
1165
2040
|
projectId: input.base.projectId,
|
|
1166
2041
|
repoRef: input.base.repoRef,
|
|
1167
2042
|
});
|
|
1168
|
-
|
|
2043
|
+
let branchName = sanitizeBranchName(renderedBranch);
|
|
1169
2044
|
const configuredParentDir = asString(rawStrategy.worktreeParentDir, "");
|
|
1170
2045
|
const worktreeParentDir = configuredParentDir
|
|
1171
2046
|
? resolveConfiguredPath(configuredParentDir, repoRoot)
|
|
1172
2047
|
: path.join(repoRoot, ".paperclip", "worktrees");
|
|
1173
2048
|
const worktreePath = path.join(worktreeParentDir, branchName);
|
|
2049
|
+
let pendingForwardBranchReconcile = null;
|
|
1174
2050
|
const configuredBaseRef = typeof rawStrategy.baseRef === "string" && rawStrategy.baseRef.length > 0
|
|
1175
2051
|
? rawStrategy.baseRef
|
|
1176
2052
|
: input.base.repoRef ?? null;
|
|
@@ -1181,12 +2057,12 @@ export async function realizeExecutionWorkspace(input) {
|
|
|
1181
2057
|
];
|
|
1182
2058
|
const currentBaseRefSha = await resolveBaseRefSha(repoRoot, baseRef);
|
|
1183
2059
|
await fs.mkdir(worktreeParentDir, { recursive: true });
|
|
1184
|
-
async function reuseExistingWorktree(reusablePath) {
|
|
2060
|
+
async function reuseExistingWorktree(reusablePath, effectiveBranchName = branchName, extraWarnings = []) {
|
|
1185
2061
|
const refresh = currentBaseRefSha
|
|
1186
2062
|
? await refreshUnstartedWorktreeToBase({
|
|
1187
2063
|
repoRoot,
|
|
1188
2064
|
worktreePath: reusablePath,
|
|
1189
|
-
branchName,
|
|
2065
|
+
branchName: effectiveBranchName,
|
|
1190
2066
|
baseRef,
|
|
1191
2067
|
currentBaseRefSha,
|
|
1192
2068
|
recorder: input.recorder ?? null,
|
|
@@ -1207,7 +2083,7 @@ export async function realizeExecutionWorkspace(input) {
|
|
|
1207
2083
|
metadata: {
|
|
1208
2084
|
repoRoot,
|
|
1209
2085
|
worktreePath: reusablePath,
|
|
1210
|
-
branchName,
|
|
2086
|
+
branchName: effectiveBranchName,
|
|
1211
2087
|
baseRef,
|
|
1212
2088
|
currentBaseRefSha: baseDrift.currentBaseRefSha,
|
|
1213
2089
|
branchBaseRefSha: baseDrift.branchBaseRefSha,
|
|
@@ -1226,7 +2102,7 @@ export async function realizeExecutionWorkspace(input) {
|
|
|
1226
2102
|
base: input.base,
|
|
1227
2103
|
repoRoot,
|
|
1228
2104
|
worktreePath: reusablePath,
|
|
1229
|
-
branchName,
|
|
2105
|
+
branchName: effectiveBranchName,
|
|
1230
2106
|
issue: input.issue,
|
|
1231
2107
|
agent: input.agent,
|
|
1232
2108
|
created: false,
|
|
@@ -1237,11 +2113,12 @@ export async function realizeExecutionWorkspace(input) {
|
|
|
1237
2113
|
repoRef: baseRef,
|
|
1238
2114
|
strategy: "git_worktree",
|
|
1239
2115
|
cwd: reusablePath,
|
|
1240
|
-
branchName,
|
|
2116
|
+
branchName: effectiveBranchName,
|
|
1241
2117
|
worktreePath: reusablePath,
|
|
1242
|
-
warnings: [...baseRefreshWarnings, ...baseDrift.warnings],
|
|
2118
|
+
warnings: [...extraWarnings, ...baseRefreshWarnings, ...baseDrift.warnings],
|
|
1243
2119
|
created: false,
|
|
1244
2120
|
baseRefSha: refresh.baseRefSha ?? baseDrift.branchBaseRefSha ?? baseDrift.currentBaseRefSha,
|
|
2121
|
+
pendingForwardBranchReconcile,
|
|
1245
2122
|
};
|
|
1246
2123
|
}
|
|
1247
2124
|
async function validateReusableWorktree(reusablePath) {
|
|
@@ -1251,38 +2128,55 @@ export async function realizeExecutionWorkspace(input) {
|
|
|
1251
2128
|
expectedBranchName: branchName,
|
|
1252
2129
|
}).catch(() => null);
|
|
1253
2130
|
if (validation && !validation.valid && validation.reasonCode === "branch_mismatch") {
|
|
1254
|
-
await ensureGitWorktreeBranchCoherent({
|
|
2131
|
+
const coherence = await ensureGitWorktreeBranchCoherent({
|
|
2132
|
+
db: input.db ?? null,
|
|
1255
2133
|
repoRoot,
|
|
1256
2134
|
worktreePath: reusablePath,
|
|
1257
2135
|
expectedBranchName: branchName,
|
|
1258
2136
|
actualBranchName: validation.actualBranchName ?? null,
|
|
1259
2137
|
sourceIssue: input.issue,
|
|
1260
2138
|
executionWorkspaceId: null,
|
|
2139
|
+
heartbeatRunId: input.heartbeatRunId ?? null,
|
|
2140
|
+
enableWorkspaceBranchReconcileForward: input.enableWorkspaceBranchReconcileForward === true,
|
|
2141
|
+
enableWorkspaceDirtyQuarantineRepair: input.enableWorkspaceDirtyQuarantineRepair === true,
|
|
2142
|
+
reconcileOperationPhase: "worktree_prepare",
|
|
1261
2143
|
recorder: input.recorder ?? null,
|
|
1262
2144
|
});
|
|
1263
|
-
|
|
2145
|
+
const effectiveBranchName = coherence.branchName ?? branchName;
|
|
2146
|
+
if (coherence.reconciledForward) {
|
|
2147
|
+
branchName = effectiveBranchName;
|
|
2148
|
+
pendingForwardBranchReconcile = coherence.pendingForwardBranchReconcile ?? null;
|
|
2149
|
+
}
|
|
2150
|
+
const nextValidation = await validateLinkedGitWorktree({
|
|
1264
2151
|
repoRoot,
|
|
1265
2152
|
worktreePath: reusablePath,
|
|
1266
|
-
expectedBranchName:
|
|
2153
|
+
expectedBranchName: effectiveBranchName,
|
|
1267
2154
|
}).catch(() => null);
|
|
2155
|
+
return {
|
|
2156
|
+
validation: nextValidation,
|
|
2157
|
+
branchName: effectiveBranchName,
|
|
2158
|
+
warnings: coherence.warnings,
|
|
2159
|
+
};
|
|
1268
2160
|
}
|
|
1269
|
-
return validation;
|
|
2161
|
+
return { validation, branchName, warnings: [] };
|
|
1270
2162
|
}
|
|
1271
2163
|
const existingWorktree = await directoryExists(worktreePath);
|
|
1272
2164
|
if (existingWorktree) {
|
|
1273
|
-
const
|
|
1274
|
-
if (validation?.valid) {
|
|
1275
|
-
return await reuseExistingWorktree(worktreePath);
|
|
2165
|
+
const reusable = await validateReusableWorktree(worktreePath);
|
|
2166
|
+
if (reusable.validation?.valid) {
|
|
2167
|
+
return await reuseExistingWorktree(worktreePath, reusable.branchName, reusable.warnings);
|
|
1276
2168
|
}
|
|
2169
|
+
const validation = reusable.validation;
|
|
1277
2170
|
const reason = validation && !validation.valid ? ` (${validation.reason})` : "";
|
|
1278
2171
|
throw new Error(`Configured worktree path "${worktreePath}" already exists and is not a reusable git worktree${reason}.`);
|
|
1279
2172
|
}
|
|
1280
2173
|
const registeredBranchWorktree = await findRegisteredGitWorktreeByBranch(repoRoot, branchName);
|
|
1281
2174
|
if (registeredBranchWorktree) {
|
|
1282
|
-
const
|
|
1283
|
-
if (validation?.valid) {
|
|
1284
|
-
return await reuseExistingWorktree(registeredBranchWorktree);
|
|
2175
|
+
const reusable = await validateReusableWorktree(registeredBranchWorktree);
|
|
2176
|
+
if (reusable.validation?.valid) {
|
|
2177
|
+
return await reuseExistingWorktree(registeredBranchWorktree, reusable.branchName, reusable.warnings);
|
|
1285
2178
|
}
|
|
2179
|
+
const validation = reusable.validation;
|
|
1286
2180
|
const reason = validation && !validation.valid ? ` (${validation.reason})` : "";
|
|
1287
2181
|
throw new Error(`Registered worktree for branch "${branchName}" at "${registeredBranchWorktree}" is not reusable${reason}.`);
|
|
1288
2182
|
}
|
|
@@ -1391,15 +2285,29 @@ export async function ensurePersistedExecutionWorkspaceAvailable(input) {
|
|
|
1391
2285
|
if (await directoryExists(cwd)) {
|
|
1392
2286
|
const reuseBaseRef = input.workspace.baseRef ?? input.base.repoRef ?? null;
|
|
1393
2287
|
const reuseWorktreePath = realized.worktreePath ?? cwd;
|
|
2288
|
+
const repairWarnings = [];
|
|
1394
2289
|
if (await isGitCheckout(reuseWorktreePath)) {
|
|
1395
|
-
await ensureGitWorktreeBranchCoherent({
|
|
2290
|
+
const coherence = await ensureGitWorktreeBranchCoherent({
|
|
2291
|
+
db: input.db ?? null,
|
|
1396
2292
|
repoRoot,
|
|
1397
2293
|
worktreePath: reuseWorktreePath,
|
|
1398
2294
|
expectedBranchName: realized.branchName,
|
|
1399
2295
|
sourceIssue: input.issue,
|
|
1400
2296
|
executionWorkspaceId: input.workspace.id ?? null,
|
|
2297
|
+
heartbeatRunId: input.heartbeatRunId ?? null,
|
|
2298
|
+
enableWorkspaceBranchReconcileForward: input.enableWorkspaceBranchReconcileForward === true,
|
|
2299
|
+
enableWorkspaceDirtyQuarantineRepair: input.enableWorkspaceDirtyQuarantineRepair === true,
|
|
2300
|
+
persistForwardReconcile: false,
|
|
2301
|
+
reconcileOperationPhase: "worktree_prepare",
|
|
1401
2302
|
recorder: input.recorder ?? null,
|
|
1402
2303
|
});
|
|
2304
|
+
if (coherence.branchName) {
|
|
2305
|
+
realized.branchName = coherence.branchName;
|
|
2306
|
+
}
|
|
2307
|
+
if (coherence.reconciledForward) {
|
|
2308
|
+
realized.pendingForwardBranchReconcile = coherence.pendingForwardBranchReconcile ?? null;
|
|
2309
|
+
}
|
|
2310
|
+
repairWarnings.push(...coherence.warnings);
|
|
1403
2311
|
}
|
|
1404
2312
|
const validation = await validateLinkedGitWorktree({
|
|
1405
2313
|
repoRoot,
|
|
@@ -1438,7 +2346,7 @@ export async function ensurePersistedExecutionWorkspaceAvailable(input) {
|
|
|
1438
2346
|
recordedBaseRefSha,
|
|
1439
2347
|
skipRefresh: true,
|
|
1440
2348
|
});
|
|
1441
|
-
realized.warnings = [...baseRefreshWarnings, ...baseDrift.warnings];
|
|
2349
|
+
realized.warnings = [...repairWarnings, ...baseRefreshWarnings, ...baseDrift.warnings];
|
|
1442
2350
|
realized.baseRefSha = refresh.baseRefSha ?? recordedBaseRefSha ?? baseDrift.branchBaseRefSha ?? baseDrift.currentBaseRefSha;
|
|
1443
2351
|
if (provisionCommand) {
|
|
1444
2352
|
await provisionExecutionWorktree({
|
|
@@ -1870,13 +2778,20 @@ async function waitForReadiness(input) {
|
|
|
1870
2778
|
const readinessType = asString(readiness.type, "");
|
|
1871
2779
|
if (readinessType !== "http" || !input.url)
|
|
1872
2780
|
return;
|
|
2781
|
+
const readinessUrl = resolveRuntimeServiceHealthUrl(input.url, {
|
|
2782
|
+
serviceName: input.serviceName,
|
|
2783
|
+
command: input.command,
|
|
2784
|
+
});
|
|
2785
|
+
if (!readinessUrl) {
|
|
2786
|
+
throw new Error(`Readiness check failed: could not resolve health URL for ${input.url}`);
|
|
2787
|
+
}
|
|
1873
2788
|
const timeoutSec = resolveWorkspaceRuntimeReadinessTimeoutSec(input.service);
|
|
1874
2789
|
const intervalMs = Math.max(100, asNumber(readiness.intervalMs, 500));
|
|
1875
2790
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
1876
2791
|
let lastError = "service did not become ready";
|
|
1877
2792
|
while (Date.now() < deadline) {
|
|
1878
2793
|
try {
|
|
1879
|
-
const response = await fetch(
|
|
2794
|
+
const response = await fetch(readinessUrl);
|
|
1880
2795
|
if (response.ok)
|
|
1881
2796
|
return;
|
|
1882
2797
|
lastError = `received HTTP ${response.status}`;
|
|
@@ -1886,7 +2801,7 @@ async function waitForReadiness(input) {
|
|
|
1886
2801
|
}
|
|
1887
2802
|
await delay(intervalMs);
|
|
1888
2803
|
}
|
|
1889
|
-
throw new Error(`Readiness check failed for ${
|
|
2804
|
+
throw new Error(`Readiness check failed for ${readinessUrl}: ${lastError}`);
|
|
1890
2805
|
}
|
|
1891
2806
|
function isPaperclipDevRuntimeService(input) {
|
|
1892
2807
|
const serviceName = (input.serviceName ?? "").trim().toLowerCase();
|
|
@@ -1917,7 +2832,7 @@ async function isRuntimeServiceUrlHealthy(url, input) {
|
|
|
1917
2832
|
return true;
|
|
1918
2833
|
const healthUrl = resolveRuntimeServiceHealthUrl(url, input);
|
|
1919
2834
|
if (!healthUrl)
|
|
1920
|
-
return
|
|
2835
|
+
return false;
|
|
1921
2836
|
try {
|
|
1922
2837
|
const response = await fetch(healthUrl, { signal: AbortSignal.timeout(2_000) });
|
|
1923
2838
|
return response.ok;
|
|
@@ -2071,7 +2986,7 @@ export function normalizeAdapterManagedRuntimeServices(input) {
|
|
|
2071
2986
|
};
|
|
2072
2987
|
});
|
|
2073
2988
|
}
|
|
2074
|
-
async function
|
|
2989
|
+
async function spawnLocalRuntimeService(input) {
|
|
2075
2990
|
const leaseRunId = input.leaseRunId === undefined ? input.runId : input.leaseRunId;
|
|
2076
2991
|
const startedByRunId = input.startedByRunId === undefined ? input.runId : input.startedByRunId;
|
|
2077
2992
|
const identity = resolveRuntimeServiceReuseIdentity({
|
|
@@ -2168,47 +3083,58 @@ async function startLocalRuntimeService(input) {
|
|
|
2168
3083
|
}
|
|
2169
3084
|
else {
|
|
2170
3085
|
return {
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
3086
|
+
record: {
|
|
3087
|
+
id: adoptedRecord.runtimeServiceId ?? randomUUID(),
|
|
3088
|
+
companyId: input.agent.companyId,
|
|
3089
|
+
projectId: input.workspace.projectId,
|
|
3090
|
+
projectWorkspaceId: input.workspace.workspaceId,
|
|
3091
|
+
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
|
3092
|
+
issueId: input.issue?.id ?? null,
|
|
3093
|
+
serviceName,
|
|
3094
|
+
status: "running",
|
|
3095
|
+
lifecycle,
|
|
3096
|
+
scopeType: input.scopeType,
|
|
3097
|
+
scopeId: input.scopeId,
|
|
3098
|
+
reuseKey: input.reuseKey,
|
|
3099
|
+
command,
|
|
3100
|
+
cwd: serviceCwd,
|
|
3101
|
+
port: adoptedRecord.port ?? port,
|
|
3102
|
+
url: adoptedRecord.url ?? url,
|
|
3103
|
+
provider: "local_process",
|
|
3104
|
+
providerRef: String(adoptedRecord.pid),
|
|
3105
|
+
ownerAgentId: input.agent.id ?? null,
|
|
3106
|
+
startedByRunId,
|
|
3107
|
+
lastUsedAt: new Date().toISOString(),
|
|
3108
|
+
startedAt: adoptedRecord.startedAt,
|
|
3109
|
+
stoppedAt: null,
|
|
3110
|
+
stopPolicy,
|
|
3111
|
+
healthStatus: "healthy",
|
|
3112
|
+
reused: true,
|
|
3113
|
+
db: input.db,
|
|
3114
|
+
child: null,
|
|
3115
|
+
leaseRunIds: leaseRunId ? new Set([leaseRunId]) : new Set(),
|
|
3116
|
+
idleTimer: null,
|
|
3117
|
+
envFingerprint,
|
|
3118
|
+
serviceKey,
|
|
3119
|
+
profileKind: "workspace-runtime",
|
|
3120
|
+
processGroupId: adoptedRecord.processGroupId ?? null,
|
|
3121
|
+
},
|
|
3122
|
+
readiness: Promise.resolve(),
|
|
2205
3123
|
};
|
|
2206
3124
|
}
|
|
2207
3125
|
}
|
|
2208
3126
|
if (identityPort) {
|
|
2209
3127
|
const ownerPid = await readLocalServicePortOwner(identityPort);
|
|
2210
3128
|
if (ownerPid) {
|
|
2211
|
-
|
|
3129
|
+
const ownerCwd = await readLocalServiceProcessCwd(ownerPid);
|
|
3130
|
+
const ownerIsInWorkspace = ownerCwd
|
|
3131
|
+
? await isLocalServiceProcessInWorkspace(ownerCwd, serviceCwd)
|
|
3132
|
+
: null;
|
|
3133
|
+
const ownerDescription = ownerCwd ? `pid ${ownerPid} (cwd: ${ownerCwd})` : `pid ${ownerPid} (cwd unavailable)`;
|
|
3134
|
+
if (ownerIsInWorkspace === false) {
|
|
3135
|
+
throw new Error(`Runtime service "${serviceName}" could not start because port ${identityPort} has a cross-workspace port conflict with ${ownerDescription}; requested workspace: ${serviceCwd}. Stop the other service or configure a different port.`);
|
|
3136
|
+
}
|
|
3137
|
+
throw new Error(`Runtime service "${serviceName}" could not start because port ${identityPort} is already in use by ${ownerDescription}`);
|
|
2212
3138
|
}
|
|
2213
3139
|
}
|
|
2214
3140
|
await ensureServerWorkspaceLinksCurrent(serviceCwd, {
|
|
@@ -2240,16 +3166,7 @@ async function startLocalRuntimeService(input) {
|
|
|
2240
3166
|
if (input.onLog)
|
|
2241
3167
|
await input.onLog("stderr", `[service:${serviceName}] ${text}`);
|
|
2242
3168
|
});
|
|
2243
|
-
|
|
2244
|
-
await Promise.race([
|
|
2245
|
-
waitForReadiness({ service: input.service, url }),
|
|
2246
|
-
spawnErrorPromise,
|
|
2247
|
-
]);
|
|
2248
|
-
}
|
|
2249
|
-
catch (err) {
|
|
2250
|
-
terminateChildProcess(child);
|
|
2251
|
-
throw new Error(`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`);
|
|
2252
|
-
}
|
|
3169
|
+
const nowIso = new Date().toISOString();
|
|
2253
3170
|
const record = {
|
|
2254
3171
|
id: stoppedReuseCandidate?.id ?? randomUUID(),
|
|
2255
3172
|
companyId: input.agent.companyId,
|
|
@@ -2258,7 +3175,7 @@ async function startLocalRuntimeService(input) {
|
|
|
2258
3175
|
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
|
2259
3176
|
issueId: input.issue?.id ?? null,
|
|
2260
3177
|
serviceName,
|
|
2261
|
-
status: "
|
|
3178
|
+
status: "starting",
|
|
2262
3179
|
lifecycle,
|
|
2263
3180
|
scopeType: input.scopeType,
|
|
2264
3181
|
scopeId: input.scopeId,
|
|
@@ -2271,11 +3188,11 @@ async function startLocalRuntimeService(input) {
|
|
|
2271
3188
|
providerRef: child.pid ? String(child.pid) : null,
|
|
2272
3189
|
ownerAgentId: input.agent.id ?? null,
|
|
2273
3190
|
startedByRunId,
|
|
2274
|
-
lastUsedAt:
|
|
2275
|
-
startedAt:
|
|
3191
|
+
lastUsedAt: nowIso,
|
|
3192
|
+
startedAt: nowIso,
|
|
2276
3193
|
stoppedAt: null,
|
|
2277
3194
|
stopPolicy,
|
|
2278
|
-
healthStatus: "
|
|
3195
|
+
healthStatus: "unknown",
|
|
2279
3196
|
reused: false,
|
|
2280
3197
|
db: input.db,
|
|
2281
3198
|
child,
|
|
@@ -2314,7 +3231,33 @@ async function startLocalRuntimeService(input) {
|
|
|
2314
3231
|
},
|
|
2315
3232
|
});
|
|
2316
3233
|
}
|
|
2317
|
-
|
|
3234
|
+
const readinessPromise = Promise.race([
|
|
3235
|
+
waitForReadiness({ service: input.service, serviceName, command, url }),
|
|
3236
|
+
spawnErrorPromise,
|
|
3237
|
+
]).then(async () => {
|
|
3238
|
+
record.status = "running";
|
|
3239
|
+
record.healthStatus = "healthy";
|
|
3240
|
+
record.lastUsedAt = new Date().toISOString();
|
|
3241
|
+
record.stoppedAt = null;
|
|
3242
|
+
await touchLocalServiceRegistryRecord(record.serviceKey, {
|
|
3243
|
+
runtimeServiceId: record.id,
|
|
3244
|
+
lastSeenAt: record.lastUsedAt,
|
|
3245
|
+
});
|
|
3246
|
+
}).catch(async (err) => {
|
|
3247
|
+
terminateChildProcess(child);
|
|
3248
|
+
record.status = "stopped";
|
|
3249
|
+
record.healthStatus = "unhealthy";
|
|
3250
|
+
record.lastUsedAt = new Date().toISOString();
|
|
3251
|
+
record.stoppedAt = new Date().toISOString();
|
|
3252
|
+
await removeLocalServiceRegistryRecord(record.serviceKey).catch(() => undefined);
|
|
3253
|
+
throw new Error(`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`);
|
|
3254
|
+
});
|
|
3255
|
+
return { record, readiness: readinessPromise };
|
|
3256
|
+
}
|
|
3257
|
+
async function startLocalRuntimeService(input) {
|
|
3258
|
+
const started = await spawnLocalRuntimeService(input);
|
|
3259
|
+
await started.readiness;
|
|
3260
|
+
return started.record;
|
|
2318
3261
|
}
|
|
2319
3262
|
function scheduleIdleStop(record) {
|
|
2320
3263
|
clearIdleTimer(record);
|
|
@@ -2533,16 +3476,10 @@ export async function ensureRuntimeServicesForRun(input) {
|
|
|
2533
3476
|
}
|
|
2534
3477
|
return refs;
|
|
2535
3478
|
}
|
|
2536
|
-
|
|
2537
|
-
const rawServices = selectRuntimeServiceEntries({
|
|
2538
|
-
config: input.config,
|
|
2539
|
-
serviceIndex: input.serviceIndex,
|
|
2540
|
-
respectDesiredStates: input.respectDesiredStates,
|
|
2541
|
-
defaultDesiredState: readDesiredRuntimeState(input.config.desiredState) ?? "stopped",
|
|
2542
|
-
serviceStates: readConfiguredServiceStates(input.config),
|
|
2543
|
-
});
|
|
3479
|
+
async function startRuntimeServicesForWorkspaceControlUnlocked(input, rawServices, invocationId, persistenceDb = input.db, registryDb = input.db, options) {
|
|
2544
3480
|
const refs = [];
|
|
2545
|
-
const
|
|
3481
|
+
const pendingReadiness = [];
|
|
3482
|
+
const startedServiceIds = [];
|
|
2546
3483
|
for (const service of rawServices) {
|
|
2547
3484
|
const { scopeType, scopeId } = resolveServiceScopeId({
|
|
2548
3485
|
service,
|
|
@@ -2572,15 +3509,13 @@ export async function startRuntimeServicesForWorkspaceControl(input) {
|
|
|
2572
3509
|
runtimeServiceId: existing.id,
|
|
2573
3510
|
lastSeenAt: existing.lastUsedAt,
|
|
2574
3511
|
});
|
|
2575
|
-
await persistRuntimeServiceRecord(
|
|
3512
|
+
await persistRuntimeServiceRecord(persistenceDb, existing);
|
|
2576
3513
|
refs.push(toRuntimeServiceRef(existing, { reused: true }));
|
|
2577
3514
|
continue;
|
|
2578
3515
|
}
|
|
2579
3516
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
const record = await startLocalRuntimeService({
|
|
2583
|
-
db: input.db,
|
|
3517
|
+
const startInput = {
|
|
3518
|
+
db: persistenceDb,
|
|
2584
3519
|
runId: invocationId,
|
|
2585
3520
|
leaseRunId: null,
|
|
2586
3521
|
startedByRunId: null,
|
|
@@ -2594,12 +3529,93 @@ export async function startRuntimeServicesForWorkspaceControl(input) {
|
|
|
2594
3529
|
reuseKey,
|
|
2595
3530
|
scopeType,
|
|
2596
3531
|
scopeId,
|
|
3532
|
+
};
|
|
3533
|
+
// Manually controlled services are not tied to a heartbeat run lifecycle, so they do not
|
|
3534
|
+
// retain a run lease and never persist a startedByRunId foreign key.
|
|
3535
|
+
const started = options?.deferReadiness
|
|
3536
|
+
? await spawnLocalRuntimeService(startInput)
|
|
3537
|
+
: {
|
|
3538
|
+
record: await startLocalRuntimeService(startInput),
|
|
3539
|
+
readiness: Promise.resolve(),
|
|
3540
|
+
};
|
|
3541
|
+
registerRuntimeService(registryDb, started.record);
|
|
3542
|
+
await persistRuntimeServiceRecord(persistenceDb, started.record);
|
|
3543
|
+
refs.push(toRuntimeServiceRef(started.record));
|
|
3544
|
+
if (options?.deferReadiness && !started.record.reused) {
|
|
3545
|
+
// Attach a rejection handler immediately; the caller awaits the same promise after
|
|
3546
|
+
// the DB transaction commits, but transaction failures may skip that wait path.
|
|
3547
|
+
started.readiness.catch(() => undefined);
|
|
3548
|
+
pendingReadiness.push(started);
|
|
3549
|
+
startedServiceIds.push(started.record.id);
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
return { refs, pendingReadiness, startedServiceIds };
|
|
3553
|
+
}
|
|
3554
|
+
export async function startRuntimeServicesForWorkspaceControl(input) {
|
|
3555
|
+
const rawServices = selectRuntimeServiceEntries({
|
|
3556
|
+
config: input.config,
|
|
3557
|
+
serviceIndex: input.serviceIndex,
|
|
3558
|
+
respectDesiredStates: input.respectDesiredStates,
|
|
3559
|
+
defaultDesiredState: readDesiredRuntimeState(input.config.desiredState) ?? "stopped",
|
|
3560
|
+
serviceStates: readConfiguredServiceStates(input.config),
|
|
3561
|
+
});
|
|
3562
|
+
const invocationId = input.invocationId ?? randomUUID();
|
|
3563
|
+
if (rawServices.length === 0 || !input.db || (!input.executionWorkspaceId && !input.workspace.workspaceId)) {
|
|
3564
|
+
const batch = await startRuntimeServicesForWorkspaceControlUnlocked(input, rawServices, invocationId);
|
|
3565
|
+
return batch.refs;
|
|
3566
|
+
}
|
|
3567
|
+
let startBatch = {
|
|
3568
|
+
refs: [],
|
|
3569
|
+
pendingReadiness: [],
|
|
3570
|
+
startedServiceIds: [],
|
|
3571
|
+
};
|
|
3572
|
+
try {
|
|
3573
|
+
await input.db.transaction(async (tx) => {
|
|
3574
|
+
const txDb = tx;
|
|
3575
|
+
if (input.executionWorkspaceId) {
|
|
3576
|
+
const [lockedExecutionWorkspace] = await tx
|
|
3577
|
+
.select({ id: executionWorkspaces.id })
|
|
3578
|
+
.from(executionWorkspaces)
|
|
3579
|
+
.where(and(eq(executionWorkspaces.id, input.executionWorkspaceId), eq(executionWorkspaces.companyId, input.actor.companyId)))
|
|
3580
|
+
.for("update");
|
|
3581
|
+
if (!lockedExecutionWorkspace)
|
|
3582
|
+
throw new Error("Execution workspace not found before starting runtime services");
|
|
3583
|
+
}
|
|
3584
|
+
if (input.workspace.workspaceId) {
|
|
3585
|
+
const [lockedProjectWorkspace] = await tx
|
|
3586
|
+
.select({ id: projectWorkspaces.id })
|
|
3587
|
+
.from(projectWorkspaces)
|
|
3588
|
+
.where(and(eq(projectWorkspaces.id, input.workspace.workspaceId), eq(projectWorkspaces.companyId, input.actor.companyId)))
|
|
3589
|
+
.for("update");
|
|
3590
|
+
if (!lockedProjectWorkspace)
|
|
3591
|
+
throw new Error("Project workspace not found before starting runtime services");
|
|
3592
|
+
}
|
|
3593
|
+
// Branch reconciliation takes these same parent row locks before mutating
|
|
3594
|
+
// a recorded branch. Persisting a `starting` service row before commit closes
|
|
3595
|
+
// the process-start window without holding the DB transaction for readiness.
|
|
3596
|
+
startBatch = await startRuntimeServicesForWorkspaceControlUnlocked({ ...input, db: txDb }, rawServices, invocationId, txDb, input.db, { deferReadiness: true });
|
|
3597
|
+
});
|
|
3598
|
+
for (const pending of startBatch.pendingReadiness) {
|
|
3599
|
+
try {
|
|
3600
|
+
await pending.readiness;
|
|
3601
|
+
await persistRuntimeServiceRecord(input.db, pending.record);
|
|
3602
|
+
}
|
|
3603
|
+
catch (error) {
|
|
3604
|
+
await persistRuntimeServiceRecord(input.db, pending.record).catch(() => undefined);
|
|
3605
|
+
throw error;
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
return startBatch.refs.map((ref) => {
|
|
3609
|
+
const record = runtimeServicesById.get(ref.id);
|
|
3610
|
+
return record ? toRuntimeServiceRef(record, { reused: ref.reused }) : ref;
|
|
2597
3611
|
});
|
|
2598
|
-
registerRuntimeService(input.db, record);
|
|
2599
|
-
await persistRuntimeServiceRecord(input.db, record);
|
|
2600
|
-
refs.push(toRuntimeServiceRef(record));
|
|
2601
3612
|
}
|
|
2602
|
-
|
|
3613
|
+
catch (error) {
|
|
3614
|
+
for (const serviceId of startBatch.startedServiceIds) {
|
|
3615
|
+
await stopRuntimeService(serviceId).catch(() => undefined);
|
|
3616
|
+
}
|
|
3617
|
+
throw error;
|
|
3618
|
+
}
|
|
2603
3619
|
}
|
|
2604
3620
|
export async function releaseRuntimeServicesForRun(runId) {
|
|
2605
3621
|
const acquired = runtimeServiceLeasesByRun.get(runId) ?? [];
|
|
@@ -3006,6 +4022,9 @@ export function buildWorkspaceReadyComment(input) {
|
|
|
3006
4022
|
if (input.workspace.worktreePath && input.workspace.worktreePath !== input.workspace.cwd) {
|
|
3007
4023
|
lines.push(`- Worktree: \`${input.workspace.worktreePath}\``);
|
|
3008
4024
|
}
|
|
4025
|
+
for (const warning of input.workspace.warnings) {
|
|
4026
|
+
lines.push(`- Warning: ${warning}`);
|
|
4027
|
+
}
|
|
3009
4028
|
for (const service of input.runtimeServices) {
|
|
3010
4029
|
const detail = service.url ? `${service.serviceName}: ${service.url}` : `${service.serviceName}: running`;
|
|
3011
4030
|
const suffix = service.reused ? " (reused)" : "";
|