@gr8ful/spf 0.12.0 → 0.13.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/README.md +104 -28
- package/assets/prompts/refiner/system.md +105 -25
- package/assets/prompts/refiner/user.md +50 -15
- package/assets/prompts/scout/system.md +2 -2
- package/assets/prompts/scout/user.md +1 -1
- package/assets/skill/references/config.md +27 -8
- package/assets/templates/ts.spf.config.yaml +11 -4
- package/dist/chains/steps.d.ts +14 -14
- package/dist/chains/steps.js +44 -25
- package/dist/cli/commands/doctor.js +6 -0
- package/dist/cli/commands/watch.js +33 -11
- package/dist/core/data_types.d.ts +199 -1
- package/dist/core/data_types.js +127 -1
- package/dist/core/gates.d.ts +24 -8
- package/dist/core/gates.js +175 -20
- package/dist/core/issues/github_provider.d.ts +10 -5
- package/dist/core/issues/github_provider.js +13 -2
- package/dist/core/issues/jira_provider.d.ts +3 -3
- package/dist/core/issues/jira_provider.js +2 -0
- package/dist/core/issues/provider.d.ts +48 -4
- package/dist/core/notify/channel.d.ts +1 -1
- package/dist/core/refine.d.ts +35 -1
- package/dist/core/refine.js +56 -1
- package/dist/core/watch.d.ts +65 -9
- package/dist/core/watch.js +157 -11
- package/package.json +1 -1
package/dist/core/watch.d.ts
CHANGED
|
@@ -43,15 +43,32 @@ export interface RefinedQuestionRef {
|
|
|
43
43
|
recommendation: string;
|
|
44
44
|
evidence: string[];
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* One proposed standalone spec, part of splitting an over-large spec into
|
|
48
|
+
* several — mirrors `SpecSplitSchema` (`core/data_types.ts`) field-for-field,
|
|
49
|
+
* same decoupling reasoning as `RefinedIssueRef`/`RefinedQuestionRef` above.
|
|
50
|
+
* Read back from `steps.publishIssues()`'s `refine_split.json` side channel
|
|
51
|
+
* (see `cli/commands/watch.ts`'s `runRefine`) and, once a human approves,
|
|
52
|
+
* handed to `core/refine.ts`'s `publishSpecs()` verbatim via
|
|
53
|
+
* `WatchDeps.publishSpecs` — structurally identical to `SpecSplit`, so no
|
|
54
|
+
* conversion is needed at that boundary.
|
|
55
|
+
*/
|
|
56
|
+
export interface RefinedSpecSplitRef {
|
|
57
|
+
title: string;
|
|
58
|
+
body: string;
|
|
59
|
+
rationale: string;
|
|
60
|
+
}
|
|
46
61
|
export interface RefineRunResult {
|
|
47
62
|
accepted: boolean;
|
|
48
63
|
adwId: string;
|
|
49
64
|
/** Shown to the engineer via a `blocked` comment on a failed/no-op run. */
|
|
50
65
|
detail: string;
|
|
51
|
-
/** What `steps.publishIssues()` created, read back from its side-channel file — see `cli/commands/watch.ts`'s `runRefine`. Empty when `!accepted` or when the run escalated instead of publishing. */
|
|
66
|
+
/** What `steps.publishIssues()` created, read back from its side-channel file — see `cli/commands/watch.ts`'s `runRefine`. Empty when `!accepted` or when the run escalated (questions) or proposed a split instead of publishing. */
|
|
52
67
|
created: RefinedIssueRef[];
|
|
53
|
-
/** What the refiner is asking, read back from its own side-channel file — see `cli/commands/watch.ts`'s `runRefine`. Empty when `!accepted` or when the run published a tree instead of escalating; `gates.refinementWellFormed` guarantees `created
|
|
68
|
+
/** What the refiner is asking, read back from its own side-channel file — see `cli/commands/watch.ts`'s `runRefine`. Empty when `!accepted` or when the run published a tree or proposed a split instead of escalating; `gates.refinementWellFormed` guarantees `created`/`questions`/`split` are never more than one non-empty at once. */
|
|
54
69
|
questions: RefinedQuestionRef[];
|
|
70
|
+
/** What the refiner proposed splitting this spec into, read back from its own side-channel file (`refine_split.json`). Empty unless this round proposed a split — see `questions`' own doc comment for the same three-way exclusion. */
|
|
71
|
+
split: RefinedSpecSplitRef[];
|
|
55
72
|
}
|
|
56
73
|
/**
|
|
57
74
|
* The fan-out lane's injected trio + the two values `WatchDeps` cannot derive.
|
|
@@ -168,6 +185,24 @@ export interface WatchDeps {
|
|
|
168
185
|
* without authoring at all (see `cli/commands/watch.ts`'s startup check).
|
|
169
186
|
*/
|
|
170
187
|
listChildren?: (parent: Issue) => Promise<Issue[]>;
|
|
188
|
+
/**
|
|
189
|
+
* `core/refine.ts`'s `publishSpecs()`, pre-bound to this run's authoring
|
|
190
|
+
* provider and label prefix — same injection reasoning as `listChildren`
|
|
191
|
+
* just above: this module drives whatever it's given without importing a
|
|
192
|
+
* concrete provider type. `undefined` only on a tracker that isn't
|
|
193
|
+
* authoring-capable, which `cli/commands/watch.ts`'s startup check already
|
|
194
|
+
* refuses to let `refineEnabled` be true for (see its own comment on
|
|
195
|
+
* `watch.refine.enabled` requiring "github" or "jira") — so
|
|
196
|
+
* `executeApprovedSplits` treats an unset value here as an invariant
|
|
197
|
+
* violation, not a graceful degrade, unlike `listChildren`'s logged no-op.
|
|
198
|
+
*/
|
|
199
|
+
publishSpecs?: (specs: RefinedSpecSplitRef[], opts: {
|
|
200
|
+
originalSpecId: string;
|
|
201
|
+
priority?: RefinedPriority | null;
|
|
202
|
+
}) => Promise<Array<{
|
|
203
|
+
id: string;
|
|
204
|
+
title: string;
|
|
205
|
+
}>>;
|
|
171
206
|
log: (message: string) => void;
|
|
172
207
|
/**
|
|
173
208
|
* Structured push, alongside `log`'s plain string — a required field, like
|
|
@@ -295,17 +330,38 @@ export declare function finishTrackedSpecs(deps: WatchDeps): Promise<void>;
|
|
|
295
330
|
* Exported and pure (no provider, no I/O) so it's directly unit-testable.
|
|
296
331
|
*/
|
|
297
332
|
export declare function buildSpecPrompt(issue: Issue, comments: IssueComment[], feedback?: WatchMarker["feedback"], priority?: RefinedPriority | null): string;
|
|
333
|
+
/**
|
|
334
|
+
* The other half of the split flow: a human already approved
|
|
335
|
+
* (`<prefix>:split-approved`) exactly what `proposeSpecSplit` recorded in
|
|
336
|
+
* `WatchMarker.split`, so this is a deterministic `code`-shaped action, not
|
|
337
|
+
* an agent one — no scout, no refiner session, no worktree, mirroring why
|
|
338
|
+
* `steps.publishIssues()` is a `code` phase rather than an agent phase.
|
|
339
|
+
* Idempotent the same way `runSpec`'s own publish path is: a non-empty
|
|
340
|
+
* `marker.refined` (this function writes the created specs' ids there, same
|
|
341
|
+
* field `finishTrackedSpecs` polls) short-circuits, so a crash between
|
|
342
|
+
* creating the specs and posting the summary comment never double-creates on
|
|
343
|
+
* the next tick.
|
|
344
|
+
*
|
|
345
|
+
* Recording the child SPECS in `marker.refined` — not their eventual
|
|
346
|
+
* decomposed issues — is deliberate: it's what lets `finishTrackedSpecs`
|
|
347
|
+
* close the ORIGINAL spec once both child specs finish their own trees
|
|
348
|
+
* (transitively — each child spec is a `spec-in-progress` tracked the same
|
|
349
|
+
* way once IT publishes), with no new tracking code needed anywhere.
|
|
350
|
+
*/
|
|
351
|
+
export declare function executeApprovedSplits(deps: WatchDeps): Promise<void>;
|
|
298
352
|
/**
|
|
299
353
|
* The refine lane's own `reconcileOrphans` — a `refining`-labeled spec this
|
|
300
|
-
* process isn't tracking is one of
|
|
354
|
+
* process isn't tracking is one of four things: a completed publish that
|
|
301
355
|
* crashed before its own `transition(issue, "done")` ran (resume: finish it,
|
|
302
|
-
* no re-run), a completed
|
|
356
|
+
* no re-run), a completed split proposal that crashed before its own
|
|
357
|
+
* `transition(issue, "split-proposed")` ran (resume: finish THAT transition
|
|
358
|
+
* — checked before the escalation case below, since `proposeSpecSplit`
|
|
359
|
+
* writes BOTH `split` and `feedback` while `escalateSpec` writes only the
|
|
360
|
+
* latter), a completed escalation that crashed before its own
|
|
303
361
|
* `transition(issue, "needs-feedback")` ran (resume: finish THAT transition,
|
|
304
|
-
* no re-asking
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* `MAX_ORPHAN_ATTEMPTS`, then give up). A no-op entirely when `watch.refine`
|
|
308
|
-
* is off — see `WatchDeps.refineEnabled`.
|
|
362
|
+
* no re-asking), or a genuine orphan (retry up to `MAX_ORPHAN_ATTEMPTS`, then
|
|
363
|
+
* give up). A no-op entirely when `watch.refine` is off — see
|
|
364
|
+
* `WatchDeps.refineEnabled`.
|
|
309
365
|
*/
|
|
310
366
|
export declare function reconcileRefining(deps: WatchDeps, state: WatchRunState): Promise<void>;
|
|
311
367
|
/** Poll every `review`-labeled issue's PR for merged (-> done) or closed-without-merging (-> blocked). */
|
package/dist/core/watch.js
CHANGED
|
@@ -12,8 +12,14 @@
|
|
|
12
12
|
* posts the questions and moves the spec to `needs-feedback`; a human
|
|
13
13
|
* answers in the issue's comments and adds `continue-refinement`;
|
|
14
14
|
* `claimSpecs` resumes it — the SAME `adw_id`, comment thread folded into
|
|
15
|
-
* the prompt (`buildSpecPrompt`) — for as many rounds as it takes.
|
|
16
|
-
*
|
|
15
|
+
* the prompt (`buildSpecPrompt`) — for as many rounds as it takes. A second,
|
|
16
|
+
* different escape hatch handles a spec too large for one decomposition (not
|
|
17
|
+
* ambiguity — size): `proposeSpecSplit` posts a concrete split into several
|
|
18
|
+
* standalone specs and moves the spec to `split-proposed`; a human either
|
|
19
|
+
* approves as-is (`split-approved`, executed deterministically by
|
|
20
|
+
* `executeApprovedSplits` — no agent re-run) or revises it the same way a
|
|
21
|
+
* question gets answered (`continue-refinement`). See `provider.ts`'s
|
|
22
|
+
* `WatchState` doc comment for the full state diagram.
|
|
17
23
|
*
|
|
18
24
|
* Provider-agnostic (drives whatever `IssueProvider` it's given) and
|
|
19
25
|
* chain-agnostic (drives whatever `runChain`/`runRefine` callback it's
|
|
@@ -395,17 +401,125 @@ async function escalateSpec(deps, issue, marker, questions, adwId, round) {
|
|
|
395
401
|
await deps.provider.transition(issue, "needs-feedback");
|
|
396
402
|
}
|
|
397
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* The refine lane's third finishing move, alongside `announceRefined`
|
|
406
|
+
* (published a tree) and `escalateSpec` (material ambiguity): a spec too
|
|
407
|
+
* large for one decomposition (see `gates.refinementWellFormed`'s budget
|
|
408
|
+
* checks, and `assets/prompts/refiner/system.md`'s "Sizing and the budget").
|
|
409
|
+
* Posts each proposed spec as its own comment section — title, a short
|
|
410
|
+
* preview of its body, and the rationale for splitting it out — records the
|
|
411
|
+
* EXACT proposal in `WatchMarker.split` so `executeApprovedSplits` creates
|
|
412
|
+
* precisely what a human reviewed rather than whatever the marker happens to
|
|
413
|
+
* hold by the time approval lands, and moves the spec to `split-proposed`.
|
|
414
|
+
*
|
|
415
|
+
* Shares `feedback`'s bookkeeping with `escalateSpec`, deliberately: a human
|
|
416
|
+
* can revise a proposed split the same way they answer a question — comment
|
|
417
|
+
* inline, add `continue-refinement` — and `buildSpecPrompt` folds the
|
|
418
|
+
* comment thread back into the resumed prompt identically either way,
|
|
419
|
+
* regardless of which of the two escalation shapes the PREVIOUS round used.
|
|
420
|
+
* `reconcileRefining` below tells the two apart by checking `marker.split`
|
|
421
|
+
* before `marker.feedback`, since this function always writes both while
|
|
422
|
+
* `escalateSpec` writes only the latter.
|
|
423
|
+
*/
|
|
424
|
+
async function proposeSpecSplit(deps, issue, marker, specs, adwId, round) {
|
|
425
|
+
const body = `## spf proposes splitting this spec (round ${round})\n\n` +
|
|
426
|
+
`This spec doesn't fit in one decomposition. Proposed split into ${specs.length} standalone specs:\n\n` +
|
|
427
|
+
specs
|
|
428
|
+
.map((s, i) => {
|
|
429
|
+
const lines = s.body.trim().split("\n");
|
|
430
|
+
const preview = lines.slice(0, 6).join("\n");
|
|
431
|
+
const truncated = lines.length > 6 ? "\n\n_(preview truncated — the full text lands in the created spec)_" : "";
|
|
432
|
+
return `### ${i + 1}. ${s.title}\n\n${preview}${truncated}\n\n**Why this is its own spec:** ${s.rationale}`;
|
|
433
|
+
})
|
|
434
|
+
.join("\n\n---\n\n") +
|
|
435
|
+
`\n\n---\n\nApprove as proposed by adding the \`${deps.labelPrefix}:split-approved\` label — spf will create these ${specs.length} specs with no further agent run. ` +
|
|
436
|
+
`Or comment with changes and add \`${deps.labelPrefix}:continue-refinement\` — refinement resumes from where it left off (adw_id \`${adwId}\`) and can revise the proposal.`;
|
|
437
|
+
deps.notify({
|
|
438
|
+
// "notice" level, not "info" — the same class of event as
|
|
439
|
+
// spec_needs_feedback: spf needs a human, so it belongs on an
|
|
440
|
+
// `attention`-scope channel just as much as an `all`-scope one.
|
|
441
|
+
kind: "spec_split_proposed",
|
|
442
|
+
level: "notice",
|
|
443
|
+
title: `spec ${issue.id} split proposed`,
|
|
444
|
+
detail: `${specs.length} proposed spec(s), round ${round}.`,
|
|
445
|
+
fields: [
|
|
446
|
+
["issue", issue.id],
|
|
447
|
+
["title", issue.title],
|
|
448
|
+
["chain", deps.refineChain],
|
|
449
|
+
["adw_id", adwId],
|
|
450
|
+
["round", String(round)],
|
|
451
|
+
],
|
|
452
|
+
});
|
|
453
|
+
if (!deps.dryRun) {
|
|
454
|
+
await deps.provider.comment(issue, body);
|
|
455
|
+
await deps.provider.writeMarker(issue, {
|
|
456
|
+
...marker,
|
|
457
|
+
split: { specs, proposed_at: new Date().toISOString(), rounds: round },
|
|
458
|
+
feedback: { rounds: round, asked_at: new Date().toISOString() },
|
|
459
|
+
});
|
|
460
|
+
await deps.provider.transition(issue, "split-proposed");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The other half of the split flow: a human already approved
|
|
465
|
+
* (`<prefix>:split-approved`) exactly what `proposeSpecSplit` recorded in
|
|
466
|
+
* `WatchMarker.split`, so this is a deterministic `code`-shaped action, not
|
|
467
|
+
* an agent one — no scout, no refiner session, no worktree, mirroring why
|
|
468
|
+
* `steps.publishIssues()` is a `code` phase rather than an agent phase.
|
|
469
|
+
* Idempotent the same way `runSpec`'s own publish path is: a non-empty
|
|
470
|
+
* `marker.refined` (this function writes the created specs' ids there, same
|
|
471
|
+
* field `finishTrackedSpecs` polls) short-circuits, so a crash between
|
|
472
|
+
* creating the specs and posting the summary comment never double-creates on
|
|
473
|
+
* the next tick.
|
|
474
|
+
*
|
|
475
|
+
* Recording the child SPECS in `marker.refined` — not their eventual
|
|
476
|
+
* decomposed issues — is deliberate: it's what lets `finishTrackedSpecs`
|
|
477
|
+
* close the ORIGINAL spec once both child specs finish their own trees
|
|
478
|
+
* (transitively — each child spec is a `spec-in-progress` tracked the same
|
|
479
|
+
* way once IT publishes), with no new tracking code needed anywhere.
|
|
480
|
+
*/
|
|
481
|
+
export async function executeApprovedSplits(deps) {
|
|
482
|
+
if (!deps.refineEnabled)
|
|
483
|
+
return;
|
|
484
|
+
const approved = await deps.provider.listInState("split-approved");
|
|
485
|
+
for (const issue of approved) {
|
|
486
|
+
const marker = await deps.provider.readMarker(issue);
|
|
487
|
+
if (marker?.refined && marker.refined.length > 0) {
|
|
488
|
+
deps.log(`watch: spec ${issue.id}: split already executed — finishing`);
|
|
489
|
+
await announceRefined(deps, issue, marker.refined.map((id) => ({ id })), marker.split?.rounds ?? marker.feedback?.rounds ?? 0);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const specs = marker?.split?.specs ?? [];
|
|
493
|
+
if (specs.length === 0) {
|
|
494
|
+
deps.log(`watch: spec ${issue.id} is \`${deps.labelPrefix}:split-approved\` but its marker records no proposed split — leaving it alone`);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (!deps.publishSpecs) {
|
|
498
|
+
// See WatchDeps.publishSpecs's own doc comment: refineEnabled true
|
|
499
|
+
// without an authoring-capable tracker is a startup failure
|
|
500
|
+
// (cli/commands/watch.ts), never a state this function should reach.
|
|
501
|
+
throw new Error(`watch: spec ${issue.id}: refine is enabled but no publishSpecs was provided — this is an spf bug, not a config problem`);
|
|
502
|
+
}
|
|
503
|
+
deps.log(`watch: spec ${issue.id}: split approved — creating ${specs.length} spec(s)`);
|
|
504
|
+
if (deps.dryRun)
|
|
505
|
+
continue;
|
|
506
|
+
const created = await deps.publishSpecs(specs, { originalSpecId: issue.id, priority: specPriorityLabel(issue, deps.labelPrefix) });
|
|
507
|
+
await announceRefined(deps, issue, created.map((c) => ({ id: c.id, title: c.title, kind: "spec" })), marker?.split?.rounds ?? 0);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
398
510
|
/**
|
|
399
511
|
* The refine lane's own `reconcileOrphans` — a `refining`-labeled spec this
|
|
400
|
-
* process isn't tracking is one of
|
|
512
|
+
* process isn't tracking is one of four things: a completed publish that
|
|
401
513
|
* crashed before its own `transition(issue, "done")` ran (resume: finish it,
|
|
402
|
-
* no re-run), a completed
|
|
514
|
+
* no re-run), a completed split proposal that crashed before its own
|
|
515
|
+
* `transition(issue, "split-proposed")` ran (resume: finish THAT transition
|
|
516
|
+
* — checked before the escalation case below, since `proposeSpecSplit`
|
|
517
|
+
* writes BOTH `split` and `feedback` while `escalateSpec` writes only the
|
|
518
|
+
* latter), a completed escalation that crashed before its own
|
|
403
519
|
* `transition(issue, "needs-feedback")` ran (resume: finish THAT transition,
|
|
404
|
-
* no re-asking
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
* `MAX_ORPHAN_ATTEMPTS`, then give up). A no-op entirely when `watch.refine`
|
|
408
|
-
* is off — see `WatchDeps.refineEnabled`.
|
|
520
|
+
* no re-asking), or a genuine orphan (retry up to `MAX_ORPHAN_ATTEMPTS`, then
|
|
521
|
+
* give up). A no-op entirely when `watch.refine` is off — see
|
|
522
|
+
* `WatchDeps.refineEnabled`.
|
|
409
523
|
*/
|
|
410
524
|
export async function reconcileRefining(deps, state) {
|
|
411
525
|
if (!deps.refineEnabled)
|
|
@@ -420,6 +534,22 @@ export async function reconcileRefining(deps, state) {
|
|
|
420
534
|
await announceRefined(deps, issue, marker.refined.map((id) => ({ id })), marker.feedback?.rounds ?? 0);
|
|
421
535
|
continue;
|
|
422
536
|
}
|
|
537
|
+
// Checked BEFORE marker.feedback: proposeSpecSplit always writes both
|
|
538
|
+
// `split` and `feedback` together, while escalateSpec writes only the
|
|
539
|
+
// latter — so `split` present is the more specific signal and must win.
|
|
540
|
+
if (marker?.split) {
|
|
541
|
+
deps.log(`watch: spec ${issue.id} orphaned after proposing round ${marker.split.rounds}'s split — finishing the transition to split-proposed`);
|
|
542
|
+
deps.notify({
|
|
543
|
+
kind: "spec_split_proposed",
|
|
544
|
+
level: "notice",
|
|
545
|
+
title: `spec ${issue.id} split proposed`,
|
|
546
|
+
detail: `Round ${marker.split.rounds}.`,
|
|
547
|
+
fields: [["issue", issue.id], ["title", issue.title], ["round", String(marker.split.rounds)]],
|
|
548
|
+
});
|
|
549
|
+
if (!deps.dryRun)
|
|
550
|
+
await deps.provider.transition(issue, "split-proposed");
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
423
553
|
if (marker?.feedback) {
|
|
424
554
|
deps.log(`watch: spec ${issue.id} orphaned after asking round ${marker.feedback.rounds} — finishing the transition to needs-feedback`);
|
|
425
555
|
deps.notify({
|
|
@@ -1058,14 +1188,25 @@ async function runSpec(deps, issue) {
|
|
|
1058
1188
|
return;
|
|
1059
1189
|
}
|
|
1060
1190
|
// Mutually exclusive by construction — gates.refinementWellFormed
|
|
1061
|
-
// guarantees a `questions`-bearing envelope publishes no `issues`
|
|
1062
|
-
// this branches before, never alongside, the publish
|
|
1191
|
+
// guarantees a `questions`-bearing envelope publishes no `issues`/`split`
|
|
1192
|
+
// — so this branches before, never alongside, the split or publish paths
|
|
1193
|
+
// below.
|
|
1063
1194
|
if (result.questions.length > 0) {
|
|
1064
1195
|
const round = (existingMarker?.feedback?.rounds ?? 0) + 1;
|
|
1065
1196
|
await escalateSpec(deps, issue, marker, result.questions, adwId, round);
|
|
1066
1197
|
cleanupWorktree(deps, { worktree: worktreePath, branch });
|
|
1067
1198
|
return;
|
|
1068
1199
|
}
|
|
1200
|
+
// Same mutual exclusion, the other escalation shape: a spec too large
|
|
1201
|
+
// for one decomposition, proposed as several standalone specs instead of
|
|
1202
|
+
// a scope question. See proposeSpecSplit's own doc comment for why this
|
|
1203
|
+
// shares `feedback`'s round-tracking with the questions branch above.
|
|
1204
|
+
if (result.split.length > 0) {
|
|
1205
|
+
const round = (existingMarker?.feedback?.rounds ?? existingMarker?.split?.rounds ?? 0) + 1;
|
|
1206
|
+
await proposeSpecSplit(deps, issue, marker, result.split, adwId, round);
|
|
1207
|
+
cleanupWorktree(deps, { worktree: worktreePath, branch });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1069
1210
|
await deps.provider.writeMarker(issue, { ...marker, refined: result.created.map((c) => c.id) });
|
|
1070
1211
|
await announceRefined(deps, issue, result.created, existingMarker?.feedback?.rounds ?? 0);
|
|
1071
1212
|
cleanupWorktree(deps, { worktree: worktreePath, branch });
|
|
@@ -1336,6 +1477,11 @@ export async function tick(deps, state) {
|
|
|
1336
1477
|
// on — checking in the same tick is strictly cheaper than making a product
|
|
1337
1478
|
// manager wait one extra poll interval to see it.
|
|
1338
1479
|
await finishTrackedSpecs(deps).catch(tickErrorHandler(deps, "finishTrackedSpecs"));
|
|
1480
|
+
// Right after finishTrackedSpecs, same reasoning: a spec a human approved
|
|
1481
|
+
// a split on this tick should get its child specs created this same tick,
|
|
1482
|
+
// not one poll interval later. No agent, no worktree — see
|
|
1483
|
+
// executeApprovedSplits's own doc comment.
|
|
1484
|
+
await executeApprovedSplits(deps).catch(tickErrorHandler(deps, "executeApprovedSplits"));
|
|
1339
1485
|
await claimSpecs(deps, state, "continue-refinement").catch(tickErrorHandler(deps, "claimSpecs(resume)"));
|
|
1340
1486
|
await claimSpecs(deps, state).catch(tickErrorHandler(deps, "claimSpecs"));
|
|
1341
1487
|
await claimNewWork(deps, state).catch(tickErrorHandler(deps, "claimNewWork"));
|