dorfl 0.14.2 → 0.15.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/advance-ci-template.d.ts +17 -16
- package/dist/advance-ci-template.d.ts.map +1 -1
- package/dist/advance-ci-template.js +42 -53
- package/dist/advance-ci-template.js.map +1 -1
- package/dist/advance-drivers.d.ts +8 -0
- package/dist/advance-drivers.d.ts.map +1 -1
- package/dist/advance-drivers.js +28 -4
- package/dist/advance-drivers.js.map +1 -1
- package/dist/advance-isolated.d.ts.map +1 -1
- package/dist/advance-isolated.js +3 -2
- package/dist/advance-isolated.js.map +1 -1
- package/dist/advance-lifecycle-template.d.ts +41 -40
- package/dist/advance-lifecycle-template.d.ts.map +1 -1
- package/dist/advance-lifecycle-template.js +309 -489
- package/dist/advance-lifecycle-template.js.map +1 -1
- package/dist/advance-loop-driver.d.ts.map +1 -1
- package/dist/advance-loop-driver.js +3 -2
- package/dist/advance-loop-driver.js.map +1 -1
- package/dist/advance-treeless-publish.d.ts +31 -0
- package/dist/advance-treeless-publish.d.ts.map +1 -1
- package/dist/advance-treeless-publish.js +40 -0
- package/dist/advance-treeless-publish.js.map +1 -1
- package/dist/advance.d.ts +63 -3
- package/dist/advance.d.ts.map +1 -1
- package/dist/advance.js +80 -12
- package/dist/advance.js.map +1 -1
- package/dist/advancing-lock.d.ts +9 -0
- package/dist/advancing-lock.d.ts.map +1 -1
- package/dist/advancing-lock.js +21 -1
- package/dist/advancing-lock.js.map +1 -1
- package/dist/agent-env.d.ts +61 -0
- package/dist/agent-env.d.ts.map +1 -0
- package/dist/agent-env.js +144 -0
- package/dist/agent-env.js.map +1 -0
- package/dist/agent-stop.d.ts +35 -0
- package/dist/agent-stop.d.ts.map +1 -1
- package/dist/agent-stop.js +62 -0
- package/dist/agent-stop.js.map +1 -1
- package/dist/apply-merge-action.d.ts +76 -34
- package/dist/apply-merge-action.d.ts.map +1 -1
- package/dist/apply-merge-action.js +339 -129
- package/dist/apply-merge-action.js.map +1 -1
- package/dist/apply-stuck-action.js +2 -2
- package/dist/apply-stuck-action.js.map +1 -1
- package/dist/ci-agent-result.d.ts +151 -0
- package/dist/ci-agent-result.d.ts.map +1 -0
- package/dist/ci-agent-result.js +314 -0
- package/dist/ci-agent-result.js.map +1 -0
- package/dist/ci-handoff-apply.d.ts +162 -0
- package/dist/ci-handoff-apply.d.ts.map +1 -0
- package/dist/ci-handoff-apply.js +497 -0
- package/dist/ci-handoff-apply.js.map +1 -0
- package/dist/ci-handoff-format.d.ts +369 -0
- package/dist/ci-handoff-format.d.ts.map +1 -0
- package/dist/ci-handoff-format.js +646 -0
- package/dist/ci-handoff-format.js.map +1 -0
- package/dist/ci-handoff-lfs.d.ts +146 -0
- package/dist/ci-handoff-lfs.d.ts.map +1 -0
- package/dist/ci-handoff-lfs.js +397 -0
- package/dist/ci-handoff-lfs.js.map +1 -0
- package/dist/ci-handoff.d.ts +99 -0
- package/dist/ci-handoff.d.ts.map +1 -0
- package/dist/ci-handoff.js +277 -0
- package/dist/ci-handoff.js.map +1 -0
- package/dist/ci-lock-outputs.d.ts +93 -0
- package/dist/ci-lock-outputs.d.ts.map +1 -0
- package/dist/ci-lock-outputs.js +223 -0
- package/dist/ci-lock-outputs.js.map +1 -0
- package/dist/ci-phase-build.d.ts +178 -0
- package/dist/ci-phase-build.d.ts.map +1 -0
- package/dist/ci-phase-build.js +989 -0
- package/dist/ci-phase-build.js.map +1 -0
- package/dist/ci-phase-driver.d.ts +247 -0
- package/dist/ci-phase-driver.d.ts.map +1 -0
- package/dist/ci-phase-driver.js +370 -0
- package/dist/ci-phase-driver.js.map +1 -0
- package/dist/ci-phase-intake.d.ts +172 -0
- package/dist/ci-phase-intake.d.ts.map +1 -0
- package/dist/ci-phase-intake.js +555 -0
- package/dist/ci-phase-intake.js.map +1 -0
- package/dist/ci-phase-tasking.d.ts +205 -0
- package/dist/ci-phase-tasking.d.ts.map +1 -0
- package/dist/ci-phase-tasking.js +906 -0
- package/dist/ci-phase-tasking.js.map +1 -0
- package/dist/ci-phase-treeless.d.ts +224 -0
- package/dist/ci-phase-treeless.d.ts.map +1 -0
- package/dist/ci-phase-treeless.js +1482 -0
- package/dist/ci-phase-treeless.js.map +1 -0
- package/dist/ci-read-token.d.ts +47 -0
- package/dist/ci-read-token.d.ts.map +1 -0
- package/dist/ci-read-token.js +80 -0
- package/dist/ci-read-token.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +447 -22
- package/dist/cli.js.map +1 -1
- package/dist/close-job-template.d.ts +10 -1
- package/dist/close-job-template.d.ts.map +1 -1
- package/dist/close-job-template.js +46 -14
- package/dist/close-job-template.js.map +1 -1
- package/dist/close-job.d.ts +20 -5
- package/dist/close-job.d.ts.map +1 -1
- package/dist/close-job.js +65 -12
- package/dist/close-job.js.map +1 -1
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +20 -1
- package/dist/complete.js.map +1 -1
- package/dist/config.d.ts +41 -44
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -4
- package/dist/config.js.map +1 -1
- package/dist/continue-branch.d.ts +63 -15
- package/dist/continue-branch.d.ts.map +1 -1
- package/dist/continue-branch.js +75 -8
- package/dist/continue-branch.js.map +1 -1
- package/dist/crashed-run-save.d.ts +73 -0
- package/dist/crashed-run-save.d.ts.map +1 -0
- package/dist/crashed-run-save.js +86 -0
- package/dist/crashed-run-save.js.map +1 -0
- package/dist/cwd-section.d.ts.map +1 -1
- package/dist/cwd-section.js +10 -2
- package/dist/cwd-section.js.map +1 -1
- package/dist/decision-engine.d.ts +10 -0
- package/dist/decision-engine.d.ts.map +1 -1
- package/dist/decision-engine.js.map +1 -1
- package/dist/do-config.d.ts +2 -24
- package/dist/do-config.d.ts.map +1 -1
- package/dist/do-config.js +1 -33
- package/dist/do-config.js.map +1 -1
- package/dist/do.d.ts +149 -1
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +115 -25
- package/dist/do.js.map +1 -1
- package/dist/dorfl-item-template.d.ts +98 -0
- package/dist/dorfl-item-template.d.ts.map +1 -0
- package/dist/dorfl-item-template.js +504 -0
- package/dist/dorfl-item-template.js.map +1 -0
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +7 -5
- package/dist/env-config.js.map +1 -1
- package/dist/frontmatter.d.ts +52 -0
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +136 -2
- package/dist/frontmatter.js.map +1 -1
- package/dist/gc.d.ts +13 -0
- package/dist/gc.d.ts.map +1 -1
- package/dist/gc.js +16 -2
- package/dist/gc.js.map +1 -1
- package/dist/git.d.ts +18 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +70 -5
- package/dist/git.js.map +1 -1
- package/dist/harness.d.ts +44 -5
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +24 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/install-ci-action-pins.d.ts +123 -0
- package/dist/install-ci-action-pins.d.ts.map +1 -0
- package/dist/install-ci-action-pins.js +190 -0
- package/dist/install-ci-action-pins.js.map +1 -0
- package/dist/install-ci-actions-pr-creation.d.ts +70 -0
- package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
- package/dist/install-ci-actions-pr-creation.js +106 -0
- package/dist/install-ci-actions-pr-creation.js.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.js +26 -0
- package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
- package/dist/install-ci-core.d.ts +155 -37
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +304 -200
- package/dist/install-ci-core.js.map +1 -1
- package/dist/install-ci-github.d.ts +47 -2
- package/dist/install-ci-github.d.ts.map +1 -1
- package/dist/install-ci-github.js +113 -1
- package/dist/install-ci-github.js.map +1 -1
- package/dist/install-ci.d.ts +25 -6
- package/dist/install-ci.d.ts.map +1 -1
- package/dist/install-ci.js +72 -36
- package/dist/install-ci.js.map +1 -1
- package/dist/intake-marker.d.ts +6 -1
- package/dist/intake-marker.d.ts.map +1 -1
- package/dist/intake-marker.js.map +1 -1
- package/dist/intake-triage.d.ts.map +1 -1
- package/dist/intake-triage.js +6 -2
- package/dist/intake-triage.js.map +1 -1
- package/dist/intake-trigger-template.d.ts +31 -25
- package/dist/intake-trigger-template.d.ts.map +1 -1
- package/dist/intake-trigger-template.js +107 -292
- package/dist/intake-trigger-template.js.map +1 -1
- package/dist/intake.d.ts +186 -4
- package/dist/intake.d.ts.map +1 -1
- package/dist/intake.js +533 -416
- package/dist/intake.js.map +1 -1
- package/dist/integration-core.d.ts +154 -1
- package/dist/integration-core.d.ts.map +1 -1
- package/dist/integration-core.js +759 -486
- package/dist/integration-core.js.map +1 -1
- package/dist/integrator.d.ts +12 -0
- package/dist/integrator.d.ts.map +1 -1
- package/dist/integrator.js +21 -0
- package/dist/integrator.js.map +1 -1
- package/dist/isolation.d.ts +12 -2
- package/dist/isolation.d.ts.map +1 -1
- package/dist/isolation.js +19 -8
- package/dist/isolation.js.map +1 -1
- package/dist/item-lock.d.ts +93 -11
- package/dist/item-lock.d.ts.map +1 -1
- package/dist/item-lock.js +233 -65
- package/dist/item-lock.js.map +1 -1
- package/dist/land-question-residue.d.ts +58 -0
- package/dist/land-question-residue.d.ts.map +1 -0
- package/dist/land-question-residue.js +95 -0
- package/dist/land-question-residue.js.map +1 -0
- package/dist/lifecycle-gather.d.ts +16 -0
- package/dist/lifecycle-gather.d.ts.map +1 -1
- package/dist/lifecycle-gather.js +54 -6
- package/dist/lifecycle-gather.js.map +1 -1
- package/dist/merge-answer.d.ts +41 -0
- package/dist/merge-answer.d.ts.map +1 -0
- package/dist/merge-answer.js +60 -0
- package/dist/merge-answer.js.map +1 -0
- package/dist/merge-question-surfacer.d.ts +105 -30
- package/dist/merge-question-surfacer.d.ts.map +1 -1
- package/dist/merge-question-surfacer.js +202 -91
- package/dist/merge-question-surfacer.js.map +1 -1
- package/dist/merge-question-tick.d.ts +64 -0
- package/dist/merge-question-tick.d.ts.map +1 -0
- package/dist/merge-question-tick.js +96 -0
- package/dist/merge-question-tick.js.map +1 -0
- package/dist/mint-adr.d.ts.map +1 -1
- package/dist/mint-adr.js +20 -2
- package/dist/mint-adr.js.map +1 -1
- package/dist/needs-attention.d.ts +72 -2
- package/dist/needs-attention.d.ts.map +1 -1
- package/dist/needs-attention.js +275 -38
- package/dist/needs-attention.js.map +1 -1
- package/dist/phase-recorder.d.ts +168 -0
- package/dist/phase-recorder.d.ts.map +1 -0
- package/dist/phase-recorder.js +218 -0
- package/dist/phase-recorder.js.map +1 -0
- package/dist/phase.d.ts +94 -0
- package/dist/phase.d.ts.map +1 -0
- package/dist/phase.js +148 -0
- package/dist/phase.js.map +1 -0
- package/dist/pi-harness.d.ts.map +1 -1
- package/dist/pi-harness.js +19 -6
- package/dist/pi-harness.js.map +1 -1
- package/dist/prepare.d.ts.map +1 -1
- package/dist/prepare.js +4 -0
- package/dist/prepare.js.map +1 -1
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +48 -1
- package/dist/prompt.js.map +1 -1
- package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/protocol/WORK-CONTRACT.md +1 -1
- package/dist/reap-branches.d.ts +48 -0
- package/dist/reap-branches.d.ts.map +1 -1
- package/dist/reap-branches.js +64 -0
- package/dist/reap-branches.js.map +1 -1
- package/dist/ref-write.d.ts +198 -0
- package/dist/ref-write.d.ts.map +1 -0
- package/dist/ref-write.js +79 -0
- package/dist/ref-write.js.map +1 -0
- package/dist/repo-config.js +8 -8
- package/dist/repo-config.js.map +1 -1
- package/dist/repo-mirror.d.ts +36 -0
- package/dist/repo-mirror.d.ts.map +1 -1
- package/dist/repo-mirror.js +91 -1
- package/dist/repo-mirror.js.map +1 -1
- package/dist/review-verdict.d.ts +1 -1
- package/dist/review-verdict.js +1 -1
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +28 -4
- package/dist/run.js.map +1 -1
- package/dist/scan.d.ts +19 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +21 -8
- package/dist/scan.js.map +1 -1
- package/dist/sidecar.d.ts +14 -0
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +21 -0
- package/dist/sidecar.js.map +1 -1
- package/dist/single-job-warning.d.ts +63 -0
- package/dist/single-job-warning.d.ts.map +1 -0
- package/dist/single-job-warning.js +110 -0
- package/dist/single-job-warning.js.map +1 -0
- package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
- package/dist/spec-complete.d.ts +13 -6
- package/dist/spec-complete.d.ts.map +1 -1
- package/dist/spec-complete.js +23 -11
- package/dist/spec-complete.js.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +47 -14
- package/dist/start.js.map +1 -1
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -3
- package/dist/status.js.map +1 -1
- package/dist/tasking.d.ts +246 -1
- package/dist/tasking.d.ts.map +1 -1
- package/dist/tasking.js +452 -358
- package/dist/tasking.js.map +1 -1
- package/dist/triage-persist.d.ts.map +1 -1
- package/dist/triage-persist.js +21 -2
- package/dist/triage-persist.js.map +1 -1
- package/dist/verify-workflow-template.d.ts.map +1 -1
- package/dist/verify-workflow-template.js +8 -4
- package/dist/verify-workflow-template.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +4 -0
- package/dist/verify.js.map +1 -1
- package/dist/watch-session.d.ts +42 -13
- package/dist/watch-session.d.ts.map +1 -1
- package/dist/watch-session.js +52 -23
- package/dist/watch-session.js.map +1 -1
- package/dist/work-layout.d.ts +10 -4
- package/dist/work-layout.d.ts.map +1 -1
- package/dist/work-layout.js +11 -3
- package/dist/work-layout.js.map +1 -1
- package/dist/workspace.d.ts +65 -4
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +159 -11
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
- package/src/advance-ci-template.ts +80 -81
- package/src/advance-drivers.ts +45 -6
- package/src/advance-isolated.ts +3 -5
- package/src/advance-lifecycle-template.ts +375 -538
- package/src/advance-loop-driver.ts +3 -5
- package/src/advance-treeless-publish.ts +61 -0
- package/src/advance.ts +145 -15
- package/src/advancing-lock.ts +36 -1
- package/src/agent-env.ts +174 -0
- package/src/agent-stop.ts +81 -0
- package/src/apply-merge-action.ts +401 -154
- package/src/apply-stuck-action.ts +2 -2
- package/src/ci-agent-result.ts +421 -0
- package/src/ci-handoff-apply.ts +737 -0
- package/src/ci-handoff-format.ts +926 -0
- package/src/ci-handoff-lfs.ts +519 -0
- package/src/ci-handoff.ts +392 -0
- package/src/ci-lock-outputs.ts +273 -0
- package/src/ci-phase-build.ts +1328 -0
- package/src/ci-phase-driver.ts +548 -0
- package/src/ci-phase-intake.ts +861 -0
- package/src/ci-phase-tasking.ts +1255 -0
- package/src/ci-phase-treeless.ts +2028 -0
- package/src/ci-read-token.ts +90 -0
- package/src/cli.ts +591 -29
- package/src/close-job-template.ts +52 -15
- package/src/close-job.ts +77 -12
- package/src/complete.ts +19 -1
- package/src/config.ts +43 -48
- package/src/continue-branch.ts +121 -16
- package/src/crashed-run-save.ts +162 -0
- package/src/cwd-section.ts +10 -0
- package/src/decision-engine.ts +10 -0
- package/src/do-config.ts +2 -57
- package/src/do.ts +170 -28
- package/src/dorfl-item-template.ts +556 -0
- package/src/env-config.ts +7 -5
- package/src/frontmatter.ts +155 -2
- package/src/gc.ts +27 -2
- package/src/git.ts +83 -5
- package/src/harness.ts +58 -6
- package/src/index.ts +50 -1
- package/src/install-ci-action-pins.ts +209 -0
- package/src/install-ci-actions-pr-creation.ts +186 -0
- package/src/install-ci-capabilities/dorfl-item.ts +33 -0
- package/src/install-ci-core.ts +388 -233
- package/src/install-ci-github.ts +149 -1
- package/src/install-ci.ts +83 -37
- package/src/intake-marker.ts +6 -1
- package/src/intake-triage.ts +7 -3
- package/src/intake-trigger-template.ts +126 -327
- package/src/intake.ts +725 -544
- package/src/integration-core.ts +975 -523
- package/src/integrator.ts +28 -0
- package/src/isolation.ts +29 -10
- package/src/item-lock.ts +308 -92
- package/src/land-question-residue.ts +122 -0
- package/src/lifecycle-gather.ts +82 -10
- package/src/merge-answer.ts +82 -0
- package/src/merge-question-surfacer.ts +320 -112
- package/src/merge-question-tick.ts +182 -0
- package/src/mint-adr.ts +28 -2
- package/src/needs-attention.ts +350 -42
- package/src/phase-recorder.ts +350 -0
- package/src/phase.ts +177 -0
- package/src/pi-harness.ts +25 -5
- package/src/prepare.ts +4 -0
- package/src/prompt.ts +68 -1
- package/src/reap-branches.ts +107 -0
- package/src/ref-write.ts +324 -0
- package/src/repo-config.ts +8 -8
- package/src/repo-mirror.ts +107 -1
- package/src/review-verdict.ts +1 -1
- package/src/run.ts +47 -6
- package/src/scan.ts +37 -7
- package/src/sidecar.ts +36 -0
- package/src/single-job-warning.ts +135 -0
- package/src/spec-complete.ts +36 -14
- package/src/start.ts +55 -16
- package/src/status.ts +29 -3
- package/src/tasking.ts +593 -378
- package/src/triage-persist.ts +24 -2
- package/src/verify-workflow-template.ts +8 -4
- package/src/verify.ts +4 -0
- package/src/watch-session.ts +86 -28
- package/src/work-layout.ts +11 -3
- package/src/workspace.ts +210 -14
package/src/triage-persist.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
import {resolveSidecarIdentity, sidecarPathFor} from './sidecar.js';
|
|
11
11
|
import type {TriageAutoKind} from './triage-gate.js';
|
|
12
12
|
import {renderTaskBody, renderSpecBody} from './buildable-body.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
assertFrontmatterFields,
|
|
15
|
+
FrontmatterRenderError,
|
|
16
|
+
quoteYamlScalar,
|
|
17
|
+
setFrontmatterMarker,
|
|
18
|
+
} from './frontmatter.js';
|
|
14
19
|
import {ensureSafeSlug} from './slug-safety.js';
|
|
15
20
|
|
|
16
21
|
/**
|
|
@@ -476,6 +481,21 @@ export async function promoteObservation(
|
|
|
476
481
|
'promotedFrom',
|
|
477
482
|
item,
|
|
478
483
|
);
|
|
484
|
+
// Re-parse the RUNNER-OWNED keys before anything is written (task
|
|
485
|
+
// `intake-frontmatter-title-injection-strips-origin-stamp`). The stamp above
|
|
486
|
+
// replaces the FIRST `slug:` line, but the frontmatter parser keeps the LAST,
|
|
487
|
+
// so a drafted body that repeats `slug:` (or `promotedFrom:`) would still set
|
|
488
|
+
// the item's identity. Refuse it loudly; the observation stays for a retry.
|
|
489
|
+
try {
|
|
490
|
+
assertFrontmatterFields(content, {slug: newSlug, promotedFrom: item});
|
|
491
|
+
} catch (err) {
|
|
492
|
+
if (!(err instanceof FrontmatterRenderError)) throw err;
|
|
493
|
+
return {
|
|
494
|
+
outcome: 'usage-error',
|
|
495
|
+
exitCode: 1,
|
|
496
|
+
message: `promote ${item}: ${err.message}`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
479
499
|
|
|
480
500
|
// The note + its answered sidecar `git rm` IN THE SAME create commit (promote =
|
|
481
501
|
// ONE atomic commit). A CAS LOSER never reaches the commit, so it leaves both
|
|
@@ -620,7 +640,9 @@ function buildPromotedBody(
|
|
|
620
640
|
const hasQuestions = openQuestions.trim() !== '';
|
|
621
641
|
const frontmatter: string[] = [
|
|
622
642
|
'---',
|
|
623
|
-
|
|
643
|
+
// The title is the (sanitised, agent-draftable) slug, still written through
|
|
644
|
+
// the shared frontmatter quoter so every runner-rendered title has one shape.
|
|
645
|
+
`title: ${quoteYamlScalar(slug)}`,
|
|
624
646
|
`slug: ${slug}`,
|
|
625
647
|
`needsAnswers: ${hasQuestions ? 'true' : 'false'}`,
|
|
626
648
|
// A SPEC is a north-star doc, not a blockable task — only the task shape carries
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* trigger surface — the forward seam the spec's Applied Answers q3 names.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import {ACTION_PINS, pinnedUses} from './install-ci-action-pins.js';
|
|
24
25
|
import type {ResolvedCIConfig} from './install-ci-core.js';
|
|
25
26
|
import {shouldDropCheckoutCredentials} from './install-ci-core.js';
|
|
26
27
|
|
|
@@ -101,9 +102,9 @@ concurrency:
|
|
|
101
102
|
group: ${VERIFY_CHECK_CONTEXT}-\${{ github.ref }}
|
|
102
103
|
cancel-in-progress: true
|
|
103
104
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
# Nothing at workflow level: the job grants its own scopes. NO \`workflows\`
|
|
106
|
+
# permission (US #9).
|
|
107
|
+
permissions: {}
|
|
107
108
|
|
|
108
109
|
jobs:
|
|
109
110
|
# The job name IS the GitHub check context — it must equal VERIFY_CHECK_CONTEXT
|
|
@@ -111,8 +112,11 @@ jobs:
|
|
|
111
112
|
# context this workflow does not produce.
|
|
112
113
|
${VERIFY_CHECK_CONTEXT}:
|
|
113
114
|
runs-on: ubuntu-latest
|
|
115
|
+
# \`contents: read\` is all the gate needs.
|
|
116
|
+
permissions:
|
|
117
|
+
contents: read
|
|
114
118
|
steps:
|
|
115
|
-
- uses:
|
|
119
|
+
- uses: ${pinnedUses(ACTION_PINS.checkout)}
|
|
116
120
|
with:
|
|
117
121
|
fetch-depth: 0${checkoutCredentials}
|
|
118
122
|
- uses: ./.github/actions/dorfl-setup
|
package/src/verify.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {spawn} from 'node:child_process';
|
|
2
|
+
import {assertAgentOrRepoCodeAllowed} from './phase.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The per-repo acceptance gate, run as `dorfl verify`. It is a
|
|
@@ -149,6 +150,9 @@ export const VERIFY_OUTPUT_TAIL_LINES = 20;
|
|
|
149
150
|
export async function runVerify(
|
|
150
151
|
options: RunVerifyOptions,
|
|
151
152
|
): Promise<RunVerifyResult> {
|
|
153
|
+
// PHASE GUARD: the gate runs repository code, never in the CI lock/apply
|
|
154
|
+
// phases (their jobs hold the write token; see `phase.ts`).
|
|
155
|
+
assertAgentOrRepoCodeAllowed('verify');
|
|
152
156
|
let commands: string[];
|
|
153
157
|
try {
|
|
154
158
|
commands = resolveVerifyCommands(options.verify);
|
package/src/watch-session.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {open, type FileHandle} from 'node:fs/promises';
|
|
2
|
+
import type {CutOffTurn} from './harness.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The **`do --watch` observer** (tasks `do-watch` + `do-watch-session-log-format`)
|
|
@@ -151,6 +152,14 @@ interface SessionLogRecord {
|
|
|
151
152
|
content?: unknown;
|
|
152
153
|
/** The Anthropic-API turn-termination reason (mirrored by pi into the session log). */
|
|
153
154
|
stop_reason?: unknown;
|
|
155
|
+
/**
|
|
156
|
+
* pi's OWN normalised turn-termination reason (`stop` / `toolUse` / `length`
|
|
157
|
+
* / `error` / `aborted`), the key real pi session logs carry on every
|
|
158
|
+
* assistant message.
|
|
159
|
+
*/
|
|
160
|
+
stopReason?: unknown;
|
|
161
|
+
/** The provider's error text pi records on an `error`-stopped turn. */
|
|
162
|
+
errorMessage?: unknown;
|
|
154
163
|
/** The turn's token usage (mirrored by pi). `output` OR `output_tokens` is the produced-token count. */
|
|
155
164
|
usage?: unknown;
|
|
156
165
|
};
|
|
@@ -292,7 +301,7 @@ function assistantContentText(content: unknown): string {
|
|
|
292
301
|
* (task `harness-agent-output`) — the agent's final ANSWER, surfaced through
|
|
293
302
|
* the harness seam as `LaunchResult.output`. A thin delegate to
|
|
294
303
|
* {@link lastAssistantTurn} (one walk, returning the `.text`); the turn reader
|
|
295
|
-
* ALSO carries the `
|
|
304
|
+
* ALSO carries the stop reason + `usage` the pi adapter uses for the
|
|
296
305
|
* `LaunchResult.outputCapped` cap-truncation signal.
|
|
297
306
|
*
|
|
298
307
|
* Kept as a PURE `string → string | undefined` function on purpose: the pi
|
|
@@ -315,8 +324,9 @@ export function lastAssistantText(jsonl: string): string | undefined {
|
|
|
315
324
|
}
|
|
316
325
|
|
|
317
326
|
/**
|
|
318
|
-
* The last assistant turn's TEXT + its
|
|
319
|
-
*
|
|
327
|
+
* The last assistant turn's TEXT + its turn-termination signal (the stop
|
|
328
|
+
* reason, read by {@link readStopReason}, + the `usage.output`/`usage.output_tokens`
|
|
329
|
+
* token count). Reused by
|
|
320
330
|
* the pi adapter to populate BOTH `LaunchResult.output` (the `.text`) AND
|
|
321
331
|
* `LaunchResult.outputCapped` (the cap-truncation signal — see
|
|
322
332
|
* {@link isOutputCappedTurn}). One walk over the `.jsonl`, not two. `text` is the
|
|
@@ -328,16 +338,34 @@ export function lastAssistantText(jsonl: string): string | undefined {
|
|
|
328
338
|
export interface LastAssistantTurn {
|
|
329
339
|
/** The last assistant turn's concatenated `text` parts (its answer). */
|
|
330
340
|
text?: string;
|
|
331
|
-
/**
|
|
332
|
-
|
|
341
|
+
/**
|
|
342
|
+
* The last TEXT turn's stop reason, read by {@link readStopReason} (pi's
|
|
343
|
+
* `stopReason`: `'stop'`, `'toolUse'`, `'length'`, …; or a raw `stop_reason`
|
|
344
|
+
* such as `'end_turn'` / `'max_tokens'` for another harness shape).
|
|
345
|
+
*/
|
|
346
|
+
stopReason?: string;
|
|
333
347
|
/** The turn's produced output-token count (`usage.output` OR `usage.output_tokens`). */
|
|
334
348
|
outputTokens?: number;
|
|
349
|
+
/**
|
|
350
|
+
* The stop reason of the FINAL assistant message in the log, WHATEVER it
|
|
351
|
+
* carried (text, tool calls, or only a thinking block), read by the SAME
|
|
352
|
+
* {@link readStopReason} as {@link stopReason}. Unlike {@link stopReason}
|
|
353
|
+
* (which belongs to the last TEXT turn), this is how the run actually ENDED:
|
|
354
|
+
* a turn that spent its whole output budget thinking has no text, so only this
|
|
355
|
+
* field sees it. `undefined` when the log has no assistant message or the
|
|
356
|
+
* final one carries no stop reason. Read by {@link cutOffTurnOf}.
|
|
357
|
+
*/
|
|
358
|
+
finalStopReason?: string;
|
|
359
|
+
/** The final assistant message's `errorMessage` (pi records it on an `error` stop). */
|
|
360
|
+
finalErrorMessage?: string;
|
|
335
361
|
}
|
|
336
362
|
|
|
337
363
|
export function lastAssistantTurn(jsonl: string): LastAssistantTurn {
|
|
338
364
|
let lastText: string | undefined;
|
|
339
|
-
let lastStopReason: string |
|
|
365
|
+
let lastStopReason: string | undefined = undefined;
|
|
340
366
|
let lastOutputTokens: number | undefined = undefined;
|
|
367
|
+
let finalStopReason: string | undefined = undefined;
|
|
368
|
+
let finalErrorMessage: string | undefined = undefined;
|
|
341
369
|
for (const line of jsonl.split('\n')) {
|
|
342
370
|
const trimmed = line.trim();
|
|
343
371
|
if (trimmed === '') {
|
|
@@ -360,10 +388,17 @@ export function lastAssistantTurn(jsonl: string): LastAssistantTurn {
|
|
|
360
388
|
if (!message || message.role !== 'assistant') {
|
|
361
389
|
continue;
|
|
362
390
|
}
|
|
391
|
+
// EVERY assistant message (text or not) moves the "how did the run end"
|
|
392
|
+
// pointer: the final one wins.
|
|
393
|
+
finalStopReason = readStopReason(message);
|
|
394
|
+
finalErrorMessage =
|
|
395
|
+
typeof message.errorMessage === 'string' && message.errorMessage !== ''
|
|
396
|
+
? message.errorMessage
|
|
397
|
+
: undefined;
|
|
363
398
|
const text = assistantContentText(message.content);
|
|
364
399
|
if (text !== '') {
|
|
365
400
|
lastText = text; // a later text turn supersedes an earlier one.
|
|
366
|
-
lastStopReason = readStopReason(message
|
|
401
|
+
lastStopReason = readStopReason(message);
|
|
367
402
|
lastOutputTokens = readOutputTokens(message.usage);
|
|
368
403
|
}
|
|
369
404
|
}
|
|
@@ -371,37 +406,60 @@ export function lastAssistantTurn(jsonl: string): LastAssistantTurn {
|
|
|
371
406
|
text: lastText,
|
|
372
407
|
stopReason: lastStopReason,
|
|
373
408
|
outputTokens: lastOutputTokens,
|
|
409
|
+
finalStopReason,
|
|
410
|
+
finalErrorMessage,
|
|
374
411
|
};
|
|
375
412
|
}
|
|
376
413
|
|
|
377
414
|
/**
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
415
|
+
* Was the run's FINAL assistant turn CUT OFF rather than ended normally (task
|
|
416
|
+
* `a-truncated-agent-turn-routes-as-agent-failed`)? `length` (the per-turn
|
|
417
|
+
* output-token cap: observed with a turn whose whole 16,384-token budget went
|
|
418
|
+
* into a thinking block, no text, no tool call) and `error` (a provider error)
|
|
419
|
+
* are the two cut-off causes; every other reason (`stop`, `toolUse`, `aborted`,
|
|
420
|
+
* or none recorded) returns `undefined`. `aborted` is deliberately NOT a cut-off
|
|
421
|
+
* here: it is what a deliberate kill (the deadline stop) leaves, which has its
|
|
422
|
+
* own checkpoint route.
|
|
423
|
+
*/
|
|
424
|
+
export function cutOffTurnOf(turn: LastAssistantTurn): CutOffTurn | undefined {
|
|
425
|
+
if (turn.finalStopReason !== 'length' && turn.finalStopReason !== 'error') {
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
return turn.finalErrorMessage === undefined
|
|
429
|
+
? {cause: turn.finalStopReason}
|
|
430
|
+
: {cause: turn.finalStopReason, errorMessage: turn.finalErrorMessage};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Did this assistant turn HIT THE OUTPUT-TOKEN CAP (and nothing else)? True iff
|
|
435
|
+
* its stop reason is pi's `'length'` (or the raw API `'max_tokens'`, for a
|
|
436
|
+
* harness that mirrors the provider's own key) AND it produced a positive
|
|
437
|
+
* token count (observation `tasker-review-edits-payload-caps-the-verdict-response`:
|
|
438
|
+
* the verdict never finished). A normal end (`'stop'`, `'toolUse'`, `'end_turn'`,
|
|
439
|
+
* …), an error/abort, or NO recorded stop reason is NOT a cap: an unknown reason
|
|
440
|
+
* proves nothing, and inferring a cap from it named every pi parse failure a cap
|
|
441
|
+
* truncation (observation `output-capped-signal-reads-the-wrong-stop-reason-key`).
|
|
386
442
|
*/
|
|
387
443
|
export function isOutputCappedTurn(turn: LastAssistantTurn): boolean {
|
|
388
444
|
const cappedReason =
|
|
389
|
-
turn.stopReason ===
|
|
390
|
-
turn.stopReason === undefined ||
|
|
391
|
-
turn.stopReason === 'None' ||
|
|
392
|
-
turn.stopReason === 'max_tokens';
|
|
445
|
+
turn.stopReason === 'length' || turn.stopReason === 'max_tokens';
|
|
393
446
|
return cappedReason && (turn.outputTokens ?? 0) > 0;
|
|
394
447
|
}
|
|
395
448
|
|
|
396
|
-
/**
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
449
|
+
/**
|
|
450
|
+
* THE one stop-reason reader, shared by the output-cap signal
|
|
451
|
+
* ({@link isOutputCappedTurn}, via `stopReason`) and the cut-off signal
|
|
452
|
+
* ({@link cutOffTurnOf}, via `finalStopReason`) so the two can never disagree
|
|
453
|
+
* about a turn. Reads pi's normalised camelCase `message.stopReason` (what real
|
|
454
|
+
* pi session logs carry), falling back to a snake_case `message.stop_reason` for
|
|
455
|
+
* other harness shapes. Anything that is not a string (absent, `null`) is
|
|
456
|
+
* `undefined`: no recorded reason.
|
|
457
|
+
*/
|
|
458
|
+
function readStopReason(
|
|
459
|
+
message: NonNullable<SessionLogRecord['message']>,
|
|
460
|
+
): string | undefined {
|
|
461
|
+
const raw = message.stopReason ?? message.stop_reason;
|
|
462
|
+
return typeof raw === 'string' ? raw : undefined;
|
|
405
463
|
}
|
|
406
464
|
|
|
407
465
|
/** Read the produced output-token count from `usage.output` OR `usage.output_tokens`. */
|
package/src/work-layout.ts
CHANGED
|
@@ -217,18 +217,26 @@ export type TaskResolutionFolder =
|
|
|
217
217
|
| 'tasks-backlog';
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
|
-
* The task LIFECYCLE folders a `
|
|
221
|
-
* in (spec-complete.ts + close-job.ts `TASK_FOLDERS`):
|
|
222
|
-
* `
|
|
220
|
+
* The task LIFECYCLE folders a `spec:<slug>` task / lone-task `issue:` can reside
|
|
221
|
+
* in (spec-complete.ts + close-job.ts `TASK_FOLDERS`): EVERY folder a task body can
|
|
222
|
+
* rest in, staged to terminal: `tasks-backlog`, `tasks-ready`, `in-progress`,
|
|
223
|
+
* `done`, `cancelled`. `tasks-backlog` and `cancelled` were added by task
|
|
224
|
+
* `spec-complete-counts-staged-and-cancelled-tasks`: tasking stages new tasks in
|
|
225
|
+
* `tasks/backlog/` by default, so a scan that skipped it declared a spec complete
|
|
226
|
+
* (and the CI close-job closed its issue) while work was still staged; `cancelled`
|
|
227
|
+
* is scanned so a caller can tell "all cancelled" apart from "no tasks".
|
|
228
|
+
* `needs-attention` was dropped post-cutover
|
|
223
229
|
* (`finish-needs-attention-folder-cutover-remove-legacy-recovery-readers`, ratified
|
|
224
230
|
* in `docs/adr/needs-attention-folder-cutover-followup-nits.md`): the folder is
|
|
225
231
|
* retired — a stuck item is the per-item lock `state: stuck` with the body resting
|
|
226
232
|
* in `tasks/ready/`, so no task ever resides in `work/needs-attention/`.
|
|
227
233
|
*/
|
|
228
234
|
export const TASK_LIFECYCLE_FOLDERS = [
|
|
235
|
+
'tasks-backlog',
|
|
229
236
|
'tasks-ready',
|
|
230
237
|
'in-progress',
|
|
231
238
|
'done',
|
|
239
|
+
'cancelled',
|
|
232
240
|
] as const satisfies readonly WorkFolderKey[];
|
|
233
241
|
|
|
234
242
|
/** One of the task lifecycle folders (spec-complete.ts / close-job.ts). */
|
package/src/workspace.ts
CHANGED
|
@@ -2,11 +2,19 @@ import {
|
|
|
2
2
|
existsSync,
|
|
3
3
|
lstatSync,
|
|
4
4
|
readFileSync,
|
|
5
|
+
realpathSync,
|
|
5
6
|
rmSync,
|
|
6
7
|
writeFileSync,
|
|
7
8
|
} from 'node:fs';
|
|
8
9
|
import {basename, dirname, join} from 'node:path';
|
|
9
|
-
import {git} from './git.js';
|
|
10
|
+
import {git, run} from './git.js';
|
|
11
|
+
// Call-time use only (gc.ts imports this module too; an ESM cycle is fine for
|
|
12
|
+
// functions/constants read inside function bodies).
|
|
13
|
+
import {
|
|
14
|
+
evaluateDeletionSafety,
|
|
15
|
+
RETAIN_REASON_TEXT,
|
|
16
|
+
type RetainReason,
|
|
17
|
+
} from './gc.js';
|
|
10
18
|
import {
|
|
11
19
|
encodeRepoKey,
|
|
12
20
|
ensureMirror,
|
|
@@ -15,8 +23,9 @@ import {
|
|
|
15
23
|
import {
|
|
16
24
|
branchAheadOfArbiter,
|
|
17
25
|
rebaseContinuedBranchOntoMain,
|
|
18
|
-
|
|
26
|
+
type ContinueRebaseFailure,
|
|
19
27
|
} from './continue-branch.js';
|
|
28
|
+
import {refWrite} from './ref-write.js';
|
|
20
29
|
import {type HarnessRecord} from './harness.js';
|
|
21
30
|
import {brand} from './brand.js';
|
|
22
31
|
import {workBranchRef, type SlugNamespace} from './slug-namespace.js';
|
|
@@ -170,6 +179,14 @@ export interface CreateJobOptions {
|
|
|
170
179
|
workspacesDir: string;
|
|
171
180
|
/** The initial harness block to persist in the record (default: null adapter). */
|
|
172
181
|
harness?: HarnessRecord;
|
|
182
|
+
/**
|
|
183
|
+
* Rebase a kept branch LOCALLY only: skip the `--force-with-lease` push of the
|
|
184
|
+
* rebased tip (ADR `ci-agent-job-holds-no-write-token` decision 7). The CI
|
|
185
|
+
* agent job holds a read-only token, so its continue rebase stays local and
|
|
186
|
+
* the apply job pushes the tip, leased on the one the lock job observed
|
|
187
|
+
* (task `ci-split-answered-merge-action`). Default `false`: today's push.
|
|
188
|
+
*/
|
|
189
|
+
localContinue?: boolean;
|
|
173
190
|
env?: NodeJS.ProcessEnv;
|
|
174
191
|
}
|
|
175
192
|
|
|
@@ -201,12 +218,21 @@ export interface Job {
|
|
|
201
218
|
*/
|
|
202
219
|
continued: boolean;
|
|
203
220
|
/**
|
|
204
|
-
* True iff a CONTINUE rebase onto fresh main
|
|
205
|
-
* auto-resolved)
|
|
206
|
-
*
|
|
207
|
-
*
|
|
221
|
+
* True iff a CONTINUE rebase onto fresh main did not apply (aborted, never
|
|
222
|
+
* auto-resolved): a genuine conflict OR any other rebase failure, which
|
|
223
|
+
* {@link continueRebaseFailure} tells apart. The caller (`run`'s pipeline /
|
|
224
|
+
* `do --remote`) routes the item to needs-attention via the §10 path; the
|
|
225
|
+
* worktree is left on the un-rebased kept branch as the never-lose-work
|
|
226
|
+
* signal.
|
|
208
227
|
*/
|
|
209
228
|
continueRebaseConflict: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Why the continue rebase did not apply (set exactly when
|
|
231
|
+
* {@link continueRebaseConflict} is): the conflicting paths or git's report,
|
|
232
|
+
* and the `main` sha rebased onto. Render it with
|
|
233
|
+
* `describeContinueRebaseFailure` in the needs-attention reason.
|
|
234
|
+
*/
|
|
235
|
+
continueRebaseFailure?: ContinueRebaseFailure;
|
|
210
236
|
/**
|
|
211
237
|
* Set iff the CONTINUE reconcile push to the arbiter FAILED TERMINALLY (the
|
|
212
238
|
* stale-lease retry cap was exhausted, or a non-stale-lease rejection such as
|
|
@@ -229,6 +255,48 @@ export interface Job {
|
|
|
229
255
|
|
|
230
256
|
const DEFAULT_HARNESS: HarnessRecord = {adapter: 'null'};
|
|
231
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Carry the commit identity git resolves in the checkout `cwd` (`user.name` /
|
|
260
|
+
* `user.email`, read with `env`) into `env`'s own git config
|
|
261
|
+
* (`GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>`), so a
|
|
262
|
+
* job worktree cut from the hub mirror commits as that checkout does.
|
|
263
|
+
*
|
|
264
|
+
* WHY (task `a-failed-rebase-reports-its-real-cause`): the hub mirror is its
|
|
265
|
+
* OWN repository under `workspacesDir`, so it does not see the checkout's
|
|
266
|
+
* local config. The generated CI workflows set the identity with a plain
|
|
267
|
+
* `git config user.name` / `user.email` IN THE CHECKOUT, and a GitHub-hosted
|
|
268
|
+
* runner has no global identity, so in the CI agent job the continue rebase of
|
|
269
|
+
* an answered merge (which rewrites commits, so needs a committer) failed with
|
|
270
|
+
* git's `unable to auto-detect email address`, reported as a conflict.
|
|
271
|
+
*
|
|
272
|
+
* Returns `env` unchanged when the checkout resolves no complete identity (git
|
|
273
|
+
* then behaves as before). The value is exactly what git already uses in the
|
|
274
|
+
* checkout, so this changes nothing for a caller whose identity was global.
|
|
275
|
+
*/
|
|
276
|
+
export function withCheckoutCommitIdentity(
|
|
277
|
+
cwd: string,
|
|
278
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
279
|
+
): NodeJS.ProcessEnv | undefined {
|
|
280
|
+
const read = (key: string): string =>
|
|
281
|
+
run('git', ['config', '--get', key], cwd, {env}).stdout.trim();
|
|
282
|
+
const name = read('user.name');
|
|
283
|
+
const email = read('user.email');
|
|
284
|
+
if (name === '' || email === '') {
|
|
285
|
+
return env;
|
|
286
|
+
}
|
|
287
|
+
const base = env ?? process.env;
|
|
288
|
+
const parsed = Number(base.GIT_CONFIG_COUNT ?? '0');
|
|
289
|
+
const count = Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
290
|
+
return {
|
|
291
|
+
...base,
|
|
292
|
+
GIT_CONFIG_COUNT: String(count + 2),
|
|
293
|
+
[`GIT_CONFIG_KEY_${count}`]: 'user.name',
|
|
294
|
+
[`GIT_CONFIG_VALUE_${count}`]: name,
|
|
295
|
+
[`GIT_CONFIG_KEY_${count + 1}`]: 'user.email',
|
|
296
|
+
[`GIT_CONFIG_VALUE_${count + 1}`]: email,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
232
300
|
/**
|
|
233
301
|
* Create (or recreate) a job: ensure the hub mirror via `repo-mirror`, then
|
|
234
302
|
* `git worktree add` a per-job worktree OUTSIDE the hub at
|
|
@@ -275,7 +343,7 @@ export function createJob(options: CreateJobOptions): Job {
|
|
|
275
343
|
env,
|
|
276
344
|
});
|
|
277
345
|
let continued = false;
|
|
278
|
-
let
|
|
346
|
+
let continueRebaseFailure: ContinueRebaseFailure | undefined;
|
|
279
347
|
let continuePushFailure: string | undefined;
|
|
280
348
|
|
|
281
349
|
if (continueFromKept) {
|
|
@@ -302,9 +370,9 @@ export function createJob(options: CreateJobOptions): Job {
|
|
|
302
370
|
// NEVER --force, NEVER to main (§11). A CONFLICT → aborted (never
|
|
303
371
|
// auto-resolved) + flagged so the caller routes to needs-attention.
|
|
304
372
|
const rebase = rebaseContinuedBranchOntoMain(dir, 'main', env);
|
|
305
|
-
if (rebase.kind
|
|
306
|
-
|
|
307
|
-
} else {
|
|
373
|
+
if (rebase.kind !== 'clean') {
|
|
374
|
+
continueRebaseFailure = rebase;
|
|
375
|
+
} else if (options.localContinue !== true) {
|
|
308
376
|
// Push the rebased tip with --force-with-lease, SURVIVING a stale-lease
|
|
309
377
|
// ("stale info") rejection: re-fetch the arbiter `work/<slug>` + main,
|
|
310
378
|
// re-rebase onto current main, and retry (bounded) instead of stranding the
|
|
@@ -320,7 +388,7 @@ export function createJob(options: CreateJobOptions): Job {
|
|
|
320
388
|
// to needs-attention — the kept work stays committed + recoverable on the
|
|
321
389
|
// branch — instead of stranding it.
|
|
322
390
|
try {
|
|
323
|
-
const pushed =
|
|
391
|
+
const pushed = refWrite.pushContinuedBranch({
|
|
324
392
|
cwd: dir,
|
|
325
393
|
branch,
|
|
326
394
|
arbiter: 'origin',
|
|
@@ -328,14 +396,30 @@ export function createJob(options: CreateJobOptions): Job {
|
|
|
328
396
|
expectedRemoteTip,
|
|
329
397
|
env,
|
|
330
398
|
});
|
|
331
|
-
if (pushed.kind
|
|
332
|
-
|
|
399
|
+
if (pushed.kind !== 'pushed') {
|
|
400
|
+
continueRebaseFailure = pushed;
|
|
333
401
|
}
|
|
334
402
|
} catch (err) {
|
|
335
403
|
continuePushFailure = err instanceof Error ? err.message : String(err);
|
|
336
404
|
}
|
|
337
405
|
}
|
|
338
406
|
} else {
|
|
407
|
+
// NEVER-LOSE-WORK guard (task
|
|
408
|
+
// `a-fresh-claim-does-not-discard-a-retained-worktrees-unsaved-work`): a
|
|
409
|
+
// RETAINED worktree at this work-id (a crashed run whose lock was released
|
|
410
|
+
// some way other than `requeue`, e.g. `release-lock`) may hold work the
|
|
411
|
+
// arbiter lacks. Apply the SAME deletion-safety predicate `gc` and the
|
|
412
|
+
// requeue save use; if it does not hold, REFUSE loudly (throw) and leave the
|
|
413
|
+
// worktree untouched instead of clearing it below.
|
|
414
|
+
refuseToClearUnsavedWorktree({
|
|
415
|
+
dir,
|
|
416
|
+
branch,
|
|
417
|
+
slug,
|
|
418
|
+
type: options.type ?? 'task',
|
|
419
|
+
mirrorPath: mirror.path,
|
|
420
|
+
env,
|
|
421
|
+
});
|
|
422
|
+
|
|
339
423
|
// Clear any stale registration for this work-id (idempotent re-create): a
|
|
340
424
|
// leftover worktree dir or branch from a prior crashed run would block the
|
|
341
425
|
// `worktree add`. We use git's own removal, never a bare rm -rf (ADR §4).
|
|
@@ -366,7 +450,8 @@ export function createJob(options: CreateJobOptions): Job {
|
|
|
366
450
|
record,
|
|
367
451
|
mirror,
|
|
368
452
|
continued,
|
|
369
|
-
continueRebaseConflict,
|
|
453
|
+
continueRebaseConflict: continueRebaseFailure !== undefined,
|
|
454
|
+
...(continueRebaseFailure === undefined ? {} : {continueRebaseFailure}),
|
|
370
455
|
continuePushFailure,
|
|
371
456
|
dispose() {
|
|
372
457
|
git(['worktree', 'remove', '--force', dir], mirror.path, {env});
|
|
@@ -376,6 +461,117 @@ export function createJob(options: CreateJobOptions): Job {
|
|
|
376
461
|
};
|
|
377
462
|
}
|
|
378
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Thrown by {@link createJob} when its FRESH-cut path finds a RETAINED job
|
|
466
|
+
* worktree at the work-id that holds work the arbiter lacks (the gc
|
|
467
|
+
* deletion-safety predicate, {@link evaluateDeletionSafety}, does not hold).
|
|
468
|
+
* Clearing it would silently discard that work, so the claim is refused and the
|
|
469
|
+
* worktree is left untouched. The message names the path and the recovery.
|
|
470
|
+
*
|
|
471
|
+
* A caller that best-effort reaps a leaked worktree after `createJob` threw
|
|
472
|
+
* (`do --isolated`'s `reapPreparedWorktreeLeak`) MUST skip that reap for this
|
|
473
|
+
* error: the worktree is not a leak, it is the work being protected.
|
|
474
|
+
*/
|
|
475
|
+
export class RetainedWorktreeUnsavedWorkError extends Error {
|
|
476
|
+
readonly dir: string;
|
|
477
|
+
/** The branch the retained worktree is on (its HEAD branch, else the job branch). */
|
|
478
|
+
readonly retainedBranch: string;
|
|
479
|
+
readonly reason: RetainReason;
|
|
480
|
+
constructor(input: {
|
|
481
|
+
message: string;
|
|
482
|
+
dir: string;
|
|
483
|
+
retainedBranch: string;
|
|
484
|
+
reason: RetainReason;
|
|
485
|
+
}) {
|
|
486
|
+
super(input.message);
|
|
487
|
+
this.name = 'RetainedWorktreeUnsavedWorkError';
|
|
488
|
+
this.dir = input.dir;
|
|
489
|
+
this.retainedBranch = input.retainedBranch;
|
|
490
|
+
this.reason = input.reason;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The claim-time never-lose-work guard: if `dir` is a real git working tree (a
|
|
496
|
+
* retained job worktree, not an orphan dir / dangling symlink the self-heal in
|
|
497
|
+
* {@link forceClearWorktreePath} handles) and the shared deletion-safety
|
|
498
|
+
* predicate does NOT hold for it (dirty tree, or a tip the arbiter lacks),
|
|
499
|
+
* throw {@link RetainedWorktreeUnsavedWorkError}. A worktree that is provably
|
|
500
|
+
* safe (clean AND merged/pushed) passes, and the caller clears it as before.
|
|
501
|
+
*/
|
|
502
|
+
function refuseToClearUnsavedWorktree(input: {
|
|
503
|
+
dir: string;
|
|
504
|
+
branch: string;
|
|
505
|
+
slug: string;
|
|
506
|
+
type: SlugNamespace;
|
|
507
|
+
mirrorPath: string;
|
|
508
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
509
|
+
}): void {
|
|
510
|
+
const {dir, branch, slug, type, mirrorPath, env} = input;
|
|
511
|
+
if (!isGitWorkingTreeRoot(dir, env)) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Judge the worktree by the branch it is ACTUALLY on (a same-slug run of the
|
|
515
|
+
// other type shares this work-id), falling back to the job branch when HEAD
|
|
516
|
+
// is detached (e.g. a run killed mid-rebase).
|
|
517
|
+
const head = run('git', ['symbolic-ref', '-q', '--short', 'HEAD'], dir, {
|
|
518
|
+
env,
|
|
519
|
+
});
|
|
520
|
+
const retainedBranch =
|
|
521
|
+
head.status === 0 && head.stdout.trim() !== ''
|
|
522
|
+
? head.stdout.trim()
|
|
523
|
+
: branch;
|
|
524
|
+
const verdict = evaluateDeletionSafety({
|
|
525
|
+
dir,
|
|
526
|
+
branch: retainedBranch,
|
|
527
|
+
env,
|
|
528
|
+
});
|
|
529
|
+
if (verdict.safe) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const reason = verdict.reason ?? 'unmerged-commits';
|
|
533
|
+
const save =
|
|
534
|
+
type === 'task' && retainedBranch === branch
|
|
535
|
+
? `Save it with \`dorfl requeue ${slug}\` (with the lock held, it commits ` +
|
|
536
|
+
`any residue as a wip commit and pushes ${branch}, so the next claim ` +
|
|
537
|
+
`continues from it), or push ${retainedBranch} from ${dir} yourself.`
|
|
538
|
+
: `Save it by pushing ${retainedBranch} from ${dir}, then release the lock.`;
|
|
539
|
+
throw new RetainedWorktreeUnsavedWorkError({
|
|
540
|
+
dir,
|
|
541
|
+
retainedBranch,
|
|
542
|
+
reason,
|
|
543
|
+
message:
|
|
544
|
+
`refusing to cut a fresh ${branch} for '${slug}': the retained job ` +
|
|
545
|
+
`worktree ${dir} (on ${retainedBranch}) holds work the arbiter lacks ` +
|
|
546
|
+
`(${RETAIN_REASON_TEXT[reason]}), and clearing it would discard that ` +
|
|
547
|
+
`work. The worktree is left untouched. ${save} To discard it on purpose, ` +
|
|
548
|
+
`run \`git -C ${mirrorPath} worktree remove --force ${dir}\` and claim again.`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* True iff `dir` is itself the ROOT of a git working tree (a registered job
|
|
554
|
+
* worktree). An orphan directory git does not manage, or a dangling symlink,
|
|
555
|
+
* is not (it holds no git-tracked work, and the self-heal removes it).
|
|
556
|
+
*/
|
|
557
|
+
function isGitWorkingTreeRoot(
|
|
558
|
+
dir: string,
|
|
559
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
560
|
+
): boolean {
|
|
561
|
+
if (!existsSync(dir)) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const top = run('git', ['rev-parse', '--show-toplevel'], dir, {env});
|
|
565
|
+
if (top.status !== 0) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
return realpathSync(top.stdout.trim()) === realpathSync(dir);
|
|
570
|
+
} catch {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
379
575
|
/**
|
|
380
576
|
* Write (or overwrite) a job's record to its SIBLING path ({@link jobRecordPath},
|
|
381
577
|
* `<work-id>.json` next to the worktree), OUTSIDE the checked-out tree. `dir` is
|