dorfl 0.7.0 → 0.8.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-drivers.d.ts.map +1 -1
- package/dist/advance-drivers.js +11 -2
- package/dist/advance-drivers.js.map +1 -1
- package/dist/bootstrap-forward.d.ts +224 -0
- package/dist/bootstrap-forward.d.ts.map +1 -0
- package/dist/bootstrap-forward.js +247 -0
- package/dist/bootstrap-forward.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +50 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -1
- package/dist/cwd-section.d.ts.map +1 -1
- package/dist/cwd-section.js +11 -2
- package/dist/cwd-section.js.map +1 -1
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +8 -0
- package/dist/env-config.js.map +1 -1
- package/dist/github.d.ts +32 -1
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +115 -0
- package/dist/github.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +20 -48
- package/dist/install-ci-core.js.map +1 -1
- package/dist/integration-core.d.ts.map +1 -1
- package/dist/integration-core.js +9 -0
- package/dist/integration-core.js.map +1 -1
- package/dist/integrator.d.ts +48 -0
- package/dist/integrator.d.ts.map +1 -1
- package/dist/integrator.js +12 -0
- package/dist/integrator.js.map +1 -1
- package/dist/item-lock.d.ts +26 -0
- package/dist/item-lock.d.ts.map +1 -1
- package/dist/item-lock.js +39 -0
- package/dist/item-lock.js.map +1 -1
- package/dist/repo-config.d.ts +1 -1
- package/dist/repo-config.d.ts.map +1 -1
- package/dist/repo-config.js +20 -1
- package/dist/repo-config.js.map +1 -1
- package/dist/scan.d.ts +24 -2
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +28 -4
- package/dist/scan.js.map +1 -1
- package/dist/skills/setup/SKILL.md +3 -0
- package/dist/tasking.d.ts.map +1 -1
- package/dist/tasking.js +124 -65
- package/dist/tasking.js.map +1 -1
- package/package.json +1 -1
- package/src/advance-drivers.ts +17 -8
- package/src/bootstrap-forward.ts +359 -0
- package/src/cli.ts +21 -1
- package/src/config.ts +75 -0
- package/src/cwd-section.ts +21 -2
- package/src/env-config.ts +8 -0
- package/src/github.ts +149 -0
- package/src/index.ts +24 -0
- package/src/install-ci-core.ts +20 -48
- package/src/integration-core.ts +10 -0
- package/src/integrator.ts +62 -0
- package/src/item-lock.ts +50 -0
- package/src/repo-config.ts +20 -0
- package/src/scan.ts +32 -8
- package/src/tasking.ts +154 -68
package/src/tasking.ts
CHANGED
|
@@ -435,6 +435,11 @@ export async function performTask(
|
|
|
435
435
|
// with no contention may task on `main` directly WITHOUT the lock.
|
|
436
436
|
let lockedBlob: string | undefined;
|
|
437
437
|
const useLock = doer === 'agent';
|
|
438
|
+
// The integrate mode resolved ONCE (`--merge`/`--propose` > default `propose`),
|
|
439
|
+
// used to decide BOTH the tasking lock's lifetime (release on `merge`, HOLD
|
|
440
|
+
// across the PR on `propose`) AND whether a not-landed surface should close a
|
|
441
|
+
// stale open PR (propose only).
|
|
442
|
+
const resolvedMode = options.integration ?? 'propose';
|
|
438
443
|
if (useLock) {
|
|
439
444
|
const acquired = await lock.acquire({slug, cwd, arbiter, env, note});
|
|
440
445
|
if (acquired.outcome === 'lost') {
|
|
@@ -545,22 +550,25 @@ export async function performTask(
|
|
|
545
550
|
loopDisposition.specQuestions,
|
|
546
551
|
);
|
|
547
552
|
if (useLock) {
|
|
548
|
-
|
|
553
|
+
return await surfaceTaskingBlock({
|
|
549
554
|
slug,
|
|
550
555
|
cwd,
|
|
551
556
|
arbiter,
|
|
557
|
+
reason,
|
|
558
|
+
message: loopDisposition.message,
|
|
552
559
|
lockedBlob,
|
|
553
|
-
|
|
560
|
+
release: lock.release,
|
|
561
|
+
mode: resolvedMode,
|
|
562
|
+
provider: options.providerInstance,
|
|
554
563
|
env,
|
|
555
564
|
note,
|
|
556
565
|
});
|
|
557
|
-
if (routed.outcome !== 'released') {
|
|
558
|
-
return releaseFailureToResult(routed, slug);
|
|
559
|
-
}
|
|
560
566
|
}
|
|
561
567
|
note(loopDisposition.message);
|
|
568
|
+
// Human, no-lock path: nothing to release/surface via the seam; a clean
|
|
569
|
+
// park-for-human is still a success terminal (exit 0).
|
|
562
570
|
return {
|
|
563
|
-
exitCode:
|
|
571
|
+
exitCode: 0,
|
|
564
572
|
outcome: 'needs-attention',
|
|
565
573
|
slug,
|
|
566
574
|
message: loopDisposition.message,
|
|
@@ -687,7 +695,7 @@ export async function performTask(
|
|
|
687
695
|
// `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
|
|
688
696
|
// The task gate family is `--review`/`--no-review`/`--review-model` only
|
|
689
697
|
// (spec US #6).
|
|
690
|
-
mode:
|
|
698
|
+
mode: resolvedMode,
|
|
691
699
|
noPR: options.noPR,
|
|
692
700
|
providerInstance: options.providerInstance,
|
|
693
701
|
// PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
|
|
@@ -736,27 +744,22 @@ export async function performTask(
|
|
|
736
744
|
// needs-attention THROUGH the lock release — the set never lands.
|
|
737
745
|
if (core.outcome === 'review-blocked') {
|
|
738
746
|
const reason = taskGateBlockedReason(slug, core.reviewBlockReason);
|
|
739
|
-
|
|
747
|
+
return await surfaceTaskingBlock({
|
|
740
748
|
slug,
|
|
741
749
|
cwd,
|
|
742
750
|
arbiter,
|
|
751
|
+
reason,
|
|
752
|
+
message:
|
|
753
|
+
`The task acceptance gate disapproved the set produced for '${slug}'; ` +
|
|
754
|
+
`parked it for your attention (no tasks landed). ` +
|
|
755
|
+
`Resolve the findings, then re-task.`,
|
|
743
756
|
lockedBlob,
|
|
744
|
-
|
|
757
|
+
release: lock.release,
|
|
758
|
+
mode: resolvedMode,
|
|
759
|
+
provider: options.providerInstance,
|
|
745
760
|
env,
|
|
746
761
|
note,
|
|
747
762
|
});
|
|
748
|
-
if (routed.outcome !== 'released') {
|
|
749
|
-
return releaseFailureToResult(routed, slug);
|
|
750
|
-
}
|
|
751
|
-
note(reason);
|
|
752
|
-
return {
|
|
753
|
-
exitCode: 1,
|
|
754
|
-
outcome: 'needs-attention',
|
|
755
|
-
slug,
|
|
756
|
-
message:
|
|
757
|
-
`The task acceptance gate blocked the set produced for '${slug}'; ` +
|
|
758
|
-
`marked the per-item lock stuck (needs attention; no tasks landed).`,
|
|
759
|
-
};
|
|
760
763
|
}
|
|
761
764
|
if (core.outcome === 'sidecar-violation') {
|
|
762
765
|
// A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
|
|
@@ -768,28 +771,22 @@ export async function performTask(
|
|
|
768
771
|
const reason =
|
|
769
772
|
`The spec '${slug}' carries a co-located asset sidecar: ` +
|
|
770
773
|
`${core.reviewBlockReason ?? core.reason ?? ''}`;
|
|
771
|
-
|
|
774
|
+
return await surfaceTaskingBlock({
|
|
772
775
|
slug,
|
|
773
776
|
cwd,
|
|
774
777
|
arbiter,
|
|
778
|
+
reason,
|
|
779
|
+
message:
|
|
780
|
+
`The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
|
|
781
|
+
`rule 8); parked it for your attention (no tasks landed; relocate it ` +
|
|
782
|
+
`to docs/spikes/${slug}/ and reference by path).`,
|
|
775
783
|
lockedBlob,
|
|
776
|
-
|
|
784
|
+
release: lock.release,
|
|
785
|
+
mode: resolvedMode,
|
|
786
|
+
provider: options.providerInstance,
|
|
777
787
|
env,
|
|
778
788
|
note,
|
|
779
789
|
});
|
|
780
|
-
if (routed.outcome !== 'released') {
|
|
781
|
-
return releaseFailureToResult(routed, slug);
|
|
782
|
-
}
|
|
783
|
-
note(reason);
|
|
784
|
-
return {
|
|
785
|
-
exitCode: 1,
|
|
786
|
-
outcome: 'needs-attention',
|
|
787
|
-
slug,
|
|
788
|
-
message:
|
|
789
|
-
`The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
|
|
790
|
-
`rule 8); marked the per-item lock stuck (needs attention; no tasks ` +
|
|
791
|
-
`landed; relocate it to docs/spikes/${slug}/ and reference by path).`,
|
|
792
|
-
};
|
|
793
790
|
}
|
|
794
791
|
if (core.outcome === 'review-unparseable') {
|
|
795
792
|
// The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
|
|
@@ -801,41 +798,50 @@ export async function performTask(
|
|
|
801
798
|
const reason =
|
|
802
799
|
`The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
|
|
803
800
|
`(re-run — transient): ${core.reason ?? ''}`;
|
|
804
|
-
|
|
801
|
+
return await surfaceTaskingBlock({
|
|
805
802
|
slug,
|
|
806
803
|
cwd,
|
|
807
804
|
arbiter,
|
|
805
|
+
reason,
|
|
806
|
+
message:
|
|
807
|
+
`The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
|
|
808
|
+
`parked it for your attention (no tasks landed; re-run).`,
|
|
808
809
|
lockedBlob,
|
|
809
|
-
|
|
810
|
+
release: lock.release,
|
|
811
|
+
mode: resolvedMode,
|
|
812
|
+
provider: options.providerInstance,
|
|
810
813
|
env,
|
|
811
814
|
note,
|
|
812
815
|
});
|
|
813
|
-
if (routed.outcome !== 'released') {
|
|
814
|
-
return releaseFailureToResult(routed, slug);
|
|
815
|
-
}
|
|
816
|
-
note(reason);
|
|
817
|
-
return {
|
|
818
|
-
exitCode: 1,
|
|
819
|
-
outcome: 'needs-attention',
|
|
820
|
-
slug,
|
|
821
|
-
message:
|
|
822
|
-
`The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
|
|
823
|
-
`marked the per-item lock stuck (needs attention; no tasks landed; re-run).`,
|
|
824
|
-
};
|
|
825
816
|
}
|
|
826
817
|
if (core.outcome === 'completed') {
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
|
|
818
|
+
// LOCK LIFETIME IS MODE-DEPENDENT (fix
|
|
819
|
+
// `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
|
|
820
|
+
// the "hold-across-the-PR" capstone the older interim half deferred).
|
|
821
|
+
//
|
|
822
|
+
// `merge`: the durable `specs/ready → specs/tasked` `main` move HAS landed, so
|
|
823
|
+
// residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
|
|
824
|
+
// has done its job — RELEASE it (delete the ref). Best-effort + idempotent
|
|
825
|
+
// (`not-held` is fine).
|
|
826
|
+
//
|
|
827
|
+
// `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
|
|
828
|
+
// still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
|
|
829
|
+
// tasked-ness. If we released here, the next in-place scan would see the spec
|
|
830
|
+
// eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
|
|
831
|
+
// force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
|
|
832
|
+
// regenerating its review forever until a human merges/closes it. So we KEEP
|
|
833
|
+
// the `spec:<slug>` lock HELD across the open PR: the held lock IS the
|
|
834
|
+
// "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
|
|
835
|
+
// specs from the taskable pool (symmetric to the task-pool held-slug
|
|
836
|
+
// subtraction). The lock is reaped when the PR resolves: a MERGE lands the
|
|
837
|
+
// durable move (which now carries tasked-ness on its own) and the reap/gc path
|
|
838
|
+
// deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
|
|
839
|
+
// recovery the human owns via `release-lock` (the same model as any other
|
|
840
|
+
// abandoned in-flight item — no liveness heartbeat/auto-steal).
|
|
841
|
+
//
|
|
842
|
+
// MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
|
|
843
|
+
// `spec-<slug>` lock entry the acquire took).
|
|
844
|
+
if (useLock && resolvedMode === 'merge') {
|
|
839
845
|
await releaseItemLock({item: `spec:${slug}`, cwd, arbiter, env});
|
|
840
846
|
}
|
|
841
847
|
}
|
|
@@ -1199,21 +1205,101 @@ function taskGateBlockedReason(
|
|
|
1199
1205
|
): string {
|
|
1200
1206
|
const head =
|
|
1201
1207
|
`The task acceptance gate (fresh-context review of the produced SET) blocked ` +
|
|
1202
|
-
`'${slug}'. The spec is
|
|
1203
|
-
`
|
|
1208
|
+
`'${slug}'. The spec is parked for your attention with no tasks landed; ` +
|
|
1209
|
+
`resolve the blocking findings, then re-task.`;
|
|
1204
1210
|
return findingsReason ? `${head}\n\n${findingsReason}` : head;
|
|
1205
1211
|
}
|
|
1206
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* Shared terminal for a tasking transition that DID NOT land — a disapproving
|
|
1215
|
+
* task-set review, a co-located sidecar violation, an unparseable verdict, or a
|
|
1216
|
+
* decomposition-unclear loop exhaustion. It:
|
|
1217
|
+
*
|
|
1218
|
+
* 1. (propose mode + a provider) CLOSES the spec's OPEN PR — if one already
|
|
1219
|
+
* exists (the multi-run artefact) — with the disapproving `reason` as the
|
|
1220
|
+
* CLOSING COMMENT, KEEPING the branch (task
|
|
1221
|
+
* `tasking-disapprove-closes-existing-pr-keeps-branch`). A later approving
|
|
1222
|
+
* re-task REOPENs the same PR via `openRequest`. Only-if-exists: we never
|
|
1223
|
+
* OPEN a PR just to close it, and merge mode has no PR to close.
|
|
1224
|
+
* 2. RELEASES the spec's per-item lock and SURFACES the item for the human as a
|
|
1225
|
+
* `needsAnswers: true` body + a question sidecar on `<arbiter>/main` (via the
|
|
1226
|
+
* release's `routeToNeedsAttention` redirect). The lock is RELEASED, not
|
|
1227
|
+
* held — this is a parked question, not a lock the human must clear.
|
|
1228
|
+
*
|
|
1229
|
+
* A clean surface is a SUCCESSFUL terminal of the loop's "drain toward done OR
|
|
1230
|
+
* surface a question and idle" contract, so it returns `exitCode: 0` (the CI leg
|
|
1231
|
+
* is GREEN — a park-for-human is not a failure). Only a surface that FAILED to
|
|
1232
|
+
* publish maps to a non-zero {@link releaseFailureToResult}.
|
|
1233
|
+
*/
|
|
1234
|
+
async function surfaceTaskingBlock(params: {
|
|
1235
|
+
slug: string;
|
|
1236
|
+
cwd: string;
|
|
1237
|
+
arbiter: string;
|
|
1238
|
+
reason: string;
|
|
1239
|
+
message: string;
|
|
1240
|
+
lockedBlob: string | undefined;
|
|
1241
|
+
release: TaskingLockSeam['release'];
|
|
1242
|
+
mode: 'propose' | 'merge';
|
|
1243
|
+
provider: ReviewProvider | undefined;
|
|
1244
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1245
|
+
note: (message: string) => void;
|
|
1246
|
+
}): Promise<TaskResult> {
|
|
1247
|
+
const {
|
|
1248
|
+
slug,
|
|
1249
|
+
cwd,
|
|
1250
|
+
arbiter,
|
|
1251
|
+
reason,
|
|
1252
|
+
message,
|
|
1253
|
+
lockedBlob,
|
|
1254
|
+
release,
|
|
1255
|
+
mode,
|
|
1256
|
+
provider,
|
|
1257
|
+
env,
|
|
1258
|
+
note,
|
|
1259
|
+
} = params;
|
|
1260
|
+
|
|
1261
|
+
// 1. Close a stale OPEN PR (propose only, only-if-exists) with the review as
|
|
1262
|
+
// the closing comment; keep the branch for a later approving re-task to
|
|
1263
|
+
// reopen. Advisory + never-throw — a close failure never blocks the surface.
|
|
1264
|
+
if (mode === 'propose' && provider !== undefined) {
|
|
1265
|
+
const closed = await provider.closeRequestOnBranch({
|
|
1266
|
+
cwd,
|
|
1267
|
+
branch: workBranchRef('spec', slug),
|
|
1268
|
+
arbiter,
|
|
1269
|
+
comment: reason,
|
|
1270
|
+
env,
|
|
1271
|
+
});
|
|
1272
|
+
note(closed.instruction);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// 2. Release the lock + surface the parked question on main.
|
|
1276
|
+
const routed = await release({
|
|
1277
|
+
slug,
|
|
1278
|
+
cwd,
|
|
1279
|
+
arbiter,
|
|
1280
|
+
lockedBlob,
|
|
1281
|
+
routeToNeedsAttention: {reason},
|
|
1282
|
+
env,
|
|
1283
|
+
note,
|
|
1284
|
+
});
|
|
1285
|
+
if (routed.outcome !== 'released') {
|
|
1286
|
+
return releaseFailureToResult(routed, slug);
|
|
1287
|
+
}
|
|
1288
|
+
note(reason);
|
|
1289
|
+
// exit 0: a clean park-for-human is a successful loop terminal, not a failure.
|
|
1290
|
+
return {exitCode: 0, outcome: 'needs-attention', slug, message};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1207
1293
|
/**
|
|
1208
1294
|
* Build the needs-attention REASON for a decomposition-unclear loop verdict (the
|
|
1209
|
-
* spec is
|
|
1210
|
-
* tasks). Prose only — recorded as the
|
|
1295
|
+
* spec is parked for the human with these open questions + a question sidecar on
|
|
1296
|
+
* main, no guessed tasks). Prose only — recorded as the parked question's reason.
|
|
1211
1297
|
*/
|
|
1212
1298
|
function decompositionUnclearReason(slug: string, questions: string[]): string {
|
|
1213
1299
|
const head =
|
|
1214
1300
|
`The tasker review→edit loop could not converge on a sound decomposition of ` +
|
|
1215
|
-
`'${slug}' (--tasker-loop-max exhausted with unresolved blockers). The spec is
|
|
1216
|
-
`
|
|
1301
|
+
`'${slug}' (--tasker-loop-max exhausted with unresolved blockers). The spec is parked ` +
|
|
1302
|
+
`for your attention with no guessed tasks; resolve:`;
|
|
1217
1303
|
const body =
|
|
1218
1304
|
questions.length > 0
|
|
1219
1305
|
? questions.map((q) => `- ${q}`).join('\n')
|