@dev-loops/core 1.0.0-rc.1 → 1.0.0-rc.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-loops/core",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -13,11 +13,11 @@ import { z } from "zod";
13
13
  // ============================================================================
14
14
 
15
15
  const StrategyConfig = z.strictObject({
16
- default: z.enum(["local-first", "github-first"]),
16
+ default: z.enum(["local-first", "github-first"]).describe("Default work-intake strategy: local-first starts from a repo plan file, github-first from a tracked issue."),
17
17
  });
18
18
 
19
19
  const InputSourceConfig = z.strictObject({
20
- default: z.enum(["tracker", "phase-docs"]),
20
+ default: z.enum(["tracker", "phase-docs"]).describe("Where local-first work reads its spec: the tracker issue body, or repo phase docs."),
21
21
  });
22
22
 
23
23
  // Built-in tier aliases shipped with zero config. A tier alias maps a
@@ -82,44 +82,45 @@ function refineRoleTiers(models, ctx) {
82
82
  }
83
83
 
84
84
  const ModelsConfigBase = z.strictObject({
85
- conductor: z.string().trim().min(1).optional(),
86
- roles: z.record(z.string(), z.string().trim().min(1)).optional(),
85
+ conductor: z.string().trim().min(1).describe("Model override for the conductor (dev-loop) session; absent = inherit the session model.").optional(),
86
+ roles: z.record(z.string(), z.string().trim().min(1)).describe("Concrete per-role/angle model overrides (highest precedence, above tiers).").optional(),
87
87
  // Tier alias → per-harness concrete model (null = inherit / no-op).
88
- tiers: z.record(z.string().min(1), ModelTierMapping).optional(),
88
+ tiers: z.record(z.string().min(1), ModelTierMapping).describe("Tier alias → per-harness concrete model; null on a harness means inherit (no override).").optional(),
89
89
  // Role / angle → tier alias (a built-in/custom alias or "inherit").
90
- roleTiers: z.record(z.string().min(1), z.string().trim().min(1)).optional(),
90
+ roleTiers: z.record(z.string().min(1), z.string().trim().min(1)).describe("Role or gate angle → tier alias: a built-in alias (low, high), a custom models.tiers alias, or \"inherit\".").optional(),
91
91
  });
92
92
 
93
93
  const ModelsConfig = ModelsConfigBase.superRefine(refineRoleTiers);
94
94
 
95
95
  const RefinementConfig = z.strictObject({
96
- fanOut: z.number().int().min(1).max(10),
97
- mode: z.enum(["parallel", "sequential"]),
98
- maxCopilotRounds: z.number().int().nonnegative().default(5),
99
- stopOnLowSignal: z.boolean().default(false),
100
- lowSignalRoundThreshold: z.number().int().nonnegative().default(3),
101
- lowSignalMaxComments: z.number().int().nonnegative().default(2),
102
- roles: z.array(z.string().trim().min(1)).optional(),
96
+ fanOut: z.number().int().min(1).max(10).describe("Parallel reviewers per refinement round."),
97
+ mode: z.enum(["parallel", "sequential"]).describe("Whether refinement reviewers run in parallel or one after another."),
98
+ maxCopilotRounds: z.number().int().nonnegative().default(5).describe("Automated Copilot review rounds before converging; 0 disables Copilot review."),
99
+ stopOnLowSignal: z.boolean().default(false).describe("Stop Copilot rounds early once they stop producing signal."),
100
+ lowSignalRoundThreshold: z.number().int().nonnegative().default(3).describe("Rounds counted toward the low-signal stop decision."),
101
+ lowSignalMaxComments: z.number().int().nonnegative().default(2).describe("A round with at most this many comments counts as low-signal."),
102
+ roles: z.array(z.string().trim().min(1)).describe("Review lenses the refinement fan-out dispatches.").optional(),
103
103
  });
104
104
 
105
105
  const GateConfig = z.strictObject({
106
- angles: z.array(z.string().trim().min(1)).optional(),
107
- excludeAngles: z.array(z.string().trim().min(1)).default([]),
108
- mandatoryAngles: z.array(z.string().trim().min(1)).default([]),
109
- required: z.boolean().default(true),
110
- requireCi: z.boolean().default(true),
106
+ angles: z.array(z.string().trim().min(1)).describe("Review lenses this gate fans out to.").optional(),
107
+ excludeAngles: z.array(z.string().trim().min(1)).default([]).describe("Angles removed from the resolved angle list."),
108
+ mandatoryAngles: z.array(z.string().trim().min(1)).default([]).describe("Angles that always run, regardless of diff-based dynamic selection."),
109
+ required: z.boolean().default(true).describe("Whether this gate must run."),
110
+ requireCi: z.boolean().default(true).describe("Per-gate CI prerequisite (default true): the gate requires green CI on the current head; false opts this gate out of the CI precondition entirely, including a real failure."),
111
111
  blockCleanOnFindingSeverities: z
112
112
  .array(z.enum(["must-fix", "worth-fixing-now", "defer"]))
113
113
  .min(1)
114
- .default(["must-fix"]),
115
- dynamicAngles: z.boolean().default(false),
114
+ .default(["must-fix"])
115
+ .describe("Finding severities that block a clean gate verdict."),
116
+ dynamicAngles: z.boolean().default(false).describe("Enable diff-driven dynamic angle resolution for this gate."),
116
117
  // Additive counterpart to the subtractive dynamicAngles path (#1048): when
117
118
  // true, the context-builder may also ADD catalog angles — from
118
119
  // resolveAnglePool() (gates.anglePool, or else the union of the persona
119
120
  // registry and this config's own configured angles) — that change-category
120
121
  // heuristics recommend but that are not already in this gate's configured
121
122
  // pool. Default false preserves today's subtractive-only behavior exactly.
122
- additiveAngles: z.boolean().default(false),
123
+ additiveAngles: z.boolean().default(false).describe("Allow diff-driven addition of catalog angles beyond this gate's configured pool."),
123
124
  });
124
125
 
125
126
  const GatesConfig = z.strictObject({
@@ -179,12 +180,12 @@ const GatesConfig = z.strictObject({
179
180
  const AutonomyConfig = z.strictObject({
180
181
  stopAt: z.array(
181
182
  z.enum(["refinement", "draft-pr", "pre-approval", "merge"])
182
- ),
183
+ ).describe("Checkpoints that require operator confirmation before the loop proceeds (default: [\"merge\"])."),
183
184
  // When true, merge is a fixed, non-overridable human action: the agent never
184
185
  // runs `gh pr merge`, `resolveAutonomyStopAt` always includes "merge", and
185
186
  // any per-run merge authorization (envelope flag / explicit instruction) is
186
187
  // ignored — it fails closed. See resolveHumanMergeOnly / resolveEffectiveMergeAuthorized.
187
- humanMergeOnly: z.boolean().optional(),
188
+ humanMergeOnly: z.boolean().describe("Merge stays a fixed human-only action: the agent never merges and any per-run merge authorization is ignored (fails closed).").optional(),
188
189
  });
189
190
 
190
191
  /**
@@ -207,33 +208,42 @@ const ApprovalConfig = z.strictObject({
207
208
  });
208
209
 
209
210
  const WorkflowConfig = z.strictObject({
210
- asyncStartMode: z.enum(["required", "allowed"]).default("required"),
211
- requireRetrospective: z.boolean(),
212
- requireDraftFirst: z.boolean(),
213
- devModeDefault: z.boolean(),
211
+ asyncStartMode: z.enum(["required", "allowed"]).default("required").describe("Whether the async start contract is required or merely allowed."),
212
+ requireRetrospective: z.boolean().describe("Require a retrospective checkpoint before a loop completes."),
213
+ requireDraftFirst: z.boolean().describe("Open pull requests as drafts and promote via the draft gate."),
214
+ devModeDefault: z.boolean().describe("Default new loops to dev mode."),
214
215
  });
215
216
 
216
217
  const LocalImplementationConfig = z.strictObject({
217
218
  /** Opt into light mode for small scoped changes */
218
219
  lightMode: z.strictObject({
219
- enabled: z.boolean(),
220
- maxFiles: z.number().int().min(1),
221
- maxLines: z.number().int().min(1),
220
+ enabled: z.boolean().describe("Opt small scoped changes into the lightweight dispatch path."),
221
+ maxFiles: z.number().int().min(1).describe("Light mode applies only when the change touches at most this many files."),
222
+ maxLines: z.number().int().min(1).describe("Light mode applies only when the change stays within this many lines."),
222
223
  // Copilot review round cap for light-dispatched PRs (#1210). Composes with
223
224
  // (does not replace) refinement.maxCopilotRounds — see
224
225
  // resolveEffectiveCopilotRoundCap.
225
- maxCopilotRounds: z.number().int().nonnegative().default(1),
226
+ maxCopilotRounds: z.number().int().nonnegative().default(1).describe("Copilot round cap for light-dispatched PRs; composes as min(this, refinement.maxCopilotRounds)."),
227
+ }).optional(),
228
+ /**
229
+ * Opt into issue-less PR-first (`--lightweight` with no --issue) at ANY
230
+ * change scope. Decoupled from lightMode: gate dispatch still resolves
231
+ * inline vs full_fanout from scope on its own, so over-threshold issue-less
232
+ * PRs get the full fan-out and the full-PR Copilot round cap.
233
+ */
234
+ issueless: z.strictObject({
235
+ enabled: z.boolean().describe("Opt into issue-less PR-first dispatch at any change scope; gate dispatch still resolves inline vs full fan-out from scope on its own."),
226
236
  }).optional(),
227
237
  });
228
238
 
229
239
  /** Queue mode config */
230
240
  const QueueConfig = z.strictObject({
231
- maxParallel: z.number().int().min(1).max(10).default(3),
232
- maxAutoFiledIssues: z.number().int().min(0).max(100).default(10),
233
- reDispatchMaxRetries: z.number().int().min(0).max(10).default(1),
234
- projectNumber: z.number().int().positive().optional(),
235
- boardTitle: z.string().trim().min(1).optional(),
236
- archiveOlderThanDays: z.number().int().positive().optional(),
241
+ maxParallel: z.number().int().min(1).max(10).default(3).describe("Maximum queue items worked in parallel."),
242
+ maxAutoFiledIssues: z.number().int().min(0).max(100).default(10).describe("Cap on auto-filed issues per run."),
243
+ reDispatchMaxRetries: z.number().int().min(0).max(10).default(1).describe("Retries when re-dispatching a failed queue item."),
244
+ projectNumber: z.number().int().positive().describe("GitHub Projects board number (explicit opt-in to Projects-based queue ordering).").optional(),
245
+ boardTitle: z.string().trim().min(1).describe("GitHub Projects board title (explicit opt-in to Projects-based queue ordering).").optional(),
246
+ archiveOlderThanDays: z.number().int().positive().describe("Archive done board items older than this many days.").optional(),
237
247
  });
238
248
 
239
249
  /**
@@ -244,8 +254,8 @@ const QueueConfig = z.strictObject({
244
254
  * data). Both optional; empty/absent is a valid no-op.
245
255
  */
246
256
  const WorktreeConfig = z.strictObject({
247
- copyOnInit: z.array(z.string().trim().min(1)).optional(),
248
- linkOnInit: z.array(z.string().trim().min(1)).optional(),
257
+ copyOnInit: z.array(z.string().trim().min(1)).describe("Repo-relative paths/globs copied into a fresh worktree (isolated per worktree — use for mutable files).").optional(),
258
+ linkOnInit: z.array(z.string().trim().min(1)).describe("Repo-relative paths/globs symlinked to the main checkout (shared — read-only data only).").optional(),
249
259
  });
250
260
 
251
261
  /**
@@ -455,17 +465,19 @@ const PersonasConfig = z.record(z.string().min(1), PersonaEntry);
455
465
 
456
466
  // Partial nested gate entries for file-level config (allows overriding only
457
467
  // requireCi/required/angles without restating the whole gate object).
458
- const FileGateConfig = GateConfig.partial();
459
468
  const FileGatesConfig = z.strictObject({
460
- draft: FileGateConfig.optional(),
461
- preApproval: FileGateConfig.optional(),
462
- spike: FileGateConfig.optional(),
463
- requireFanoutEvidence: z.boolean().optional(),
464
- requireFanoutProvenance: z.boolean().optional(),
465
- maxFanoutReviewers: z.number().int().min(1).max(64).optional(),
466
- postFindingsComments: z.boolean().optional(),
467
- anglePool: z.array(z.string().trim().min(1)).optional(),
468
- rejectForeignAngles: z.boolean().optional(),
469
+ // Each gate gets its own GateConfig.partial() instance rather than three
470
+ // .describe() clones of one shared partial, so no underlying def is shared
471
+ // and per-gate metadata renders unambiguously.
472
+ draft: GateConfig.partial().describe("Draft gate config (runs before a PR leaves draft).").optional(),
473
+ preApproval: GateConfig.partial().describe("Pre-approval gate config (final re-review before the merge handoff).").optional(),
474
+ spike: GateConfig.partial().describe("Relaxed spike gate profile; applies only to spike-mode work.").optional(),
475
+ requireFanoutEvidence: z.boolean().describe("Require fan-out/fan-in review evidence on gate verdicts; inline single-agent verdicts are rejected except under the strict light-mode exception (under-threshold scope, no gate:full label, recorded inline reason).").optional(),
476
+ requireFanoutProvenance: z.boolean().describe("Additionally require recorded, internally-consistent fan-out provenance (distinct reviewer count + per-angle dispatch).").optional(),
477
+ maxFanoutReviewers: z.number().int().min(1).max(64).describe("Cap on parallel gate fan-out reviewers; overflow runs in sequential batches.").optional(),
478
+ postFindingsComments: z.boolean().describe("Post consolidated gate findings as a marker-tagged PR comment (default true).").optional(),
479
+ anglePool: z.array(z.string().trim().min(1)).describe("Explicit global lens catalog for additive angle selection.").optional(),
480
+ rejectForeignAngles: z.boolean().describe("Reject fan-out provenance naming angles outside the gate's configured pool (default true).").optional(),
469
481
  });
470
482
 
471
483
  // Partial persona entries for file-level config (allows omitting fields)
@@ -527,6 +539,7 @@ export const BUILT_IN_DEFAULTS = Object.freeze({
527
539
  }),
528
540
  localImplementation: Object.freeze({
529
541
  lightMode: Object.freeze({ enabled: false, maxFiles: 3, maxLines: 200, maxCopilotRounds: 1 }),
542
+ issueless: Object.freeze({ enabled: false }),
530
543
  }),
531
544
  queue: Object.freeze({
532
545
  maxParallel: 3,
@@ -553,21 +566,21 @@ export const BUILT_IN_DEFAULTS = Object.freeze({
553
566
  // ============================================================================
554
567
 
555
568
  export const FileConfigSchema = z.strictObject({
556
- version: z.literal(1),
557
- strategy: StrategyConfig.partial().optional(),
558
- inputSource: InputSourceConfig.partial().optional(),
559
- models: ModelsConfigBase.partial().superRefine(refineRoleTiers).optional(),
560
- refinement: RefinementConfig.partial().optional(),
561
- gates: FileGatesConfig.optional(),
562
- autonomy: AutonomyConfig.partial().optional(),
563
- approval: ApprovalConfig.partial().optional(),
564
- workflow: WorkflowConfig.partial().optional(),
565
- localImplementation: LocalImplementationConfig.partial().optional(),
566
- queue: QueueConfig.partial().optional(),
567
- personas: FilePersonasConfig.optional(),
568
- internalPathPatterns: InternalPatternsConfig.optional(),
569
- worktree: WorktreeConfig.partial().optional(),
570
- uiReview: UiReviewConfig.partial().optional(),
569
+ version: z.literal(1).describe("Config format version; always 1."),
570
+ strategy: StrategyConfig.partial().describe("Work-intake strategy defaults.").optional(),
571
+ inputSource: InputSourceConfig.partial().describe("Spec source for local-first work.").optional(),
572
+ models: ModelsConfigBase.partial().superRefine(refineRoleTiers).describe("Model routing: conductor override, per-role/angle overrides, tier aliases, and role→tier policy.").optional(),
573
+ refinement: RefinementConfig.partial().describe("Refinement fan-out and Copilot review-round behavior.").optional(),
574
+ gates: FileGatesConfig.describe("Gate review configuration: per-gate angle sets plus fan-out enforcement knobs.").optional(),
575
+ autonomy: AutonomyConfig.partial().describe("How far the loop proceeds without operator confirmation.").optional(),
576
+ approval: ApprovalConfig.partial().describe("Approval / merge-handoff behavior (human-handoff offer).").optional(),
577
+ workflow: WorkflowConfig.partial().describe("Workflow posture: draft-first, retrospectives, dev mode, async start.").optional(),
578
+ localImplementation: LocalImplementationConfig.partial().describe("Local implementation dispatch (light mode for small scoped changes).").optional(),
579
+ queue: QueueConfig.partial().describe("Queue mode: parallelism, auto-filing caps, and Projects board opt-in.").optional(),
580
+ personas: FilePersonasConfig.describe("Gate-angle → reviewer persona registry overrides (angle name → persona, prompt, default model).").optional(),
581
+ internalPathPatterns: InternalPatternsConfig.describe("Regex whitelist for internal-only PR detection.").optional(),
582
+ worktree: WorktreeConfig.partial().describe("Worktree provisioning: gitignored files/dirs copied or symlinked into fresh worktrees.").optional(),
583
+ uiReview: UiReviewConfig.partial().describe("UI-review route recipes: per-project run/boot, dev-login, driven flows, and caps.").optional(),
571
584
  // Deprecated (removed in #1088): tolerated so consumer .devloops files that
572
585
  // still carry a localPlanning block keep parsing. Accepted, never read.
573
586
  localPlanning: z.unknown().optional(),
@@ -1424,6 +1437,19 @@ export function resolveLightMode(config) {
1424
1437
  };
1425
1438
  }
1426
1439
 
1440
+ /**
1441
+ * Resolve the issue-less PR-first any-scope opt-in (#1349).
1442
+ *
1443
+ * True only when `localImplementation.issueless.enabled` is exactly `true`;
1444
+ * absent, false, or malformed values resolve to false (fail closed).
1445
+ *
1446
+ * @param {DevLoopConfig} config
1447
+ * @returns {boolean}
1448
+ */
1449
+ export function resolveIssuelessEnabled(config) {
1450
+ return config?.localImplementation?.issueless?.enabled === true;
1451
+ }
1452
+
1427
1453
  /**
1428
1454
  * Resolve the effective Copilot review round cap for a PR (#1210).
1429
1455
  *