dorfl 0.10.1 → 0.11.1
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/cli.d.ts.map +1 -1
- package/dist/cli.js +99 -21
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +73 -12
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -1
- package/dist/do-config.d.ts +21 -1
- package/dist/do-config.d.ts.map +1 -1
- package/dist/do-config.js +19 -0
- package/dist/do-config.js.map +1 -1
- package/dist/do.d.ts +3 -1
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js.map +1 -1
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +3 -0
- package/dist/env-config.js.map +1 -1
- package/dist/gc.d.ts +42 -0
- package/dist/gc.d.ts.map +1 -1
- package/dist/gc.js +71 -1
- package/dist/gc.js.map +1 -1
- package/dist/protocol/WORK-CONTRACT.md +11 -2
- package/dist/protocol/spec-template.md +1 -1
- package/dist/protocol/task-template.md +1 -1
- package/dist/repo-config.d.ts +1 -1
- package/dist/repo-config.d.ts.map +1 -1
- package/dist/repo-config.js +19 -2
- package/dist/repo-config.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +4 -3
- package/dist/run.js.map +1 -1
- package/dist/skills/drive-tasks/SKILL.md +21 -3
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +11 -2
- package/dist/skills/setup/protocol/spec-template.md +1 -1
- package/dist/skills/setup/protocol/task-template.md +1 -1
- package/package.json +1 -1
- package/src/cli.ts +143 -21
- package/src/config.ts +90 -12
- package/src/do-config.ts +38 -0
- package/src/do.ts +3 -1
- package/src/env-config.ts +3 -0
- package/src/gc.ts +108 -1
- package/src/repo-config.ts +19 -1
- package/src/run.ts +4 -3
package/src/gc.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {existsSync, lstatSync, readdirSync, rmSync, statSync} from 'node:fs';
|
|
2
2
|
import {join} from 'node:path';
|
|
3
3
|
import {git, run} from './git.js';
|
|
4
|
+
import {encodeRepoKey, readOriginUrl} from './repo-mirror.js';
|
|
4
5
|
import {
|
|
5
6
|
JOB_RECORD_FILENAME,
|
|
6
7
|
jobRecordPath,
|
|
@@ -251,6 +252,17 @@ export interface RetainedJob extends GcJob {
|
|
|
251
252
|
export interface GcOptions {
|
|
252
253
|
/** The execution working area (config `workspacesDir`, default `~/.dorfl`). */
|
|
253
254
|
workspacesDir: string;
|
|
255
|
+
/**
|
|
256
|
+
* SCOPE the sweep to a SINGLE arbiter: only worktrees whose hub mirror's
|
|
257
|
+
* `origin` encodes to this hub key ({@link encodeRepoKey}) are considered.
|
|
258
|
+
* This is the DEFAULT operating mode of the `gc` CLI (the arbiter resolved
|
|
259
|
+
* from the cwd) — a `gc` run from repo A must NEVER touch repo B's worktrees.
|
|
260
|
+
*
|
|
261
|
+
* When UNSET the sweep is GLOBAL (every arbiter under `workspacesDir/work/*`).
|
|
262
|
+
* Global is the destructive, cross-repo mode: the CLI only reaches it behind
|
|
263
|
+
* the explicit + loud `--all-arbiters` flag, NEVER by defaulting here.
|
|
264
|
+
*/
|
|
265
|
+
arbiterKey?: string;
|
|
254
266
|
/**
|
|
255
267
|
* Override the predicate for EVERY job: discard un-saved work. Loud +
|
|
256
268
|
* explicit (the CLI guards it behind a confirmation) — NEVER the default.
|
|
@@ -295,20 +307,42 @@ export interface GcResult {
|
|
|
295
307
|
* `force: true` overrides the predicate for every job (discard un-saved work) —
|
|
296
308
|
* loud + explicit (the CLI confirms first), NEVER the default. A job whose work
|
|
297
309
|
* is NOT on the arbiter is never auto-removed without `force`.
|
|
310
|
+
*
|
|
311
|
+
* SCOPE: when `arbiterKey` is set the sweep considers ONLY worktrees belonging
|
|
312
|
+
* to that arbiter (the CLI's default, resolved from the cwd) — so a `gc` in repo
|
|
313
|
+
* A cannot reap repo B's worktrees. An UNSET `arbiterKey` sweeps GLOBALLY across
|
|
314
|
+
* every arbiter (the CLI reaches that only behind the loud `--all-arbiters`).
|
|
298
315
|
*/
|
|
299
316
|
export function gc(options: GcOptions): GcResult {
|
|
300
317
|
const note = options.note ?? (() => {});
|
|
301
318
|
const env = options.env;
|
|
319
|
+
const arbiterKey = options.arbiterKey;
|
|
302
320
|
const reaped: ReapedJob[] = [];
|
|
303
321
|
const retained: RetainedJob[] = [];
|
|
304
322
|
|
|
305
323
|
// First, self-heal any record-less ORPHAN `work/*` entry (a dangling symlink
|
|
306
324
|
// or a dir git never registered) so a half-set-up claim does not linger unseen
|
|
307
|
-
// by the job loop below and wedge the next `worktree add`.
|
|
325
|
+
// by the job loop below and wedge the next `worktree add`. Orphans carry NO
|
|
326
|
+
// durable work and are NOT any repo's real worktree (no record ⇒ no mirror to
|
|
327
|
+
// attribute to an arbiter), so the arbiter scope does not gate them — they are
|
|
328
|
+
// bounded, worthless residue either way.
|
|
308
329
|
const sweptOrphans = sweepOrphans(options.workspacesDir, note);
|
|
309
330
|
|
|
310
331
|
for (const job of discoverJobs(options.workspacesDir)) {
|
|
311
332
|
const mirrorPath = resolveMirrorPath(options.workspacesDir, job);
|
|
333
|
+
|
|
334
|
+
// ARBITER SCOPE (the default `gc` mode): when `arbiterKey` is set, consider
|
|
335
|
+
// ONLY worktrees whose mirror `origin` encodes to that hub key — a `gc` run
|
|
336
|
+
// from repo A must never reap repo B's worktrees. A job whose mirror origin
|
|
337
|
+
// is unreadable (a malformed mirror) is SKIPPED under scope (the safe
|
|
338
|
+
// direction: we cannot prove it belongs to this arbiter, so leave it).
|
|
339
|
+
if (
|
|
340
|
+
arbiterKey !== undefined &&
|
|
341
|
+
jobArbiterKey(mirrorPath, env) !== arbiterKey
|
|
342
|
+
) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
312
346
|
const result = reapJob({
|
|
313
347
|
dir: job.dir,
|
|
314
348
|
branch: job.branch,
|
|
@@ -479,6 +513,79 @@ function resolveMirrorPath(workspacesDir: string, job: GcJob): string {
|
|
|
479
513
|
return join(workspacesDir, 'repos');
|
|
480
514
|
}
|
|
481
515
|
|
|
516
|
+
/**
|
|
517
|
+
* The hub KEY ({@link encodeRepoKey}) of the arbiter a job worktree belongs to,
|
|
518
|
+
* read from its hub mirror's `origin` URL (the arbiter URL the mirror was cloned
|
|
519
|
+
* from). This is the SAME identity `do --isolated` / `ensureMirror` key a repo's
|
|
520
|
+
* arbiter under (`mirrorPath` = `<workspacesDir>/repos/<key>.git`), so a `gc`
|
|
521
|
+
* scoped to the cwd's arbiter key matches exactly the worktrees cut for that
|
|
522
|
+
* arbiter. Returns `undefined` when the mirror `origin` cannot be read (a
|
|
523
|
+
* malformed mirror) — the caller then treats the job as NOT belonging to the
|
|
524
|
+
* scoped arbiter (the safe direction under a scoped sweep).
|
|
525
|
+
*/
|
|
526
|
+
function jobArbiterKey(
|
|
527
|
+
mirrorPath: string,
|
|
528
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
529
|
+
): string | undefined {
|
|
530
|
+
const originUrl = readOriginUrl(mirrorPath, env);
|
|
531
|
+
return originUrl === undefined ? undefined : encodeRepoKey(originUrl);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** One distinct arbiter with worktrees under the work area (for the loud
|
|
535
|
+
* `--all-arbiters` banner). */
|
|
536
|
+
export interface WorkspaceArbiter {
|
|
537
|
+
/** The hub key ({@link encodeRepoKey}) — or `undefined` for a malformed mirror. */
|
|
538
|
+
key?: string;
|
|
539
|
+
/** The arbiter `origin` URL of the hub mirror, when readable. */
|
|
540
|
+
url?: string;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Enumerate the DISTINCT arbiters that currently own a worktree under
|
|
545
|
+
* `<workspacesDir>/work/*` — i.e. the set a GLOBAL (`--all-arbiters`) sweep would
|
|
546
|
+
* touch. Reads each discovered job's hub mirror `origin` URL and de-duplicates
|
|
547
|
+
* by hub key. Used ONLY to print the loud banner before a cross-arbiter sweep;
|
|
548
|
+
* the sweep itself re-derives per-job scope in {@link gc}.
|
|
549
|
+
*/
|
|
550
|
+
export function enumerateWorkspaceArbiters(
|
|
551
|
+
workspacesDir: string,
|
|
552
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
553
|
+
): WorkspaceArbiter[] {
|
|
554
|
+
const seen = new Map<string, WorkspaceArbiter>();
|
|
555
|
+
for (const job of discoverJobs(workspacesDir)) {
|
|
556
|
+
const mirrorPath = resolveMirrorPath(workspacesDir, job);
|
|
557
|
+
const url = readOriginUrl(mirrorPath, env);
|
|
558
|
+
const key = url === undefined ? undefined : encodeRepoKey(url);
|
|
559
|
+
const dedupe = key ?? `path:${mirrorPath}`;
|
|
560
|
+
if (!seen.has(dedupe)) {
|
|
561
|
+
seen.set(dedupe, {key, url});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return [...seen.values()];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The hub KEY ({@link encodeRepoKey}) of the arbiter reachable from a working
|
|
569
|
+
* repo `cwd` via its `<arbiter>` remote (default `origin`), i.e. `encodeRepoKey`
|
|
570
|
+
* of `git -C <cwd> remote get-url <arbiter>`. This is how the `gc` CLI resolves
|
|
571
|
+
* the DEFAULT scope from the current directory — the SAME arbiter-resolution the
|
|
572
|
+
* mirror / `do --isolated` path uses (`ensureMirror`'s `fromRepo` + `arbiter`).
|
|
573
|
+
* Returns `undefined` when `cwd` is not a git repo or has no such remote (the
|
|
574
|
+
* CLI then errors rather than silently going global).
|
|
575
|
+
*/
|
|
576
|
+
export function resolveArbiterKeyFromCwd(
|
|
577
|
+
cwd: string,
|
|
578
|
+
arbiter: string,
|
|
579
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
580
|
+
): string | undefined {
|
|
581
|
+
const res = run('git', ['remote', 'get-url', arbiter], cwd, {env});
|
|
582
|
+
if (res.status !== 0) {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
const url = res.stdout.trim();
|
|
586
|
+
return url === '' ? undefined : encodeRepoKey(url);
|
|
587
|
+
}
|
|
588
|
+
|
|
482
589
|
/**
|
|
483
590
|
* True iff the working tree has NO uncommitted WORK (tracked, staged, or
|
|
484
591
|
* untracked). The agent's edits and any untracked artefacts count: untracked
|
package/src/repo-config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {readFileSync, existsSync} from 'node:fs';
|
|
|
2
2
|
import {join} from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
mergeConfig,
|
|
5
|
+
applyModelFallbacks,
|
|
5
6
|
validateDeadlineConfig,
|
|
6
7
|
validateDorflCmdConfig,
|
|
7
8
|
warnDeprecatedConfigKeys,
|
|
@@ -209,8 +210,12 @@ export const REPO_ALLOWED_KEYS = [
|
|
|
209
210
|
// `model` (which model this repo's work runs on) and `harness` (which adapter)
|
|
210
211
|
// are legitimate repo properties (ADR §13) — model is routing intent, not auth,
|
|
211
212
|
// and a repo may prefer a given harness. `piBin`/`agentCmd` stay host-only
|
|
212
|
-
// (machine paths/commands), so they are rejected below.
|
|
213
|
+
// (machine paths/commands), so they are rejected below. `buildModel` is the
|
|
214
|
+
// builder-specific override of the general `model` (falls back to `model` when
|
|
215
|
+
// unset at all levels — see `applyModelFallbacks`); like `model` it is routing
|
|
216
|
+
// intent (not auth), so it is repo-appropriate.
|
|
213
217
|
'model',
|
|
218
|
+
'buildModel',
|
|
214
219
|
'harness',
|
|
215
220
|
// Gate 2 (PR/code review) policy is a genuine repo property (GATES spec
|
|
216
221
|
// `work/specs/tasked/review.md`), resolved per-repo like `integration`/`autoBuild`:
|
|
@@ -230,6 +235,14 @@ export const REPO_ALLOWED_KEYS = [
|
|
|
230
235
|
'taskerLoop',
|
|
231
236
|
'taskerLoopMax',
|
|
232
237
|
'taskerLoopModel',
|
|
238
|
+
// `triageModel` (the model the advance lifecycle gates — surface, apply,
|
|
239
|
+
// triage — run on) is a genuine repo property like `reviewModel`/
|
|
240
|
+
// `taskerLoopModel`: routing intent (not auth), resolved per-repo through the
|
|
241
|
+
// same chain. `intakeModel` (the front-door intake decision agent's model) is
|
|
242
|
+
// likewise repo-appropriate. Each falls back to `model` when unset at all
|
|
243
|
+
// levels (see `applyModelFallbacks`).
|
|
244
|
+
'triageModel',
|
|
245
|
+
'intakeModel',
|
|
233
246
|
// `freshWorktreeGate` (run the acceptance gate against the REBASED tip in a
|
|
234
247
|
// clean throwaway worktree, ON by default) is a genuine repo property exactly
|
|
235
248
|
// like `verify`/`prepare`/`review`: whether this repo's gate tests the merged
|
|
@@ -646,6 +659,11 @@ export function resolveRepoConfigFromLoaded(
|
|
|
646
659
|
// (flag / env / per-repo / global) surfaces the same clear error (ADR
|
|
647
660
|
// `dorfl-cmd-repo-settable-exception-to-host-only`).
|
|
648
661
|
validateDorflCmdConfig(config);
|
|
662
|
+
// Apply the per-role model fallbacks: `buildModel`, `reviewModel`, and
|
|
663
|
+
// `taskerLoopModel` each inherit the general `model` when unset at EVERY level
|
|
664
|
+
// (flag / env / per-repo / global). Applied HERE — the resolution FINAL point —
|
|
665
|
+
// so the fallback uses the FULLY resolved `model`, never an intermediate layer.
|
|
666
|
+
applyModelFallbacks(config);
|
|
649
667
|
return {
|
|
650
668
|
config,
|
|
651
669
|
rejected: repo.rejected,
|
package/src/run.ts
CHANGED
|
@@ -834,8 +834,9 @@ async function runOneItem(
|
|
|
834
834
|
|
|
835
835
|
// 4. Run the agent — via the injected runner (tests) or the harness seam
|
|
836
836
|
// (null adapter by default), shelling out to the configured agentCmd. The
|
|
837
|
-
// resolved per-repo `
|
|
838
|
-
//
|
|
837
|
+
// resolved per-repo `buildModel` (ADR §13, which inherits the general
|
|
838
|
+
// `model` when unset) flows through the seam to the adapter; a
|
|
839
|
+
// `{model}`-in-agentCmd misconfiguration surfaces as agent-failed.
|
|
839
840
|
let agent: {ok: boolean; detail?: string; output?: string};
|
|
840
841
|
try {
|
|
841
842
|
agent = runAgent(
|
|
@@ -844,7 +845,7 @@ async function runOneItem(
|
|
|
844
845
|
prompt,
|
|
845
846
|
slug,
|
|
846
847
|
config.agentCmd,
|
|
847
|
-
config.
|
|
848
|
+
config.buildModel,
|
|
848
849
|
config.sessionsDir,
|
|
849
850
|
);
|
|
850
851
|
} catch (err) {
|