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/github.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
type PostPRCommentInput,
|
|
11
11
|
type PostPRCommentOnBranchInput,
|
|
12
12
|
type PostPRCommentResult,
|
|
13
|
+
type CloseRequestOnBranchInput,
|
|
14
|
+
type CloseRequestOnBranchResult,
|
|
13
15
|
} from './integrator.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -185,6 +187,27 @@ export class GitHubProvider implements ReviewProvider {
|
|
|
185
187
|
* record / `status`).
|
|
186
188
|
*/
|
|
187
189
|
async openRequest(input: OpenRequestInput): Promise<OpenRequestResult> {
|
|
190
|
+
// REOPEN a CLOSED PR before trying to CREATE one (task
|
|
191
|
+
// `tasking-disapprove-closes-existing-pr-keeps-branch`): a spec whose earlier
|
|
192
|
+
// tasking review DISAPPROVED had its PR CLOSED (branch kept). A later re-task
|
|
193
|
+
// that now APPROVES must REOPEN that SAME PR (preserving its history +
|
|
194
|
+
// closing-comment thread) rather than leaving a new duplicate. `gh pr create`
|
|
195
|
+
// on a branch with a closed PR would open a SECOND PR, so we reopen first.
|
|
196
|
+
// Best-effort + never-throw; a failed reopen falls through to the normal
|
|
197
|
+
// create path (the branch is already pushed, so the work is safe).
|
|
198
|
+
const existingForReopen = this.resolvePrForBranch(
|
|
199
|
+
input.branch,
|
|
200
|
+
input.cwd,
|
|
201
|
+
input.env,
|
|
202
|
+
);
|
|
203
|
+
if (existingForReopen?.state === 'CLOSED') {
|
|
204
|
+
const reopened = this.reopenExistingRequest(input, existingForReopen.url);
|
|
205
|
+
if (reopened !== undefined) {
|
|
206
|
+
return reopened;
|
|
207
|
+
}
|
|
208
|
+
// Could not reopen (transient `gh` failure) — fall through to create.
|
|
209
|
+
}
|
|
210
|
+
|
|
188
211
|
const args = [
|
|
189
212
|
'pr',
|
|
190
213
|
'create',
|
|
@@ -307,6 +330,93 @@ export class GitHubProvider implements ReviewProvider {
|
|
|
307
330
|
};
|
|
308
331
|
}
|
|
309
332
|
|
|
333
|
+
/**
|
|
334
|
+
* REOPEN a previously-CLOSED PR for the branch (task
|
|
335
|
+
* `tasking-disapprove-closes-existing-pr-keeps-branch`): `gh pr reopen <url>`,
|
|
336
|
+
* then refresh its title/body (reusing {@link updateExistingRequest}'s edit so
|
|
337
|
+
* the reopened PR carries this run's content). Returns `{opened: true, url}` on
|
|
338
|
+
* a successful reopen, or `undefined` when the reopen itself failed (the caller
|
|
339
|
+
* then falls through to `gh pr create`). Best-effort edit — a failed refresh
|
|
340
|
+
* still returns the reopened PR. NEVER throws.
|
|
341
|
+
*/
|
|
342
|
+
private reopenExistingRequest(
|
|
343
|
+
input: OpenRequestInput,
|
|
344
|
+
url: string,
|
|
345
|
+
): OpenRequestResult | undefined {
|
|
346
|
+
const reopen = this.runGh(['pr', 'reopen', url], input.cwd, input.env);
|
|
347
|
+
if (reopen === undefined || reopen.status !== 0) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
// Refresh title/body on the reopened PR (best-effort; only when supplied).
|
|
351
|
+
if (input.title !== undefined || input.body !== undefined) {
|
|
352
|
+
const editArgs = ['pr', 'edit', url];
|
|
353
|
+
if (input.title !== undefined) {
|
|
354
|
+
editArgs.push('--title', input.title);
|
|
355
|
+
}
|
|
356
|
+
if (input.body !== undefined) {
|
|
357
|
+
editArgs.push('--body', input.body);
|
|
358
|
+
}
|
|
359
|
+
this.runGh(editArgs, input.cwd, input.env);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
opened: true,
|
|
363
|
+
url,
|
|
364
|
+
instruction: `Reopened the existing GitHub PR for ${input.branch}: ${url}`,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* CLOSE the branch's OPEN PR (keeping the branch) with the disapproving review
|
|
370
|
+
* as the closing comment — the disapprove artefact-cleanup path (task
|
|
371
|
+
* `tasking-disapprove-closes-existing-pr-keeps-branch`). Resolves the PR from
|
|
372
|
+
* the branch; when there is NO OPEN PR (none at all, already closed/merged, or
|
|
373
|
+
* `gh` missing) it is a clean no-op (`closed: false`) — we never OPEN a PR just
|
|
374
|
+
* to close it. `gh pr close <url> --comment <review>` (NO `--delete-branch`, so
|
|
375
|
+
* the branch — the recovery point — survives for a later approving re-task to
|
|
376
|
+
* REOPEN). NEVER throws, NEVER `--force`s (ADR §6).
|
|
377
|
+
*/
|
|
378
|
+
async closeRequestOnBranch(
|
|
379
|
+
input: CloseRequestOnBranchInput,
|
|
380
|
+
): Promise<CloseRequestOnBranchResult> {
|
|
381
|
+
const existing = this.resolvePrForBranch(
|
|
382
|
+
input.branch,
|
|
383
|
+
input.cwd,
|
|
384
|
+
input.env,
|
|
385
|
+
);
|
|
386
|
+
if (existing === undefined || existing.state !== 'OPEN') {
|
|
387
|
+
// No OPEN PR to close (none, already closed/merged, or gh unavailable):
|
|
388
|
+
// honest no-op AFTER trying. Surface the review so it is never lost.
|
|
389
|
+
return {
|
|
390
|
+
closed: false,
|
|
391
|
+
instruction:
|
|
392
|
+
`No open PR to close for ${input.branch} (the branch is kept). ` +
|
|
393
|
+
`The disapproving review:\n${input.comment}`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const closed = this.runGh(
|
|
397
|
+
['pr', 'close', existing.url, '--comment', input.comment],
|
|
398
|
+
input.cwd,
|
|
399
|
+
input.env,
|
|
400
|
+
);
|
|
401
|
+
if (closed === undefined || closed.status !== 0) {
|
|
402
|
+
// The close failed (transient) — the branch + PR are untouched; surface
|
|
403
|
+
// the review text and report no-close. The item is still surfaced to
|
|
404
|
+
// needs-attention by the caller regardless.
|
|
405
|
+
return {
|
|
406
|
+
closed: false,
|
|
407
|
+
instruction:
|
|
408
|
+
`Could not close the PR for ${input.branch} (the branch is kept). ` +
|
|
409
|
+
`The disapproving review:\n${input.comment}`,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
closed: true,
|
|
414
|
+
instruction:
|
|
415
|
+
`Closed the stale PR for ${input.branch} with the disapproving ` +
|
|
416
|
+
`review as its closing comment (branch kept): ${existing.url}`,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
310
420
|
/** Map a successful `gh pr create` RunResult to an OpenRequestResult. */
|
|
311
421
|
private parseOpened(
|
|
312
422
|
input: OpenRequestInput,
|
|
@@ -429,6 +539,45 @@ export class GitHubProvider implements ReviewProvider {
|
|
|
429
539
|
return parsePrUrl(result.stdout);
|
|
430
540
|
}
|
|
431
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Resolve the branch's PR url AND state (`OPEN` / `CLOSED` / `MERGED`) via
|
|
544
|
+
* `gh pr view <branch> --json url,state`, or `undefined` when no PR exists /
|
|
545
|
+
* `gh` is missing. Needed by {@link openRequest} to REOPEN a CLOSED PR (the
|
|
546
|
+
* disapprove-close artefact — a later approving re-task reopens the SAME PR
|
|
547
|
+
* instead of leaving a new one) and by {@link closeRequestOnBranch} to no-op
|
|
548
|
+
* when there is no OPEN PR to close. `gh pr view <branch>` reports the most
|
|
549
|
+
* recent PR for the branch, including a closed one. Read-only.
|
|
550
|
+
*/
|
|
551
|
+
private resolvePrForBranch(
|
|
552
|
+
branch: string,
|
|
553
|
+
cwd: string,
|
|
554
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
555
|
+
): {url: string; state: string} | undefined {
|
|
556
|
+
const result = this.runGh(
|
|
557
|
+
[
|
|
558
|
+
'pr',
|
|
559
|
+
'view',
|
|
560
|
+
branch,
|
|
561
|
+
'--json',
|
|
562
|
+
'url,state',
|
|
563
|
+
'--jq',
|
|
564
|
+
'.url + " " + .state',
|
|
565
|
+
],
|
|
566
|
+
cwd,
|
|
567
|
+
env,
|
|
568
|
+
);
|
|
569
|
+
if (result === undefined || result.status !== 0) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
const url = parsePrUrl(result.stdout);
|
|
573
|
+
if (url === undefined) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
// The `--jq` prints `<url> <STATE>`; the state is the last whitespace token.
|
|
577
|
+
const state = result.stdout.trim().split(/\s+/).pop() ?? '';
|
|
578
|
+
return {url, state};
|
|
579
|
+
}
|
|
580
|
+
|
|
432
581
|
/**
|
|
433
582
|
* Is `gh` available AND authenticated (so a PR can actually be opened)?
|
|
434
583
|
* `gh auth status` exits 0 when authenticated. A missing `gh` (spawn failure)
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,30 @@ export {
|
|
|
35
35
|
resolveRepoConfig,
|
|
36
36
|
} from './repo-config.js';
|
|
37
37
|
|
|
38
|
+
export type {
|
|
39
|
+
ForwardDecision,
|
|
40
|
+
ForwardOutcome,
|
|
41
|
+
ForwardSpawn,
|
|
42
|
+
ForwardSpawnResult,
|
|
43
|
+
RepoCmdReader,
|
|
44
|
+
} from './bootstrap-forward.js';
|
|
45
|
+
export {
|
|
46
|
+
FORWARDED_ENV_MARKER,
|
|
47
|
+
NO_FORWARD_ENV,
|
|
48
|
+
NO_FORWARD_FLAG,
|
|
49
|
+
decideForward,
|
|
50
|
+
performForward,
|
|
51
|
+
maybeForward,
|
|
52
|
+
defaultRepoCmdReader,
|
|
53
|
+
defaultForwardSpawn,
|
|
54
|
+
forwardNotice,
|
|
55
|
+
forwardFailureMessage,
|
|
56
|
+
argvHasNoForward,
|
|
57
|
+
envHasNoForward,
|
|
58
|
+
envIsForwarded,
|
|
59
|
+
stripNoForwardFlag,
|
|
60
|
+
} from './bootstrap-forward.js';
|
|
61
|
+
|
|
38
62
|
export type {ConfigOverrideMap} from './config-override.js';
|
|
39
63
|
export {
|
|
40
64
|
defaultConfigOverridePath,
|
package/src/install-ci-core.ts
CHANGED
|
@@ -828,54 +828,26 @@ ${indent(modelsJsonStr, 8)}
|
|
|
828
828
|
run: npm install -g dorfl${installHarness}`;
|
|
829
829
|
}
|
|
830
830
|
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
// and
|
|
843
|
-
//
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
shell: bash
|
|
852
|
-
run: |
|
|
853
|
-
global_dorfl="$(command -v dorfl || true)"
|
|
854
|
-
if [ -z "$global_dorfl" ]; then
|
|
855
|
-
echo "ERROR: dorfl bootstrap not found on PATH after install" >&2
|
|
856
|
-
exit 1
|
|
857
|
-
fi
|
|
858
|
-
shim_dir="\${RUNNER_TEMP:-/tmp}/dorfl-resolver"
|
|
859
|
-
mkdir -p "$shim_dir"
|
|
860
|
-
printf '%s\\n' "$global_dorfl" > "$shim_dir/global-path"
|
|
861
|
-
# Write the shim via printf (not a heredoc) so the shebang lands at
|
|
862
|
-
# column 0 — a YAML \`run: |\` block's common-indent would otherwise
|
|
863
|
-
# prefix every heredoc line and break the shebang.
|
|
864
|
-
printf '%s\\n' \\
|
|
865
|
-
'#!/usr/bin/env bash' \\
|
|
866
|
-
'# Prefer the project-pinned dorfl (devDep, populated by the' \\
|
|
867
|
-
"# project's dependency install) over the global bootstrap." \\
|
|
868
|
-
'# Generated by install-ci.' \\
|
|
869
|
-
'local_bin="\${GITHUB_WORKSPACE:-$PWD}/node_modules/.bin/dorfl"' \\
|
|
870
|
-
'if [ -x "$local_bin" ]; then' \\
|
|
871
|
-
' exec "$local_bin" "$@"' \\
|
|
872
|
-
'fi' \\
|
|
873
|
-
'global_path_file="$(dirname "$0")/global-path"' \\
|
|
874
|
-
'exec "$(cat "$global_path_file")" "$@"' \\
|
|
875
|
-
> "$shim_dir/dorfl"
|
|
876
|
-
chmod +x "$shim_dir/dorfl"
|
|
877
|
-
echo "$shim_dir" >> "$GITHUB_PATH"`;
|
|
878
|
-
installSteps = installSteps + resolverStep;
|
|
831
|
+
// NO bespoke CI resolver shim (task install-ci-shim-converges-on-dorfl-cmd,
|
|
832
|
+
// spec `dorfl-self-version-pinning-and-bootstrap-forward` §6 / story 4).
|
|
833
|
+
// Historically this action emitted a `$PATH` shim that preferred a
|
|
834
|
+
// project-local `node_modules/.bin/dorfl` over the global bootstrap (task
|
|
835
|
+
// install-ci-prefer-project-local-dorfl). That shim was CI-only AND
|
|
836
|
+
// JS-specific (it hardcoded `node_modules/.bin`). Now that the global
|
|
837
|
+
// bootstrap dorfl SELF-FORWARDS to the repo-declared `dorflCmd` on its own
|
|
838
|
+
// (task `dorfl-bootstrap-self-forward`), the shim is redundant: CI's global
|
|
839
|
+
// `dorfl` forwards to the repo's declared `dorflCmd` by the SAME generic
|
|
840
|
+
// mechanism the laptop uses — ONE code path, JS and non-JS alike. A JS repo
|
|
841
|
+
// that pinned via a devDep declares `dorflCmd: "node_modules/.bin/dorfl"`
|
|
842
|
+
// (one line; `setup` nudges it) and gets the pin in CI via the forward. A
|
|
843
|
+
// repo with NO `dorflCmd` runs the global bootstrap — identically on CI and
|
|
844
|
+
// the laptop (onboarding-safe). Keeping a JS-only no-`dorflCmd` fallback was
|
|
845
|
+
// deliberately REJECTED (see the ## Decisions note in the done record): it
|
|
846
|
+
// would re-introduce the JS-specific CI-only special case the convergence
|
|
847
|
+
// removes, AND when a repo declares BOTH the devDep AND `dorflCmd` the shim
|
|
848
|
+
// would exec the local bin which then forwards AGAIN — a confusing
|
|
849
|
+
// double-resolution. The install step below leaves `dorfl` on `$PATH` as the
|
|
850
|
+
// bootstrap; the forward does the pinning.
|
|
879
851
|
|
|
880
852
|
// One optional ACTION INPUT per provider key (models-json mode), named
|
|
881
853
|
// identically to the secret/env var. Optional + default '' so a workflow that
|
package/src/integration-core.ts
CHANGED
|
@@ -2256,6 +2256,16 @@ function bridgeProvider(
|
|
|
2256
2256
|
req.body,
|
|
2257
2257
|
};
|
|
2258
2258
|
},
|
|
2259
|
+
// The legacy bridge has no PR-close capability — a clean no-op (keep the
|
|
2260
|
+
// branch), surfacing the disapproving review text.
|
|
2261
|
+
async closeRequestOnBranch(req) {
|
|
2262
|
+
return {
|
|
2263
|
+
closed: false,
|
|
2264
|
+
instruction:
|
|
2265
|
+
'The legacy review bridge cannot close a PR (branch kept); the ' +
|
|
2266
|
+
`disapproving review:\n${req.comment}`,
|
|
2267
|
+
};
|
|
2268
|
+
},
|
|
2259
2269
|
};
|
|
2260
2270
|
}
|
|
2261
2271
|
|
package/src/integrator.ts
CHANGED
|
@@ -81,6 +81,52 @@ export interface ReviewProvider {
|
|
|
81
81
|
* review — ADR §6).
|
|
82
82
|
*/
|
|
83
83
|
postPRCommentOnBranch(input: PostPRCommentOnBranchInput): PostPRCommentResult;
|
|
84
|
+
/**
|
|
85
|
+
* CLOSE the PR opened for an already-pushed `work/<slug>` BRANCH — WITHOUT
|
|
86
|
+
* deleting the branch — posting {@link CloseRequestOnBranchInput.comment} (the
|
|
87
|
+
* review that disapproved) as the closing comment. RESOLVES the PR from the
|
|
88
|
+
* branch (the disapprove path holds the branch, not the PR url).
|
|
89
|
+
*
|
|
90
|
+
* Used when a tasking review DISAPPROVES a spec whose PR is already open (the
|
|
91
|
+
* multi-run artefact): the stale PR is closed with the review as its closing
|
|
92
|
+
* comment (so the reason is visible ON the PR), while the branch is KEPT — the
|
|
93
|
+
* safety-bearing recovery point — so a later re-task that APPROVES can REOPEN
|
|
94
|
+
* the same PR (via {@link openRequest}, which reopens an existing-closed PR).
|
|
95
|
+
*
|
|
96
|
+
* ONLY-IF-EXISTS: a real provider first resolves the branch's PR and, when
|
|
97
|
+
* there is NO open PR to close, cleanly no-ops (`closed: false`) — the disapprove
|
|
98
|
+
* path never OPENS a PR just to close it.
|
|
99
|
+
*
|
|
100
|
+
* ADVISORY — it gates nothing; like the other PR-text methods it must NEVER
|
|
101
|
+
* throw (a missing/unauthenticated `gh`, the `none` provider, or no resolvable
|
|
102
|
+
* open PR all DEGRADE: surface the text in the result, close nothing, keep the
|
|
103
|
+
* branch — ADR §6).
|
|
104
|
+
*/
|
|
105
|
+
closeRequestOnBranch(
|
|
106
|
+
input: CloseRequestOnBranchInput,
|
|
107
|
+
): Promise<CloseRequestOnBranchResult>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface CloseRequestOnBranchInput {
|
|
111
|
+
cwd: string;
|
|
112
|
+
/**
|
|
113
|
+
* The pushed `work/<slug>` branch whose OPEN PR to close (keeping the branch).
|
|
114
|
+
* The GitHub provider resolves the PR via `gh pr view <branch>` and closes it
|
|
115
|
+
* with `gh pr close <branch>` (NO `--delete-branch`).
|
|
116
|
+
*/
|
|
117
|
+
branch: string;
|
|
118
|
+
/** The arbiter remote the branch was pushed to. */
|
|
119
|
+
arbiter: string;
|
|
120
|
+
/** The closing comment — the disapproving review prose (JSON block stripped). */
|
|
121
|
+
comment: string;
|
|
122
|
+
env?: NodeJS.ProcessEnv;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface CloseRequestOnBranchResult {
|
|
126
|
+
/** True iff an open PR was actually resolved from the branch AND closed. */
|
|
127
|
+
closed: boolean;
|
|
128
|
+
/** Human-readable confirmation / fallback (the review text on degrade/no-op). */
|
|
129
|
+
instruction: string;
|
|
84
130
|
}
|
|
85
131
|
|
|
86
132
|
export interface PostPRCommentInput {
|
|
@@ -216,6 +262,22 @@ export class NoneProvider implements ReviewProvider {
|
|
|
216
262
|
`comment. The review:\n${input.body}`,
|
|
217
263
|
};
|
|
218
264
|
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* No API to resolve/close a PR (a local `--bare` arbiter has no review
|
|
268
|
+
* concept), so DEGRADE: close nothing, keep the branch, surface the review
|
|
269
|
+
* text, never throw. A clean no-op for the PR.
|
|
270
|
+
*/
|
|
271
|
+
async closeRequestOnBranch(
|
|
272
|
+
input: CloseRequestOnBranchInput,
|
|
273
|
+
): Promise<CloseRequestOnBranchResult> {
|
|
274
|
+
return {
|
|
275
|
+
closed: false,
|
|
276
|
+
instruction:
|
|
277
|
+
'No review provider configured — no PR was closed (the branch is ' +
|
|
278
|
+
`kept). The disapproving review:\n${input.comment}`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
219
281
|
}
|
|
220
282
|
|
|
221
283
|
export interface IntegrateInput {
|
package/src/item-lock.ts
CHANGED
|
@@ -1940,6 +1940,56 @@ export async function heldTaskSlugsStrict(
|
|
|
1940
1940
|
);
|
|
1941
1941
|
}
|
|
1942
1942
|
|
|
1943
|
+
/**
|
|
1944
|
+
* List the SPEC slugs currently lock-held on the arbiter — the held-SPEC set the
|
|
1945
|
+
* TASKABLE-spec pool readers SUBTRACT (fix
|
|
1946
|
+
* `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`).
|
|
1947
|
+
* The SPEC analogue of {@link heldTaskSlugsStrict}: enumerates {@link listItemLocks}
|
|
1948
|
+
* and keeps only the `spec-<slug>` entries (a task/observation lock does not gate
|
|
1949
|
+
* the SPEC pool), mapping each to its bare `<slug>`.
|
|
1950
|
+
*
|
|
1951
|
+
* LOAD-BEARING for the propose-mode tasking loop: an `advance spec:<slug> --propose`
|
|
1952
|
+
* that opens a PR now KEEPS the `spec:<slug>` lock HELD across the open PR (the
|
|
1953
|
+
* durable `specs/ready → specs/tasked` move lives only on the branch, so `main`
|
|
1954
|
+
* residence does NOT yet signal tasked-ness). This held-spec set is the ONLY thing
|
|
1955
|
+
* that keeps such an in-flight spec out of the taskable pool — WITHOUT it the spec
|
|
1956
|
+
* is re-tasked every tick and its PR force-pushed. Symmetric to the task-pool
|
|
1957
|
+
* subtraction; the same fail-open/fail-closed split applies (see
|
|
1958
|
+
* {@link heldSpecSlugs} vs this strict twin).
|
|
1959
|
+
*/
|
|
1960
|
+
export async function heldSpecSlugsStrict(
|
|
1961
|
+
cwd: string,
|
|
1962
|
+
arbiter = 'origin',
|
|
1963
|
+
env?: NodeJS.ProcessEnv,
|
|
1964
|
+
): Promise<Set<string>> {
|
|
1965
|
+
const entries = await listItemLocks(cwd, arbiter, env);
|
|
1966
|
+
const prefix = 'spec-';
|
|
1967
|
+
return new Set(
|
|
1968
|
+
entries
|
|
1969
|
+
.filter((e) => e.startsWith(prefix))
|
|
1970
|
+
.map((e) => e.slice(prefix.length)),
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/**
|
|
1975
|
+
* GRACEFUL (fail-OPEN) twin of {@link heldSpecSlugsStrict} for read-only SURFACE
|
|
1976
|
+
* paths and the local autopick driver: a fetch fault yields an EMPTY set rather
|
|
1977
|
+
* than throwing (the follow-on tasking-lock CAS is the load-bearing safety net
|
|
1978
|
+
* locally, exactly as {@link heldTaskSlugs} relies on the claim CAS). The SELECTION
|
|
1979
|
+
* path that must refuse an untrusted pool uses the strict twin.
|
|
1980
|
+
*/
|
|
1981
|
+
export async function heldSpecSlugs(
|
|
1982
|
+
cwd: string,
|
|
1983
|
+
arbiter = 'origin',
|
|
1984
|
+
env?: NodeJS.ProcessEnv,
|
|
1985
|
+
): Promise<Set<string>> {
|
|
1986
|
+
try {
|
|
1987
|
+
return await heldSpecSlugsStrict(cwd, arbiter, env);
|
|
1988
|
+
} catch {
|
|
1989
|
+
return new Set();
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1943
1993
|
/**
|
|
1944
1994
|
* Parse a serialised lock entry body back into a {@link LockEntry} — the exact
|
|
1945
1995
|
* inverse of {@link serialiseLockEntry}. Post-`retire-stuck-lock-state` a lock
|
package/src/repo-config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {join} from 'node:path';
|
|
|
3
3
|
import {
|
|
4
4
|
mergeConfig,
|
|
5
5
|
validateDeadlineConfig,
|
|
6
|
+
validateDorflCmdConfig,
|
|
6
7
|
warnDeprecatedConfigKeys,
|
|
7
8
|
type Config,
|
|
8
9
|
type PartialConfig,
|
|
@@ -110,6 +111,20 @@ export const REPO_ALLOWED_KEYS = [
|
|
|
110
111
|
// through the SAME chain as `verify`. Install belongs HERE, never baked into
|
|
111
112
|
// `verify`.
|
|
112
113
|
'prepare',
|
|
114
|
+
// `dorflCmd` (the repo-declared dorfl COMMAND bare `dorfl` self-forwards to —
|
|
115
|
+
// spec `dorfl-self-version-pinning-and-bootstrap-forward` §1/§3) is a genuine
|
|
116
|
+
// repo property exactly like `verify`/`prepare`: WHICH dorfl this repo builds/
|
|
117
|
+
// advances/intakes with is agreed by all collaborators + travels with the repo,
|
|
118
|
+
// for reproducibility. It is the DELIBERATE, ADR-recorded EXCEPTION to ADR §13's
|
|
119
|
+
// host-only rule: a machine-command key (same class as the REJECTED
|
|
120
|
+
// `agentCmd`/`piBin`/`sessionsDir` below) that IS repo-settable, because its
|
|
121
|
+
// purpose is repo-declared reproducibility, it carries no more trust than the
|
|
122
|
+
// committed `verify` command the repo already runs, and the forward is ANNOUNCED
|
|
123
|
+
// on stderr (unlike a silent `piBin`). There is NO trust gate. See ADR
|
|
124
|
+
// `dorfl-cmd-repo-settable-exception-to-host-only` for the full why + the
|
|
125
|
+
// reversal of §13 for this one key. Resolved per-repo through the SAME chain as
|
|
126
|
+
// `verify` (flag > env `DORFL_DORFL_CMD` > per-repo > global > default unset).
|
|
127
|
+
'dorflCmd',
|
|
113
128
|
'defaultArbiter',
|
|
114
129
|
// `autoBuild` (may an agent auto-BUILD undeclared, not-`humanOnly` tasks in
|
|
115
130
|
// this repo?) is a genuine repo property — the build member of the symmetric
|
|
@@ -591,6 +606,11 @@ export function resolveRepoConfigFromLoaded(
|
|
|
591
606
|
// (flag / env / per-repo / global) throws with a clear message naming the
|
|
592
607
|
// field + range — NEVER silently clamped.
|
|
593
608
|
validateDeadlineConfig(config);
|
|
609
|
+
// Validate + normalise the repo-declared dorfl command (trim; empty ⇒ unset;
|
|
610
|
+
// non-string ⇒ fail-loud) after layering, so a malformed value from ANY layer
|
|
611
|
+
// (flag / env / per-repo / global) surfaces the same clear error (ADR
|
|
612
|
+
// `dorfl-cmd-repo-settable-exception-to-host-only`).
|
|
613
|
+
validateDorflCmdConfig(config);
|
|
594
614
|
return {
|
|
595
615
|
config,
|
|
596
616
|
rejected: repo.rejected,
|
package/src/scan.ts
CHANGED
|
@@ -252,16 +252,30 @@ export function scoreSpecs(
|
|
|
252
252
|
repoPath: string,
|
|
253
253
|
pool: LedgerSpecPool,
|
|
254
254
|
autoTask: boolean,
|
|
255
|
+
/**
|
|
256
|
+
* The held-SPEC set to SUBTRACT from the taskable pool (fix
|
|
257
|
+
* `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`):
|
|
258
|
+
* a spec whose `refs/dorfl/lock/spec-<slug>` is HELD is IN-FLIGHT (a propose-mode
|
|
259
|
+
* tasking PR is open, or a merge tasking is mid-flight) and must NOT be enumerated
|
|
260
|
+
* as taskable — otherwise CI re-tasks it every tick, force-pushing the open PR.
|
|
261
|
+
* Symmetric to {@link scoreItems}'s task held-slug subtraction (specs took NONE
|
|
262
|
+
* before this fix). Defaults empty (offline read — the caller supplies the set,
|
|
263
|
+
* fail-CLOSED for selection via {@link heldSpecSlugsStrict}, graceful for the
|
|
264
|
+
* surface).
|
|
265
|
+
*/
|
|
266
|
+
heldSpecSlugs: Set<string> = new Set(),
|
|
255
267
|
): ScannedSpec[] {
|
|
256
268
|
const taskable = new Set(
|
|
257
269
|
taskableSpecs({
|
|
258
|
-
candidates: pool.specs
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
candidates: pool.specs
|
|
271
|
+
.filter((p) => !heldSpecSlugs.has(p.slug))
|
|
272
|
+
.map((p) => ({
|
|
273
|
+
repoPath,
|
|
274
|
+
slug: p.slug,
|
|
275
|
+
humanOnly: p.humanOnly,
|
|
276
|
+
needsAnswers: p.needsAnswers,
|
|
277
|
+
taskedAfter: p.taskedAfter,
|
|
278
|
+
})),
|
|
265
279
|
taskedSlugs: pool.taskedSlugs,
|
|
266
280
|
autoTask,
|
|
267
281
|
}).map((p) => p.slug),
|
|
@@ -567,6 +581,16 @@ export function scanRepoPaths(
|
|
|
567
581
|
* empty (no override).
|
|
568
582
|
*/
|
|
569
583
|
override?: ConfigOverrideMap,
|
|
584
|
+
/**
|
|
585
|
+
* The held-SPEC set to SUBTRACT from each repo's TASKABLE-spec pool (fix
|
|
586
|
+
* `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`):
|
|
587
|
+
* passed into {@link scoreSpecs} so a spec whose per-item lock is HELD (a
|
|
588
|
+
* propose-mode tasking PR is open, or a merge tasking is mid-flight) never leaks
|
|
589
|
+
* into the propose matrix's `spec:` legs — the SPEC counterpart of the `heldSlugs`
|
|
590
|
+
* task subtraction. Supplied by the in-place caller (offline scan has no arbiter
|
|
591
|
+
* handle); DEFAULTS empty.
|
|
592
|
+
*/
|
|
593
|
+
heldSpecSlugs: Set<string> = new Set(),
|
|
570
594
|
): ScanReport {
|
|
571
595
|
const repos: RepoReport[] = [];
|
|
572
596
|
const counts = {totalItems: 0, totalEligible: 0};
|
|
@@ -585,7 +609,7 @@ export function scanRepoPaths(
|
|
|
585
609
|
// predicate. This is what makes the propose-mode CI matrix enumerate `spec:`
|
|
586
610
|
// legs (see `ci-propose-matrix-must-enumerate-sliceable-prds-not-only-slices`).
|
|
587
611
|
const specPool = ledgerRead.resolveSpecPool({repoPath: path});
|
|
588
|
-
const specs = scoreSpecs(path, specPool, resolved.autoTask);
|
|
612
|
+
const specs = scoreSpecs(path, specPool, resolved.autoTask, heldSpecSlugs);
|
|
589
613
|
// The per-repo LIFECYCLE pool (`ci-propose-matrix-enumerates-lifecycle-items`),
|
|
590
614
|
// gated by this working tree's `observationTriage` / `surfaceBlockers` (resolved
|
|
591
615
|
// the same way as `autoBuild`/`autoTask`) and computed by REUSING
|