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/repo-mirror.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {existsSync, mkdirSync} from 'node:fs';
|
|
2
|
-
import {dirname, join} from 'node:path';
|
|
2
|
+
import {dirname, isAbsolute, join} from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
3
4
|
import {git, run} from './git.js';
|
|
4
5
|
import type {Config, PartialConfig} from './config.js';
|
|
5
6
|
import type {ConfigOverrideMap} from './config-override.js';
|
|
@@ -35,6 +36,27 @@ export function readOriginUrl(
|
|
|
35
36
|
return url === '' ? undefined : url;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Is `mirrorPath` inside a `--reconcile-locks` write scope? `undefined` scope
|
|
41
|
+
* means every mirror (the explicit `--all-arbiters`); otherwise only the mirror
|
|
42
|
+
* whose `origin` URL hub-keys to `arbiterKey` (the SAME keying `gc` uses for its
|
|
43
|
+
* per-job arbiter scope). A mirror whose `origin` is unreadable is OUT of scope:
|
|
44
|
+
* the safe direction for a write. Shared by `status --reconcile-locks` (task
|
|
45
|
+
* `reconcile-locks-stays-within-the-current-arbiter`) and `scan
|
|
46
|
+
* --reconcile-locks` (task `status-no-arbiter-is-honoured`).
|
|
47
|
+
*/
|
|
48
|
+
export function mirrorInReconcileScope(
|
|
49
|
+
mirrorPath: string,
|
|
50
|
+
arbiterKey: string | undefined,
|
|
51
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
52
|
+
): boolean {
|
|
53
|
+
if (arbiterKey === undefined) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const url = readOriginUrl(mirrorPath, env);
|
|
57
|
+
return url !== undefined && encodeRepoKey(url) === arbiterKey;
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
/**
|
|
39
61
|
* The shared **hub-mirror primitive**: one bare mirror per repo under
|
|
40
62
|
* `<workspacesDir>/repos/<key>.git`, re-fetched from the arbiter. Both the
|
|
@@ -409,3 +431,87 @@ export function fetchMirrorMainOrWarn(options: {
|
|
|
409
431
|
return false;
|
|
410
432
|
}
|
|
411
433
|
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The mirror's `origin` URL when it names a LOCAL arbiter (a `file://` URL or an
|
|
437
|
+
* absolute path) that no longer exists on disk; `undefined` otherwise (a live
|
|
438
|
+
* local arbiter, a network remote, or an unreadable origin). Read-only.
|
|
439
|
+
*/
|
|
440
|
+
export function missingLocalOrigin(
|
|
441
|
+
mirrorPath: string,
|
|
442
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
443
|
+
): string | undefined {
|
|
444
|
+
const url = readOriginUrl(mirrorPath, env);
|
|
445
|
+
if (url === undefined) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
let localPath: string | undefined;
|
|
449
|
+
if (url.startsWith('file://')) {
|
|
450
|
+
try {
|
|
451
|
+
localPath = fileURLToPath(url);
|
|
452
|
+
} catch {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
} else if (isAbsolute(url)) {
|
|
456
|
+
localPath = url;
|
|
457
|
+
}
|
|
458
|
+
return localPath !== undefined && !existsSync(localPath) ? url : undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** How many dead mirrors the summary line names before "and N more". */
|
|
462
|
+
const DEAD_MIRROR_EXAMPLES = 3;
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The fetch-first refresh `status` and `scan` run over every registered mirror,
|
|
466
|
+
* with DEAD LOCAL ARBITERS summarised on ONE line instead of one warning each.
|
|
467
|
+
*
|
|
468
|
+
* A mirror whose origin is a local path that no longer exists (typically a
|
|
469
|
+
* throwaway `/tmp` arbiter, or a disk that is not mounted) cannot be fetched, and
|
|
470
|
+
* warning once per mirror buried the real output under hundreds of identical
|
|
471
|
+
* git errors. Such a mirror is NOT fetched and NOT removed: it is still read
|
|
472
|
+
* from its last-known state exactly like an offline mirror, because a read-only
|
|
473
|
+
* command must never delete state (an unmounted disk comes back). `flush()`
|
|
474
|
+
* then emits a single line with the count, a few examples and the removal
|
|
475
|
+
* command. Every other fetch failure keeps its own warning, as before.
|
|
476
|
+
*/
|
|
477
|
+
export function createMirrorRefresher(options: {
|
|
478
|
+
warn?: (message: string) => void;
|
|
479
|
+
env?: NodeJS.ProcessEnv;
|
|
480
|
+
}): {refresh(mirrorPath: string): void; flush(): void} {
|
|
481
|
+
const dead: {mirrorPath: string; url: string}[] = [];
|
|
482
|
+
return {
|
|
483
|
+
refresh(mirrorPath: string): void {
|
|
484
|
+
const url = missingLocalOrigin(mirrorPath, options.env);
|
|
485
|
+
if (url !== undefined) {
|
|
486
|
+
dead.push({mirrorPath, url});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
fetchMirrorMainOrWarn({
|
|
490
|
+
mirrorPath,
|
|
491
|
+
warn: options.warn,
|
|
492
|
+
env: options.env,
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
flush(): void {
|
|
496
|
+
if (dead.length === 0) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const shown = dead
|
|
500
|
+
.slice(0, DEAD_MIRROR_EXAMPLES)
|
|
501
|
+
.map((d) => d.url)
|
|
502
|
+
.join(', ');
|
|
503
|
+
const more =
|
|
504
|
+
dead.length > DEAD_MIRROR_EXAMPLES
|
|
505
|
+
? ` and ${dead.length - DEAD_MIRROR_EXAMPLES} more`
|
|
506
|
+
: '';
|
|
507
|
+
const noun =
|
|
508
|
+
dead.length === 1
|
|
509
|
+
? '1 registered mirror points at a local arbiter that no longer exists'
|
|
510
|
+
: `${dead.length} registered mirrors point at a local arbiter that no longer exists`;
|
|
511
|
+
options.warn?.(
|
|
512
|
+
`${noun} (${shown}${more}); skipped the fetch and read their last-known ` +
|
|
513
|
+
'state. Remove each with `dorfl remote rm <origin-url>` if it is gone for good.',
|
|
514
|
+
);
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
}
|
package/src/review-verdict.ts
CHANGED
|
@@ -124,7 +124,7 @@ export class ReviewParseError extends Error {}
|
|
|
124
124
|
* shared the capped response with the verdict) rather than chasing a model flake.
|
|
125
125
|
*
|
|
126
126
|
* Detected at the harness seam: the adapter surfaces the last assistant turn's
|
|
127
|
-
*
|
|
127
|
+
* stop reason (pi's `length`, or a raw `max_tokens`: the turn hit the output cap) and
|
|
128
128
|
* its `usage.output` token count; the gate throws this when a parse fails AND that
|
|
129
129
|
* cap signal is present. When the adapter CANNOT see the signal (the null/shell
|
|
130
130
|
* adapter, or a future adapter without usage telemetry), the parse still fails as a
|
package/src/run.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {continueRebaseSentence} from './continue-branch.js';
|
|
1
2
|
import {type Config, resolvePromptGuidance} from './config.js';
|
|
2
3
|
import {resolveRepoConfig} from './repo-config.js';
|
|
3
4
|
import type {ConfigOverrideMap} from './config-override.js';
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
type IsolationStrategy,
|
|
17
18
|
} from './isolation.js';
|
|
18
19
|
import {runConcurrent, createKeyedLock} from './concurrency.js';
|
|
19
|
-
import type {Harness} from './harness.js';
|
|
20
|
+
import type {CutOffTurn, Harness} from './harness.js';
|
|
20
21
|
import {createHarness} from './pi-harness.js';
|
|
21
22
|
import {generateSessionPath} from './session-path.js';
|
|
22
23
|
import {
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
isWorkBranchDiffEmpty,
|
|
33
34
|
emptyDiffStopReason,
|
|
34
35
|
emptyDiffDisposeEnvelope,
|
|
36
|
+
resolveCutOffTurnFailure,
|
|
35
37
|
type AgentStopKind,
|
|
36
38
|
} from './agent-stop.js';
|
|
37
39
|
import {surfaceStuckToNeedsAttention} from './needs-attention.js';
|
|
@@ -233,6 +235,12 @@ export type Dorfl = (input: {
|
|
|
233
235
|
* provider degrades to `--fill` (no regression).
|
|
234
236
|
*/
|
|
235
237
|
output?: string;
|
|
238
|
+
/**
|
|
239
|
+
* The agent's final model turn was CUT OFF (`length` / `error` stop; the
|
|
240
|
+
* harness seam's `LaunchResult.cutOffTurn`, mirrors `DoDorfl.cutOffTurn`). An
|
|
241
|
+
* empty diff with this set routes `agent-failed`, never the empty-diff STOP.
|
|
242
|
+
*/
|
|
243
|
+
cutOffTurn?: CutOffTurn;
|
|
236
244
|
};
|
|
237
245
|
|
|
238
246
|
export interface RunOnceOptions {
|
|
@@ -725,8 +733,9 @@ async function runOneItem(
|
|
|
725
733
|
// predicate still HOLDS and this worktree is reaped — more §14-aligned.
|
|
726
734
|
if (tree.continueRebaseConflict) {
|
|
727
735
|
const reason =
|
|
728
|
-
`continuing the kept ${tree.branch}:
|
|
729
|
-
|
|
736
|
+
`continuing the kept ${tree.branch}: ` +
|
|
737
|
+
`${continueRebaseSentence(tree.branch, tree.continueRebaseFailure)} — ` +
|
|
738
|
+
'run `requeue --reconcile` ' +
|
|
730
739
|
'to non-destructively re-sync the mirror and retry the rebase (keeps ' +
|
|
731
740
|
'the work). Last resort: `requeue --reset` DESTRUCTIVELY discards the ' +
|
|
732
741
|
'branch and starts fresh.';
|
|
@@ -824,6 +833,7 @@ async function runOneItem(
|
|
|
824
833
|
cwd: tree.dir,
|
|
825
834
|
continueContext,
|
|
826
835
|
promptGuidance: itemGuidance,
|
|
836
|
+
taskFolder: task.folder,
|
|
827
837
|
});
|
|
828
838
|
} catch (err) {
|
|
829
839
|
if (err instanceof PromptError) {
|
|
@@ -837,7 +847,12 @@ async function runOneItem(
|
|
|
837
847
|
// resolved per-repo `buildModel` (ADR §13, which inherits the general
|
|
838
848
|
// `model` when unset) flows through the seam to the adapter; a
|
|
839
849
|
// `{model}`-in-agentCmd misconfiguration surfaces as agent-failed.
|
|
840
|
-
let agent: {
|
|
850
|
+
let agent: {
|
|
851
|
+
ok: boolean;
|
|
852
|
+
detail?: string;
|
|
853
|
+
output?: string;
|
|
854
|
+
cutOffTurn?: CutOffTurn;
|
|
855
|
+
};
|
|
841
856
|
try {
|
|
842
857
|
agent = runAgent(
|
|
843
858
|
ctx,
|
|
@@ -874,6 +889,21 @@ async function runOneItem(
|
|
|
874
889
|
// catches a stop without one. Either routes to needs-attention (surfaced on
|
|
875
890
|
// the arbiter) and SKIPS the gate + Gate-2 (the whole `performIntegration`
|
|
876
891
|
// band) — a clean STOP is NOT "a build that changed nothing".
|
|
892
|
+
// A CUT-OFF final turn (`length`/`error`) with an empty diff is a harness
|
|
893
|
+
// failure, not a STOP: route it `agent-failed` before the empty-diff
|
|
894
|
+
// backstop (task `a-truncated-agent-turn-routes-as-agent-failed`, the
|
|
895
|
+
// same guard `do` runs).
|
|
896
|
+
const cutOffDetail = await resolveCutOffTurnFailure({
|
|
897
|
+
cutOffTurn: agent.cutOffTurn,
|
|
898
|
+
output: agent.output,
|
|
899
|
+
slug,
|
|
900
|
+
cwd: tree.dir,
|
|
901
|
+
arbiter: tree.arbiterRemote,
|
|
902
|
+
env: gitEnv,
|
|
903
|
+
});
|
|
904
|
+
if (cutOffDetail !== undefined) {
|
|
905
|
+
return await saveAgentFailure(base, tree, slug, cutOffDetail, ctx);
|
|
906
|
+
}
|
|
877
907
|
const sentinel = parseStopSentinel(agent.output);
|
|
878
908
|
const stopReason: {kind: AgentStopKind; reason: string} | undefined =
|
|
879
909
|
sentinel !== undefined
|
|
@@ -1119,7 +1149,12 @@ function runAgent(
|
|
|
1119
1149
|
agentCmd: string,
|
|
1120
1150
|
model: string | undefined,
|
|
1121
1151
|
sessionsDir: string | undefined,
|
|
1122
|
-
): {
|
|
1152
|
+
): {
|
|
1153
|
+
ok: boolean;
|
|
1154
|
+
detail?: string;
|
|
1155
|
+
output?: string;
|
|
1156
|
+
cutOffTurn?: CutOffTurn;
|
|
1157
|
+
} {
|
|
1123
1158
|
if (ctx.dorfl) {
|
|
1124
1159
|
return ctx.dorfl({cwd: tree.dir, prompt, slug, env: ctx.env});
|
|
1125
1160
|
}
|
|
@@ -1148,7 +1183,13 @@ function runAgent(
|
|
|
1148
1183
|
// Surface the agent's FINAL SUMMARY (`LaunchResult.output`) — the source channel
|
|
1149
1184
|
// for the propose-mode PR body — instead of dropping it (mirrors `do`'s
|
|
1150
1185
|
// `runDoAgent`). Absent (no parseable assistant text) ⇒ undefined ⇒ `--fill`.
|
|
1151
|
-
return {
|
|
1186
|
+
return {
|
|
1187
|
+
ok: launched.ok,
|
|
1188
|
+
detail: launched.detail,
|
|
1189
|
+
output: launched.output,
|
|
1190
|
+
// How the final model turn ended (the cut-off guard before the STOP route).
|
|
1191
|
+
cutOffTurn: launched.cutOffTurn,
|
|
1192
|
+
};
|
|
1152
1193
|
}
|
|
1153
1194
|
|
|
1154
1195
|
/**
|
package/src/scan.ts
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
} from './ledger-read.js';
|
|
10
10
|
import {listMirrors} from './registry.js';
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
createMirrorRefresher,
|
|
13
|
+
mirrorInReconcileScope,
|
|
13
14
|
resolveRepoConfigFromMirror,
|
|
14
15
|
} from './repo-mirror.js';
|
|
15
16
|
import {resolveRepoConfig} from './repo-config.js';
|
|
@@ -425,6 +426,16 @@ export async function scan(
|
|
|
425
426
|
* claim path sweeps on every unit of work.
|
|
426
427
|
*/
|
|
427
428
|
reconcileLocks?: boolean;
|
|
429
|
+
/**
|
|
430
|
+
* ARBITER SCOPE of the {@link reconcileLocks} write (task
|
|
431
|
+
* `status-no-arbiter-is-honoured`, the same scoping `status --reconcile-locks`
|
|
432
|
+
* has). When set, ONLY the registered mirror whose `origin` hub-keys to this
|
|
433
|
+
* key has its stale locks RELEASED; every other mirror is merely classified,
|
|
434
|
+
* exactly as without `reconcileLocks`. `undefined` reconciles EVERY mirror:
|
|
435
|
+
* the CLI leaves it undefined only behind the explicit `--all-arbiters`.
|
|
436
|
+
* Ignored when `reconcileLocks` is not `true`.
|
|
437
|
+
*/
|
|
438
|
+
reconcileArbiterKey?: string;
|
|
428
439
|
env?: NodeJS.ProcessEnv;
|
|
429
440
|
/**
|
|
430
441
|
* The per-machine {@link ConfigOverrideMap} (from `loadConfigOverride`),
|
|
@@ -444,15 +455,17 @@ export async function scan(
|
|
|
444
455
|
const repos: RepoReport[] = [];
|
|
445
456
|
const counts = {totalItems: 0, totalEligible: 0};
|
|
446
457
|
|
|
458
|
+
// Dead LOCAL arbiters are summarised on one line after the loop (see
|
|
459
|
+
// `createMirrorRefresher`), not warned once per mirror.
|
|
460
|
+
const refresher = createMirrorRefresher({
|
|
461
|
+
warn: options.warn,
|
|
462
|
+
env: options.env,
|
|
463
|
+
});
|
|
447
464
|
for (const mirror of mirrors) {
|
|
448
465
|
// Fetch-first (ADR §5/§6): refresh this mirror's `main` so the read below
|
|
449
466
|
// sees the remote truth. Never fatal — a failed fetch WARNS and falls back to
|
|
450
467
|
// the mirror's last-known `main` (the read strategy is unchanged).
|
|
451
|
-
|
|
452
|
-
mirrorPath: mirror.path,
|
|
453
|
-
warn: options.warn,
|
|
454
|
-
env: options.env,
|
|
455
|
-
});
|
|
468
|
+
refresher.refresh(mirror.path);
|
|
456
469
|
// Read the full `work/` lifecycle from the mirror's bare `main` ref through
|
|
457
470
|
// the read seam (git ls-tree/show; NOT a working-tree read).
|
|
458
471
|
const state = await ledgerRead.resolveMirrorState({
|
|
@@ -494,7 +507,14 @@ export async function scan(
|
|
|
494
507
|
const MIRROR_MAIN = {mainRef: 'main'};
|
|
495
508
|
let staleLockEntries: string[];
|
|
496
509
|
let lockHeld: LockEntry[];
|
|
497
|
-
if (
|
|
510
|
+
if (
|
|
511
|
+
options.reconcileLocks === true &&
|
|
512
|
+
mirrorInReconcileScope(
|
|
513
|
+
mirror.path,
|
|
514
|
+
options.reconcileArbiterKey,
|
|
515
|
+
options.env,
|
|
516
|
+
)
|
|
517
|
+
) {
|
|
498
518
|
const swept = await reconcileTerminalItemLocks(
|
|
499
519
|
mirror.path,
|
|
500
520
|
'origin',
|
|
@@ -612,6 +632,7 @@ export async function scan(
|
|
|
612
632
|
staleLocks: staleLockEntries,
|
|
613
633
|
});
|
|
614
634
|
}
|
|
635
|
+
refresher.flush();
|
|
615
636
|
|
|
616
637
|
return {
|
|
617
638
|
repos,
|
|
@@ -668,6 +689,14 @@ export function scanRepoPaths(
|
|
|
668
689
|
* handle); DEFAULTS empty.
|
|
669
690
|
*/
|
|
670
691
|
heldSpecSlugs: Set<string> = new Set(),
|
|
692
|
+
/**
|
|
693
|
+
* The held TASK slugs whose lock a finished propose build keeps (the
|
|
694
|
+
* `propose-pr` marker): passed to {@link gatherLifecycleInPlace}, which lets
|
|
695
|
+
* such a slug through the lifecycle subtraction when its merge question is
|
|
696
|
+
* answered `merge` (task `wire-merge-questions-into-the-advance-tick`,
|
|
697
|
+
* decision 6). The build pool is unaffected. DEFAULTS empty.
|
|
698
|
+
*/
|
|
699
|
+
proposeKeptSlugs: Set<string> = new Set(),
|
|
671
700
|
): ScanReport {
|
|
672
701
|
const repos: RepoReport[] = [];
|
|
673
702
|
const counts = {totalItems: 0, totalEligible: 0};
|
|
@@ -705,6 +734,7 @@ export function scanRepoPaths(
|
|
|
705
734
|
// symmetric to the `scoreItems` subtraction above, so a held task never
|
|
706
735
|
// leaks into a surface/apply leg either.
|
|
707
736
|
heldSlugs,
|
|
737
|
+
proposeKeptSlugs,
|
|
708
738
|
}),
|
|
709
739
|
);
|
|
710
740
|
// The one-slug-one-folder LINT over THIS working tree's `work/` ledger.
|
package/src/sidecar.ts
CHANGED
|
@@ -144,8 +144,23 @@ export interface SidecarEntry {
|
|
|
144
144
|
* kind-based SUBFOLDERS.
|
|
145
145
|
*/
|
|
146
146
|
kind?: SidecarKind;
|
|
147
|
+
/**
|
|
148
|
+
* The arbiter `main` commit this question was ASKED AGAINST (a full hex
|
|
149
|
+
* sha), set on a `kind: merge` entry by the merge-question surfacer and by
|
|
150
|
+
* the `strictMergeApproval` re-surface path (task
|
|
151
|
+
* `strict-merge-approval-restale-check-runs-before-the-continue-rebase`).
|
|
152
|
+
* With `strictMergeApproval` on, an answered merge re-surfaces when `main`'s
|
|
153
|
+
* code moved since this commit. Absent on every other entry and on a merge
|
|
154
|
+
* entry written before the field existed (which then never re-stales).
|
|
155
|
+
* Carried in the per-entry HTML comment as `askedAtMain=<sha>`; a malformed
|
|
156
|
+
* value reads as absent.
|
|
157
|
+
*/
|
|
158
|
+
askedAtMain?: string;
|
|
147
159
|
}
|
|
148
160
|
|
|
161
|
+
/** A full git object name: 40 hex (SHA-1) or 64 hex (SHA-256). */
|
|
162
|
+
const FULL_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
163
|
+
|
|
149
164
|
/** The parsed sidecar: identity frontmatter + ordered entries. */
|
|
150
165
|
export interface SidecarModel {
|
|
151
166
|
/** The NAMESPACED identity (`spec:autotask`, `task:foo`, `observation:bar`). */
|
|
@@ -434,6 +449,7 @@ interface EntryFields {
|
|
|
434
449
|
id?: string;
|
|
435
450
|
answered?: boolean;
|
|
436
451
|
kind?: SidecarKind;
|
|
452
|
+
askedAtMain?: string;
|
|
437
453
|
}
|
|
438
454
|
|
|
439
455
|
/** Parse a per-entry `<!-- qN fields: key=val … -->` HTML comment. */
|
|
@@ -467,6 +483,11 @@ function parseEntryComment(line: string): EntryFields | undefined {
|
|
|
467
483
|
if (SIDECAR_KINDS.has(value as SidecarKind)) {
|
|
468
484
|
out.kind = value as SidecarKind;
|
|
469
485
|
}
|
|
486
|
+
} else if (key === 'askedAtMain') {
|
|
487
|
+
// Silent-on-malformed, as for `kind`: only a full sha is kept.
|
|
488
|
+
if (FULL_SHA.test(value)) {
|
|
489
|
+
out.askedAtMain = value;
|
|
490
|
+
}
|
|
470
491
|
}
|
|
471
492
|
}
|
|
472
493
|
return out;
|
|
@@ -481,6 +502,7 @@ function parseEntrySection(lines: string[]): SidecarEntry {
|
|
|
481
502
|
let defaultVal: string | undefined;
|
|
482
503
|
let answeredOverride: boolean | undefined;
|
|
483
504
|
let kind: SidecarKind | undefined;
|
|
505
|
+
let askedAtMain: string | undefined;
|
|
484
506
|
let answerStart = -1;
|
|
485
507
|
|
|
486
508
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -500,6 +522,9 @@ function parseEntrySection(lines: string[]): SidecarEntry {
|
|
|
500
522
|
if (entryFields.kind !== undefined) {
|
|
501
523
|
kind = entryFields.kind;
|
|
502
524
|
}
|
|
525
|
+
if (entryFields.askedAtMain !== undefined) {
|
|
526
|
+
askedAtMain = entryFields.askedAtMain;
|
|
527
|
+
}
|
|
503
528
|
inBlockquote = false;
|
|
504
529
|
continue;
|
|
505
530
|
}
|
|
@@ -568,6 +593,9 @@ function parseEntrySection(lines: string[]): SidecarEntry {
|
|
|
568
593
|
if (kind !== undefined) {
|
|
569
594
|
entry.kind = kind;
|
|
570
595
|
}
|
|
596
|
+
if (askedAtMain !== undefined) {
|
|
597
|
+
entry.askedAtMain = askedAtMain;
|
|
598
|
+
}
|
|
571
599
|
return entry;
|
|
572
600
|
}
|
|
573
601
|
|
|
@@ -718,6 +746,9 @@ export function serialiseSidecar(
|
|
|
718
746
|
if (entry.kind !== undefined) {
|
|
719
747
|
fields.push(`kind=${entry.kind}`);
|
|
720
748
|
}
|
|
749
|
+
if (entry.askedAtMain !== undefined && FULL_SHA.test(entry.askedAtMain)) {
|
|
750
|
+
fields.push(`askedAtMain=${entry.askedAtMain}`);
|
|
751
|
+
}
|
|
721
752
|
out.push(`<!-- ${entry.id} fields: ${fields.join(' ')} -->`);
|
|
722
753
|
out.push('');
|
|
723
754
|
out.push(ANSWER_MARKER);
|
|
@@ -775,6 +806,8 @@ export interface NewQuestion {
|
|
|
775
806
|
* agentic `decide()` path. INTERIM — removable once kind-subfolders land.
|
|
776
807
|
*/
|
|
777
808
|
kind?: SidecarKind;
|
|
809
|
+
/** The arbiter `main` sha the question is asked against (see {@link SidecarEntry.askedAtMain}). */
|
|
810
|
+
askedAtMain?: string;
|
|
778
811
|
}
|
|
779
812
|
|
|
780
813
|
/**
|
|
@@ -802,6 +835,9 @@ export function appendQuestions(
|
|
|
802
835
|
if (q.kind !== undefined) {
|
|
803
836
|
entry.kind = q.kind;
|
|
804
837
|
}
|
|
838
|
+
if (q.askedAtMain !== undefined) {
|
|
839
|
+
entry.askedAtMain = q.askedAtMain;
|
|
840
|
+
}
|
|
805
841
|
entries.push(entry);
|
|
806
842
|
}
|
|
807
843
|
return {...model, entries};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **old single-job workflow warning** (decision 9 of ADR
|
|
3
|
+
* `ci-agent-job-holds-no-write-token`, task `ci-split-warn-single-job-workflows`).
|
|
4
|
+
*
|
|
5
|
+
* A consumer that upgrades dorfl but keeps a workflow generated before the CI
|
|
6
|
+
* split runs an agent verb in ONE job whose checkout persisted the write token
|
|
7
|
+
* in `.git/config`. The agent can read that credential. dorfl cannot fix the
|
|
8
|
+
* workflow from inside it, so it warns loudly: in GitHub Actions, when an
|
|
9
|
+
* agent-spawning verb ({@link AGENT_SPAWNING_VERBS}) runs WITHOUT `--phase` in a
|
|
10
|
+
* checkout whose local git config holds a credential. Behaviour is otherwise
|
|
11
|
+
* unchanged; the next minor version turns this warning into a refusal (an
|
|
12
|
+
* intent recorded in the warning text and the changeset, not in code).
|
|
13
|
+
*
|
|
14
|
+
* The credential itself is NEVER printed: detection returns only a boolean.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {inGitHubActions} from './agent-env.js';
|
|
18
|
+
import {run} from './git.js';
|
|
19
|
+
import {AGENT_SPAWNING_VERBS, type Phase} from './phase.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Agent-spawning verbs this warning deliberately skips. `verify` is in
|
|
23
|
+
* {@link AGENT_SPAWNING_VERBS} because it runs repository code, but it launches
|
|
24
|
+
* no agent, and the generated `verify.yml` persists only a `contents: read`
|
|
25
|
+
* credential. Warning there would flag every private consumer's PR check, and
|
|
26
|
+
* the planned refusal would break it. Task `ci-split-generate-workflows` (the
|
|
27
|
+
* workflow guard) must apply the same exemption.
|
|
28
|
+
*/
|
|
29
|
+
export const SINGLE_JOB_WARNING_EXEMPT_VERBS: ReadonlySet<string> = new Set([
|
|
30
|
+
'verify',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** An HTTP(S) URL carrying userinfo (`https://user:token@host/...`). */
|
|
34
|
+
const URL_WITH_USERINFO = /https?:\/\/[^/@\s]+@/i;
|
|
35
|
+
|
|
36
|
+
/** An `http.extraheader` / `http.<url>.extraheader` key. */
|
|
37
|
+
const EXTRAHEADER_KEY = /^http\.(?:.*\.)?extraheader$/i;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Does this `git config --null --list` output hold a credential: a non-empty
|
|
41
|
+
* `http.*.extraheader`, or any value that is an HTTP(S) URL with userinfo?
|
|
42
|
+
* Returns a boolean only, so no caller can print the secret by accident.
|
|
43
|
+
*/
|
|
44
|
+
export function gitConfigListHoldsCredential(nullList: string): boolean {
|
|
45
|
+
for (const entry of nullList.split('\0')) {
|
|
46
|
+
if (entry === '') {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const newline = entry.indexOf('\n');
|
|
50
|
+
const key = newline === -1 ? entry : entry.slice(0, newline);
|
|
51
|
+
const value = newline === -1 ? '' : entry.slice(newline + 1);
|
|
52
|
+
if (EXTRAHEADER_KEY.test(key) && value.trim() !== '') {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (URL_WITH_USERINFO.test(value)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Does the checkout at `cwd` persist a credential in its LOCAL git config
|
|
64
|
+
* (`.git/config` and the files it includes: `actions/checkout` v6+ writes the
|
|
65
|
+
* header to a separate file pulled in by an `includeIf`)? Global, system and
|
|
66
|
+
* environment (`GIT_CONFIG_*`) config are not read: the agent phase hands its
|
|
67
|
+
* read token to dorfl's git through the environment, which is not a persisted
|
|
68
|
+
* credential. Any failure (not a repository, no git) answers `false`: the
|
|
69
|
+
* warning never breaks a run.
|
|
70
|
+
*/
|
|
71
|
+
export function checkoutPersistsCredential(cwd: string): boolean {
|
|
72
|
+
try {
|
|
73
|
+
const result = run(
|
|
74
|
+
'git',
|
|
75
|
+
['config', '--local', '--includes', '--null', '--list'],
|
|
76
|
+
cwd,
|
|
77
|
+
);
|
|
78
|
+
return result.status === 0 && gitConfigListHoldsCredential(result.stdout);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The warning text (never contains the credential). */
|
|
85
|
+
export function singleJobWarningText(verb: string): string {
|
|
86
|
+
return [
|
|
87
|
+
'',
|
|
88
|
+
'!! WARNING: unsafe single-job CI workflow',
|
|
89
|
+
`!! 'dorfl ${verb}' is running in GitHub Actions without --phase, in a checkout`,
|
|
90
|
+
'!! whose git config persists a credential. Any agent this run launches can read',
|
|
91
|
+
'!! and use that token. Re-run `dorfl install-ci` to upgrade to the split',
|
|
92
|
+
'!! lock / agent / apply workflow, where the agent job holds no write token.',
|
|
93
|
+
'!! The next minor version of dorfl will REFUSE to run in this shape.',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Inputs to {@link singleJobWarning}. */
|
|
99
|
+
export interface SingleJobWarningInput {
|
|
100
|
+
/** The top-level CLI verb that is about to run. */
|
|
101
|
+
verb: string;
|
|
102
|
+
/** Its `--phase`, or `undefined` when none was given. */
|
|
103
|
+
phase: Phase | undefined;
|
|
104
|
+
env: NodeJS.ProcessEnv;
|
|
105
|
+
/** The checkout to inspect. */
|
|
106
|
+
cwd: string;
|
|
107
|
+
/** Injectable credential probe (default {@link checkoutPersistsCredential}). */
|
|
108
|
+
persistsCredential?: (cwd: string) => boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The warning to print for this invocation, or `undefined` when the shape is
|
|
113
|
+
* safe (a `--phase` run, not in GitHub Actions, a verb that spawns no agent or
|
|
114
|
+
* is exempt, or no persisted credential). The git config is read last, only
|
|
115
|
+
* when every cheap condition already says "unsafe".
|
|
116
|
+
*/
|
|
117
|
+
export function singleJobWarning(
|
|
118
|
+
input: SingleJobWarningInput,
|
|
119
|
+
): string | undefined {
|
|
120
|
+
const {verb, phase, env, cwd} = input;
|
|
121
|
+
if (phase !== undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (!AGENT_SPAWNING_VERBS.has(verb)) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (SINGLE_JOB_WARNING_EXEMPT_VERBS.has(verb)) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (!inGitHubActions(env)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const probe = input.persistsCredential ?? checkoutPersistsCredential;
|
|
134
|
+
return probe(cwd) ? singleJobWarningText(verb) : undefined;
|
|
135
|
+
}
|
package/src/spec-complete.ts
CHANGED
|
@@ -12,8 +12,13 @@ import {
|
|
|
12
12
|
/**
|
|
13
13
|
* The read-only **"is this spec complete?"** core query (spec `issue-intake`, US #8 —
|
|
14
14
|
* the closure-linkage half). Given a spec slug + a `work/` tree, a spec is COMPLETE
|
|
15
|
-
* iff there is **≥1 task carrying `spec:<slug
|
|
16
|
-
*
|
|
15
|
+
* iff there is **≥1 task carrying `spec:<slug>` in `work/tasks/done/`** AND **every
|
|
16
|
+
* other such task is in `work/tasks/cancelled/`**. A task that names the spec
|
|
17
|
+
* ANYWHERE else (staged in `tasks/backlog/`, in `tasks/ready/`, in progress) keeps
|
|
18
|
+
* the spec incomplete; a cancelled task does not block completion, but a spec whose
|
|
19
|
+
* tasks are ALL cancelled (none done) is NOT complete (reported as
|
|
20
|
+
* {@link SpecCompleteResult.allCancelled}, so the close-job never closes its issue
|
|
21
|
+
* as `completed`). Pure `work/`-folder logic — no seam, no git, no `gh`, no mutation.
|
|
17
22
|
*
|
|
18
23
|
* This is the LINKAGE the intake engine emits for CI to ACT on: a spec fans out to N
|
|
19
24
|
* tasks = N PRs whose tasks carry `spec:` ONLY (no `Refs #N` keyword is emitted),
|
|
@@ -26,9 +31,11 @@ import {
|
|
|
26
31
|
* It is a `work/`-FOLDER RESIDENCE scan keyed on the parsed `spec:` field — NOT the
|
|
27
32
|
* claim ledger (`ledger-read.ts` resolves claim-STATE, a different concern). It
|
|
28
33
|
* reuses {@link parseFrontmatter} (the `spec:` field) rather than hand-rolling a YAML
|
|
29
|
-
* parse, and scans
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
34
|
+
* parse, and scans EVERY task lifecycle folder directly ({@link TASK_LIFECYCLE_FOLDERS}:
|
|
35
|
+
* backlog, ready, in-progress, done, cancelled). Task
|
|
36
|
+
* `spec-complete-counts-staged-and-cancelled-tasks`: the scan used to skip
|
|
37
|
+
* `tasks/backlog/`, so a spec whose tasks were still staged read as complete as
|
|
38
|
+
* soon as its first task landed.
|
|
32
39
|
*/
|
|
33
40
|
|
|
34
41
|
/** The task lifecycle folders a `spec:<slug>` task can reside in. */
|
|
@@ -62,11 +69,18 @@ export interface SpecTask {
|
|
|
62
69
|
*/
|
|
63
70
|
export interface SpecCompleteResult {
|
|
64
71
|
/**
|
|
65
|
-
* `true` iff ≥1
|
|
66
|
-
* `work/
|
|
67
|
-
* any matching task is still
|
|
72
|
+
* `true` iff ≥1 `spec:<slug>` task resides in `work/tasks/done/` AND every other
|
|
73
|
+
* such task resides in `work/tasks/cancelled/`. `false` when no task carries the
|
|
74
|
+
* slug, when any matching task is still open (backlog / ready / in-progress), or
|
|
75
|
+
* when every matching task is cancelled (see {@link allCancelled}).
|
|
68
76
|
*/
|
|
69
77
|
complete: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* `true` iff ≥1 task carries `spec:<slug>` AND every one of them is in
|
|
80
|
+
* `work/tasks/cancelled/` (none done): the spec did not complete, it was
|
|
81
|
+
* abandoned. Never `true` together with {@link complete}.
|
|
82
|
+
*/
|
|
83
|
+
allCancelled: boolean;
|
|
70
84
|
/** Every task carrying `spec:<slug>`, across all task folders, sorted by slug. */
|
|
71
85
|
tasks: SpecTask[];
|
|
72
86
|
}
|
|
@@ -84,10 +98,10 @@ function listMarkdown(repoPath: string, folder: TaskFolder): string[] {
|
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
/**
|
|
87
|
-
* Is this spec COMPLETE? Read-only: scan
|
|
101
|
+
* Is this spec COMPLETE? Read-only: scan every task lifecycle folder, parse each
|
|
88
102
|
* task's `spec:` via {@link parseFrontmatter}, keep those whose `spec:` equals
|
|
89
|
-
* `slug`, and return COMPLETE iff
|
|
90
|
-
* `work/done/`. Touches no git, no network, no mutation — a pure `work/`-folder
|
|
103
|
+
* `slug`, and return COMPLETE iff ≥1 member resides in `work/tasks/done/` and EVERY
|
|
104
|
+
* member resides in `work/tasks/done/` or `work/tasks/cancelled/`. Touches no git, no network, no mutation — a pure `work/`-folder
|
|
91
105
|
* residence scan keyed on the parsed `spec:` field.
|
|
92
106
|
*/
|
|
93
107
|
export function isSpecComplete(input: SpecCompleteInput): SpecCompleteResult {
|
|
@@ -111,7 +125,15 @@ export function isSpecComplete(input: SpecCompleteInput): SpecCompleteResult {
|
|
|
111
125
|
}
|
|
112
126
|
tasks.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
113
127
|
|
|
114
|
-
// COMPLETE iff ≥1 such task AND every one of them is
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
// COMPLETE iff ≥1 such task is done AND every one of them is done or
|
|
129
|
+
// cancelled. A staged/ready/in-progress task keeps it open; an all-cancelled
|
|
130
|
+
// set is NOT complete (nothing was delivered).
|
|
131
|
+
const settled = tasks.every(
|
|
132
|
+
(s) => s.folder === 'done' || s.folder === 'cancelled',
|
|
133
|
+
);
|
|
134
|
+
const anyDone = tasks.some((s) => s.folder === 'done');
|
|
135
|
+
const complete = settled && anyDone;
|
|
136
|
+
const allCancelled =
|
|
137
|
+
tasks.length > 0 && tasks.every((s) => s.folder === 'cancelled');
|
|
138
|
+
return {complete, allCancelled, tasks};
|
|
117
139
|
}
|