pi-soly 2.1.0 → 2.1.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/README.md +1 -0
- package/commands.ts +34 -78
- package/core.ts +1 -1
- package/init.ts +3 -7
- package/package.json +1 -1
- package/workflows/done.ts +5 -13
- package/workflows/execute.ts +33 -3
- package/workflows/goal-verify.ts +297 -0
- package/workflows/inspect.ts +9 -12
- package/workflows/migrate.ts +1 -1
- package/workflows/new.ts +1 -1
- package/workflows/quick.ts +5 -5
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ Restart pi (`/reload`), and you have:
|
|
|
29
29
|
- **Project management** — plans, state, phases, decisions
|
|
30
30
|
- **Workflow engine** — runs inline (no subagent plugin). The model proposes the next step and drives it via the `soly_workflow` tool on your plain-language intent; the verbs `soly discuss` · `plan` · `execute` · `verify` · `pause`/`resume` still work as text
|
|
31
31
|
- **Self-review loop** — `soly verify` re-reviews the work until "No issues found."
|
|
32
|
+
- **Goal-aware verification** — at the end of every `soly execute` (task / plan / phase plan-level), the worker reads PLAN.md's `## Goal` + `## Acceptance` and judges each item against `git diff`. A `## Status` section is appended to PLAN.md with a PASS / BLOCKED verdict. On `BLOCKED` the worker halts before calling `soly done` — so you can't ship a plan whose goal isn't actually met.
|
|
32
33
|
- **Visual chrome** — native footer, equalizer working spinner with live telemetry, gradient welcome banner
|
|
33
34
|
- **Rules & docs modal** — `/rules` and `/docs` open a fuzzy list + preview panel (no chat dumps)
|
|
34
35
|
- **Mandatory rules** — strict-mode directives injected every turn
|
package/commands.ts
CHANGED
|
@@ -171,7 +171,7 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
171
171
|
const rules = getRules();
|
|
172
172
|
const overridden = getOverridden();
|
|
173
173
|
if (rules.length === 0 && overridden.length === 0) {
|
|
174
|
-
|
|
174
|
+
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
177
|
// Rich modal in the TUI; plain select elsewhere (RPC/print).
|
|
@@ -205,17 +205,11 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
205
205
|
if (choice < rules.length) {
|
|
206
206
|
const rel = rules[choice];
|
|
207
207
|
if (rel) {
|
|
208
|
-
|
|
209
|
-
`[${rel.sourceLabel}] ${rel.relPath}\n\n${rel.body}`,
|
|
210
|
-
"info",
|
|
211
|
-
);
|
|
208
|
+
|
|
212
209
|
}
|
|
213
210
|
} else {
|
|
214
211
|
const idx = choice - rules.length;
|
|
215
|
-
|
|
216
|
-
`overridden: ${overridden[idx]} (skipped — a higher-priority source defines this rule)`,
|
|
217
|
-
"info",
|
|
218
|
-
);
|
|
212
|
+
|
|
219
213
|
}
|
|
220
214
|
}
|
|
221
215
|
return;
|
|
@@ -224,7 +218,7 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
224
218
|
if (sub === "analytics") {
|
|
225
219
|
const rules = getRules();
|
|
226
220
|
const analytics = analyzeRules(rules, CONTEXT_WINDOW_TOKENS);
|
|
227
|
-
|
|
221
|
+
|
|
228
222
|
return;
|
|
229
223
|
}
|
|
230
224
|
|
|
@@ -234,7 +228,7 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
234
228
|
// Surfaces context bloat and verifies rules will actually fire.
|
|
235
229
|
const rules = getRules();
|
|
236
230
|
const stats = buildRulesContextStats(rules, CONTEXT_WINDOW_TOKENS);
|
|
237
|
-
|
|
231
|
+
|
|
238
232
|
return;
|
|
239
233
|
}
|
|
240
234
|
|
|
@@ -250,13 +244,13 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
250
244
|
ui.notify(`Rule not found: ${target}`, "error");
|
|
251
245
|
return;
|
|
252
246
|
}
|
|
253
|
-
|
|
247
|
+
|
|
254
248
|
return;
|
|
255
249
|
}
|
|
256
250
|
|
|
257
251
|
if (sub === "reload") {
|
|
258
252
|
refreshRules();
|
|
259
|
-
|
|
253
|
+
|
|
260
254
|
updateStatus(ui);
|
|
261
255
|
return;
|
|
262
256
|
}
|
|
@@ -274,7 +268,7 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
274
268
|
return;
|
|
275
269
|
}
|
|
276
270
|
rule.enabled = sub === "enable";
|
|
277
|
-
|
|
271
|
+
|
|
278
272
|
updateStatus(ui);
|
|
279
273
|
return;
|
|
280
274
|
}
|
|
@@ -289,10 +283,7 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
289
283
|
count++;
|
|
290
284
|
}
|
|
291
285
|
}
|
|
292
|
-
|
|
293
|
-
`${count} rule(s) ${enable ? "enabled" : "disabled"} (${rules.length} total)`,
|
|
294
|
-
enable ? "info" : "warning",
|
|
295
|
-
);
|
|
286
|
+
|
|
296
287
|
updateStatus(ui);
|
|
297
288
|
return;
|
|
298
289
|
}
|
|
@@ -313,7 +304,7 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
313
304
|
categories.map((c) => `${c.name} — ${c.description}`),
|
|
314
305
|
);
|
|
315
306
|
if (choice == null) {
|
|
316
|
-
|
|
307
|
+
|
|
317
308
|
return;
|
|
318
309
|
}
|
|
319
310
|
const cat = categories[choice];
|
|
@@ -359,11 +350,7 @@ What must the LLM do?
|
|
|
359
350
|
`;
|
|
360
351
|
try {
|
|
361
352
|
fs.writeFileSync(filePath, template, "utf-8");
|
|
362
|
-
|
|
363
|
-
`soly: created ${path.relative(cwd, filePath)}\n\n` +
|
|
364
|
-
`Next: edit the file (description, globs, body), then \`/rules reload\` to load it.`,
|
|
365
|
-
"info",
|
|
366
|
-
);
|
|
353
|
+
|
|
367
354
|
refreshRules();
|
|
368
355
|
updateStatus(ui);
|
|
369
356
|
} catch (e) {
|
|
@@ -385,7 +372,7 @@ What must the LLM do?
|
|
|
385
372
|
ui.notify(`soly: only http(s) URLs are supported (got ${parsed.protocol})`, "error");
|
|
386
373
|
return;
|
|
387
374
|
}
|
|
388
|
-
|
|
375
|
+
|
|
389
376
|
const res = await fetch(url, {
|
|
390
377
|
signal: AbortSignal.timeout(10_000),
|
|
391
378
|
headers: { "user-agent": "soly-extension/1.0" },
|
|
@@ -420,16 +407,13 @@ What must the LLM do?
|
|
|
420
407
|
`${fileName} already exists. Overwrite?`,
|
|
421
408
|
);
|
|
422
409
|
if (!overwrite) {
|
|
423
|
-
|
|
410
|
+
|
|
424
411
|
return;
|
|
425
412
|
}
|
|
426
413
|
}
|
|
427
414
|
fs.writeFileSync(targetFile, text, "utf-8");
|
|
428
415
|
refreshRules();
|
|
429
|
-
|
|
430
|
-
`soly: installed ${path.relative(process.cwd(), targetFile)} (${(text.length / 1024).toFixed(1)}KB)`,
|
|
431
|
-
"info",
|
|
432
|
-
);
|
|
416
|
+
|
|
433
417
|
updateStatus(ui);
|
|
434
418
|
} catch (e) {
|
|
435
419
|
ui.notify(`soly: download failed: ${(e as Error).message}`, "error");
|
|
@@ -466,7 +450,7 @@ What must the LLM do?
|
|
|
466
450
|
if (sub === "list") {
|
|
467
451
|
const docs = getIntentDocs();
|
|
468
452
|
if (docs.length === 0) {
|
|
469
|
-
|
|
453
|
+
|
|
470
454
|
return;
|
|
471
455
|
}
|
|
472
456
|
if (ctx.mode === "tui") {
|
|
@@ -493,15 +477,11 @@ What must the LLM do?
|
|
|
493
477
|
const docs = getIntentDocs();
|
|
494
478
|
const inlineBodies: IntentInlineDoc[] = loadInlineIntentBodies(docs);
|
|
495
479
|
const stats = buildIntentStats(docs, inlineBodies);
|
|
496
|
-
|
|
480
|
+
|
|
497
481
|
return;
|
|
498
482
|
}
|
|
499
483
|
|
|
500
|
-
|
|
501
|
-
`Usage: /docs [list|stats] — open the docs panel, or show the context breakdown\n` +
|
|
502
|
-
`Found ${getIntentDocs().length} doc(s) loaded.`,
|
|
503
|
-
"info",
|
|
504
|
-
);
|
|
484
|
+
|
|
505
485
|
},
|
|
506
486
|
});
|
|
507
487
|
|
|
@@ -543,7 +523,7 @@ What must the LLM do?
|
|
|
543
523
|
|
|
544
524
|
const state = getState();
|
|
545
525
|
if (!state.exists) {
|
|
546
|
-
|
|
526
|
+
|
|
547
527
|
return;
|
|
548
528
|
}
|
|
549
529
|
|
|
@@ -557,7 +537,7 @@ What must the LLM do?
|
|
|
557
537
|
content.length > MAX
|
|
558
538
|
? `${content.slice(0, MAX)}\n\n[...truncated, file is ${content.length} chars]`
|
|
559
539
|
: content;
|
|
560
|
-
|
|
540
|
+
|
|
561
541
|
};
|
|
562
542
|
|
|
563
543
|
type SolySub = {
|
|
@@ -584,7 +564,7 @@ What must the LLM do?
|
|
|
584
564
|
};
|
|
585
565
|
const state = getState();
|
|
586
566
|
if (!state.exists) {
|
|
587
|
-
|
|
567
|
+
|
|
588
568
|
return;
|
|
589
569
|
}
|
|
590
570
|
switch (verb) {
|
|
@@ -624,7 +604,7 @@ What must the LLM do?
|
|
|
624
604
|
return;
|
|
625
605
|
}
|
|
626
606
|
default:
|
|
627
|
-
|
|
607
|
+
|
|
628
608
|
return;
|
|
629
609
|
}
|
|
630
610
|
};
|
|
@@ -651,7 +631,7 @@ What must the LLM do?
|
|
|
651
631
|
out.push(` - project: edit \`${state.solyDir}/soly.json\` directly`);
|
|
652
632
|
out.push(` - global: edit \`~/.agents/soly.json\``);
|
|
653
633
|
out.push("After editing, run /soly reload to re-pick up changes.");
|
|
654
|
-
|
|
634
|
+
|
|
655
635
|
},
|
|
656
636
|
},
|
|
657
637
|
position: {
|
|
@@ -659,21 +639,9 @@ What must the LLM do?
|
|
|
659
639
|
run: () => {
|
|
660
640
|
const s = getState();
|
|
661
641
|
if (s.position) {
|
|
662
|
-
|
|
663
|
-
[
|
|
664
|
-
`milestone: ${s.milestone}${s.milestoneName ? ` — ${s.milestoneName}` : ""}`,
|
|
665
|
-
`phase: ${s.position.phase}`,
|
|
666
|
-
`plan: ${s.position.plan}`,
|
|
667
|
-
`status: ${s.position.status}`,
|
|
668
|
-
`progress: ${buildProgressBar(s.progress.percent, 20)} ${s.progress.percent}% (${s.progress.completedPhases}/${s.progress.totalPhases} phases, ${s.progress.completedPlans}/${s.progress.totalPlans} plans)`,
|
|
669
|
-
].join("\n"),
|
|
670
|
-
"info",
|
|
671
|
-
);
|
|
642
|
+
|
|
672
643
|
} else {
|
|
673
|
-
|
|
674
|
-
`milestone: ${s.milestone} — no position set in STATE.md`,
|
|
675
|
-
"info",
|
|
676
|
-
);
|
|
644
|
+
|
|
677
645
|
}
|
|
678
646
|
},
|
|
679
647
|
},
|
|
@@ -735,16 +703,7 @@ What must the LLM do?
|
|
|
735
703
|
description: "progress bar + counts",
|
|
736
704
|
run: () => {
|
|
737
705
|
const s = getState();
|
|
738
|
-
|
|
739
|
-
[
|
|
740
|
-
`milestone: ${s.milestone}${s.milestoneName ? ` — ${s.milestoneName}` : ""}`,
|
|
741
|
-
`status: ${s.status}`,
|
|
742
|
-
`progress: ${buildProgressBar(s.progress.percent, 30)} ${s.progress.percent}%`,
|
|
743
|
-
`phases: ${s.progress.completedPhases}/${s.progress.totalPhases}`,
|
|
744
|
-
`plans: ${s.progress.completedPlans}/${s.progress.totalPlans}`,
|
|
745
|
-
].join("\n"),
|
|
746
|
-
"info",
|
|
747
|
-
);
|
|
706
|
+
|
|
748
707
|
},
|
|
749
708
|
},
|
|
750
709
|
phases: {
|
|
@@ -752,7 +711,7 @@ What must the LLM do?
|
|
|
752
711
|
run: () => {
|
|
753
712
|
const phases = getState().phases;
|
|
754
713
|
if (phases.length === 0) {
|
|
755
|
-
|
|
714
|
+
|
|
756
715
|
return;
|
|
757
716
|
}
|
|
758
717
|
const current = getState().currentPhase?.number;
|
|
@@ -761,7 +720,7 @@ What must the LLM do?
|
|
|
761
720
|
const cr = (p.contextExists ? "C" : "·") + (p.researchExists ? "R" : "·");
|
|
762
721
|
return `${marker} ${String(p.number).padStart(2, "0")}. ${p.name} [${cr}] plans=${p.planCount}`;
|
|
763
722
|
});
|
|
764
|
-
|
|
723
|
+
|
|
765
724
|
},
|
|
766
725
|
},
|
|
767
726
|
tasks: {
|
|
@@ -769,7 +728,7 @@ What must the LLM do?
|
|
|
769
728
|
run: () => {
|
|
770
729
|
const s = getState();
|
|
771
730
|
if (s.tasks.length === 0) {
|
|
772
|
-
|
|
731
|
+
|
|
773
732
|
return;
|
|
774
733
|
}
|
|
775
734
|
const byFeature = new Map<string, typeof s.tasks>();
|
|
@@ -788,7 +747,7 @@ What must the LLM do?
|
|
|
788
747
|
}
|
|
789
748
|
out.push("");
|
|
790
749
|
}
|
|
791
|
-
|
|
750
|
+
|
|
792
751
|
},
|
|
793
752
|
},
|
|
794
753
|
task: {
|
|
@@ -819,7 +778,7 @@ What must the LLM do?
|
|
|
819
778
|
if (summaryBody) {
|
|
820
779
|
showFile("SUMMARY.md", summaryBody);
|
|
821
780
|
} else {
|
|
822
|
-
|
|
781
|
+
|
|
823
782
|
}
|
|
824
783
|
},
|
|
825
784
|
},
|
|
@@ -828,14 +787,14 @@ What must the LLM do?
|
|
|
828
787
|
run: () => {
|
|
829
788
|
const features = getState().features;
|
|
830
789
|
if (features.length === 0) {
|
|
831
|
-
|
|
790
|
+
|
|
832
791
|
return;
|
|
833
792
|
}
|
|
834
793
|
const lines = features.map((f) => {
|
|
835
794
|
const rm = f.readmeExists ? "R" : "·";
|
|
836
795
|
return ` ${f.name.padEnd(28)} tasks=${f.taskCount} [${rm}]`;
|
|
837
796
|
});
|
|
838
|
-
|
|
797
|
+
|
|
839
798
|
},
|
|
840
799
|
},
|
|
841
800
|
milestone: {
|
|
@@ -843,7 +802,7 @@ What must the LLM do?
|
|
|
843
802
|
run: () => {
|
|
844
803
|
const s = getState();
|
|
845
804
|
if (!s.milestone || s.milestone === "—") {
|
|
846
|
-
|
|
805
|
+
|
|
847
806
|
return;
|
|
848
807
|
}
|
|
849
808
|
const candidates = [
|
|
@@ -869,10 +828,7 @@ What must the LLM do?
|
|
|
869
828
|
refreshState();
|
|
870
829
|
updateStatus(ui);
|
|
871
830
|
const s = getState();
|
|
872
|
-
|
|
873
|
-
`soly: reloaded — ${s.milestone} · ${s.phases.length} phases`,
|
|
874
|
-
"info",
|
|
875
|
-
);
|
|
831
|
+
|
|
876
832
|
},
|
|
877
833
|
},
|
|
878
834
|
// ------------------------------------------------------------------
|
package/core.ts
CHANGED
|
@@ -561,7 +561,7 @@ export function buildRulesSection(
|
|
|
561
561
|
|
|
562
562
|
## ⚠️ MANDATORY: soly project rules
|
|
563
563
|
|
|
564
|
-
**These rules are NON-NEGOTIABLE.
|
|
564
|
+
**These rules are NON-NEGOTIABLE. If a rule contradicts your instinct, the rule wins.**
|
|
565
565
|
|
|
566
566
|
${headerHint}
|
|
567
567
|
|
package/init.ts
CHANGED
|
@@ -223,7 +223,7 @@ export async function initSolyProject(
|
|
|
223
223
|
["minimal", "web-app", "library", "cli"],
|
|
224
224
|
);
|
|
225
225
|
if (!pick) {
|
|
226
|
-
|
|
226
|
+
|
|
227
227
|
return { created: false, template: null, projectName };
|
|
228
228
|
}
|
|
229
229
|
template = pick as InitTemplate;
|
|
@@ -237,7 +237,7 @@ export async function initSolyProject(
|
|
|
237
237
|
`Create .agents/ structure in:\n ${cwd}\n\nProject name: ${projectName}`,
|
|
238
238
|
);
|
|
239
239
|
if (!ok) {
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
return { created: false, template: null, projectName };
|
|
242
242
|
}
|
|
243
243
|
}
|
|
@@ -288,11 +288,7 @@ export async function initSolyProject(
|
|
|
288
288
|
for (const extra of TEMPLATE_EXTRAS[template]) {
|
|
289
289
|
created.push(`.agents/${extra.file}`);
|
|
290
290
|
}
|
|
291
|
-
|
|
292
|
-
`soly init: done (${template}). Created:\n - ${created.join("\n - ")}\n\n` +
|
|
293
|
-
`Next:\n 1. Edit \`.agents/docs/vision.md\`\n 2. \`/plan 1\` to start the first phase`,
|
|
294
|
-
"info",
|
|
295
|
-
);
|
|
291
|
+
|
|
296
292
|
return { created: true, template, projectName: safeProjectName };
|
|
297
293
|
}
|
|
298
294
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-soly",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Project management + workflow framework for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker, native footer + welcome chrome — one npm install, zero config. The LLM drives the workflow inline via the soly_workflow tool — no external subagent plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/workflows/done.ts
CHANGED
|
@@ -144,7 +144,7 @@ export function buildDoneTransform(
|
|
|
144
144
|
pushed = true;
|
|
145
145
|
} catch (err) {
|
|
146
146
|
const msg = err instanceof Error ? err.message : String(err);
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
return {
|
|
149
149
|
handled: true,
|
|
150
150
|
transformedText: `Plan ${branchName} committed locally (${commitHash.slice(0, 7)}), but push failed.\n${msg}`,
|
|
@@ -152,7 +152,7 @@ export function buildDoneTransform(
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
} else {
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// 4. Draft PR via gh
|
|
@@ -174,18 +174,10 @@ export function buildDoneTransform(
|
|
|
174
174
|
// gh failed (maybe not authenticated, or PR already exists, etc.)
|
|
175
175
|
// Don't fail the whole workflow — just report.
|
|
176
176
|
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
-
|
|
178
|
-
`soly done: pushed OK, but draft PR creation failed — ${msg}\n` +
|
|
179
|
-
`Run \`gh pr create --draft --fill\` manually.`,
|
|
180
|
-
"warning",
|
|
181
|
-
);
|
|
177
|
+
|
|
182
178
|
}
|
|
183
179
|
} else if (pushed) {
|
|
184
|
-
|
|
185
|
-
`soly done: pushed OK, but \`gh\` CLI not found — draft PR not created.\n` +
|
|
186
|
-
`Install \`gh\` (https://cli.github.com) and run \`gh pr create --draft --fill\` manually.`,
|
|
187
|
-
"info",
|
|
188
|
-
);
|
|
180
|
+
|
|
189
181
|
}
|
|
190
182
|
|
|
191
183
|
// 5. Done — summarize
|
|
@@ -198,7 +190,7 @@ export function buildDoneTransform(
|
|
|
198
190
|
|
|
199
191
|
// (No registry file to update — the plan's git branch IS the source of
|
|
200
192
|
// truth for "active plans". See PLAN.md / "Out of scope" for the trade-off.)
|
|
201
|
-
|
|
193
|
+
|
|
202
194
|
return {
|
|
203
195
|
handled: true,
|
|
204
196
|
transformedText: notice,
|
package/workflows/execute.ts
CHANGED
|
@@ -26,6 +26,23 @@ import {
|
|
|
26
26
|
renderPlanSummaryInline,
|
|
27
27
|
writeIterationContext,
|
|
28
28
|
} from "../iteration.js";
|
|
29
|
+
import { buildVerificationPrompt, parseGoalAndAcceptance } from "./goal-verify.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Append the goal-verification step to a worker close-out block, gated on
|
|
33
|
+
* whether PLAN.md at `planPath` actually has Goal + Acceptance sections.
|
|
34
|
+
* If parsing fails (empty/missing sections), we skip the block rather than
|
|
35
|
+
* inject a malformed prompt — the worker has nothing meaningful to verify.
|
|
36
|
+
*/
|
|
37
|
+
function withGoalCheck(planPath: string, suffix: string): string {
|
|
38
|
+
try {
|
|
39
|
+
const r = parseGoalAndAcceptance(planPath);
|
|
40
|
+
if (!r.ok) return suffix;
|
|
41
|
+
return buildVerificationPrompt(r, planPath) + suffix;
|
|
42
|
+
} catch {
|
|
43
|
+
return suffix;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
29
46
|
|
|
30
47
|
/** Resolve <extension>/workflows-data/<name>.md regardless of cwd. */
|
|
31
48
|
function loadWorkflowMarkdown(name: string): string | null {
|
|
@@ -225,7 +242,10 @@ Hard rules:
|
|
|
225
242
|
- Interactive-only rules are NOT in scope here: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}.
|
|
226
243
|
|
|
227
244
|
When done, confirm the SUMMARY.md was written and the task flipped to \`status: done\`. Then suggest \`soly verify\` to self-review the change with fresh eyes before calling it done.`;
|
|
228
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
handled: true,
|
|
247
|
+
transformedText: withGoalCheck(path.join(task.dir, "PLAN.md"), instruction),
|
|
248
|
+
};
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
// === ALL / FEATURE (new dual-mode, sequential in v0.1) ===
|
|
@@ -348,7 +368,10 @@ Hard rules:
|
|
|
348
368
|
- Do not commit unless the workflow tells you to; the user reviews and merges.
|
|
349
369
|
|
|
350
370
|
When done, suggest \`soly verify\` to self-review with fresh eyes, then \`soly done ${target.raw}\` to open the PR.`;
|
|
351
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
handled: true,
|
|
373
|
+
transformedText: withGoalCheck(planFile, instruction),
|
|
374
|
+
};
|
|
352
375
|
}
|
|
353
376
|
|
|
354
377
|
// === PHASE MODE ===
|
|
@@ -480,5 +503,12 @@ Hard rules:
|
|
|
480
503
|
|
|
481
504
|
When done, confirm STATE.md was updated. Then suggest \`soly verify\` to self-review the work with fresh eyes before calling the phase done.`;
|
|
482
505
|
|
|
483
|
-
|
|
506
|
+
// Goal verification only makes sense for plan-level phase execution —
|
|
507
|
+
// wave-based / task-based execution iterates many plans and has no
|
|
508
|
+
// single PLAN.md to verify against.
|
|
509
|
+
const phasePlanPath = isPlanLevel && planFileResolved ? planFileResolved : null;
|
|
510
|
+
return {
|
|
511
|
+
handled: true,
|
|
512
|
+
transformedText: phasePlanPath ? withGoalCheck(phasePlanPath, instruction) : instruction,
|
|
513
|
+
};
|
|
484
514
|
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// workflows/goal-verify.ts — Goal & Acceptance verification for `soly execute`
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Reads PLAN.md `## Goal` + `## Acceptance`, hands the LLM a structured
|
|
6
|
+
// "verify against the diff" prompt at end of execute, and lets the worker
|
|
7
|
+
// append a `## Status` section (idempotent, fenced by marker comments).
|
|
8
|
+
//
|
|
9
|
+
// Pure LLM review — no `bun/dotnet/pytest`-style mechanical checks. This
|
|
10
|
+
// plugin is multi-language; mechanical checks would be unreliable. The LLM
|
|
11
|
+
// walks `git diff master..HEAD` itself and judges.
|
|
12
|
+
//
|
|
13
|
+
// Headless-safe by construction: this module MUST NOT import `ask_pro`,
|
|
14
|
+
// `decision_deck`, `@earendil-works/pi-tui`, or call `ctx.ui.custom(...)`.
|
|
15
|
+
// A regression test greps the source for those tokens.
|
|
16
|
+
//
|
|
17
|
+
// Three public surfaces:
|
|
18
|
+
// 1. `parseGoalAndAcceptance(planPath)` — read PLAN.md, return parsed
|
|
19
|
+
// sections or `{ ok:false, error }`.
|
|
20
|
+
// 2. `buildVerificationPrompt(parsed, planPath)` — instruction string for
|
|
21
|
+
// the execute worker.
|
|
22
|
+
// 3. `appendStatus(planPath, report)` — idempotent append/replace of
|
|
23
|
+
// the `## Status` section.
|
|
24
|
+
//
|
|
25
|
+
// Pure helpers (`buildStatusSection`, `findMarkerBlock`) are exported for tests.
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
import * as fs from "node:fs";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Public types
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export type GoalAcceptanceParsed = {
|
|
35
|
+
ok: true;
|
|
36
|
+
goal: string;
|
|
37
|
+
acceptance: string[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type GoalAcceptanceError = {
|
|
41
|
+
ok: false;
|
|
42
|
+
error: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type GoalAcceptanceResult = GoalAcceptanceParsed | GoalAcceptanceError;
|
|
46
|
+
|
|
47
|
+
export type StatusAcceptanceItem = {
|
|
48
|
+
item: string;
|
|
49
|
+
met: boolean;
|
|
50
|
+
evidence: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type StatusReport = {
|
|
54
|
+
goalMet: boolean;
|
|
55
|
+
acceptance: StatusAcceptanceItem[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const STATUS_BEGIN = "<!-- soly:status:begin -->";
|
|
59
|
+
export const STATUS_END = "<!-- soly:status:end -->";
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Parser
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse a PLAN.md and extract the `## Goal` text + each `- [ ]` bullet from
|
|
67
|
+
* `## Acceptance`. Never throws — returns `{ ok:false, error }` so callers
|
|
68
|
+
* (the verifier prompt) can surface a clean gap report instead of crashing
|
|
69
|
+
* the execute worker.
|
|
70
|
+
*
|
|
71
|
+
* Tolerates frontmatter at the top of the file. Stops each section at the
|
|
72
|
+
* next `## ` heading (case-insensitive on the marker). Multiple
|
|
73
|
+
* `## Acceptance` sections: only the first is used.
|
|
74
|
+
*/
|
|
75
|
+
export function parseGoalAndAcceptance(planPath: string): GoalAcceptanceResult {
|
|
76
|
+
let raw: string;
|
|
77
|
+
try {
|
|
78
|
+
raw = fs.readFileSync(planPath, "utf-8");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
return { ok: false, error: `Cannot read PLAN.md at ${planPath}: ${msg}` };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Strip optional frontmatter (--- ... --- at top of file).
|
|
85
|
+
const body = stripFrontmatter(raw);
|
|
86
|
+
|
|
87
|
+
const goalText = extractSectionBody(body, "Goal");
|
|
88
|
+
if (goalText === null) {
|
|
89
|
+
return { ok: false, error: "PLAN.md is missing required `## Goal` section." };
|
|
90
|
+
}
|
|
91
|
+
if (goalText.trim().length === 0) {
|
|
92
|
+
return { ok: false, error: "`## Goal` section is empty." };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const acceptanceText = extractSectionBody(body, "Acceptance");
|
|
96
|
+
if (acceptanceText === null) {
|
|
97
|
+
return { ok: false, error: "PLAN.md is missing required `## Acceptance` section." };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const items = extractAcceptanceItems(acceptanceText);
|
|
101
|
+
if (items.length === 0) {
|
|
102
|
+
return { ok: false, error: "`## Acceptance` section has no `- [ ]` items." };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { ok: true, goal: goalText.trim(), acceptance: items };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Strip YAML frontmatter (`---\n...\n---\n`) at the top of the file. */
|
|
109
|
+
function stripFrontmatter(raw: string): string {
|
|
110
|
+
const m = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
111
|
+
return m ? raw.slice(m[0].length) : raw;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract the body of the FIRST `## <name>` section: everything from the
|
|
116
|
+
* heading line (exclusive) up to the next `## ` heading or end of file.
|
|
117
|
+
* Returns null if the section does not exist. Returns "" if the section
|
|
118
|
+
* exists but is empty.
|
|
119
|
+
*/
|
|
120
|
+
function extractSectionBody(body: string, name: string): string | null {
|
|
121
|
+
const lines = body.split(/\r?\n/);
|
|
122
|
+
const headingRe = new RegExp(`^##\\s+${escapeRegex(name)}\\s*$`);
|
|
123
|
+
const nextHeadingRe = /^##\s+\S+/;
|
|
124
|
+
let startIdx = -1;
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
if (headingRe.test(lines[i] ?? "")) {
|
|
127
|
+
startIdx = i;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (startIdx === -1) return null;
|
|
132
|
+
let endIdx = lines.length;
|
|
133
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
134
|
+
if (nextHeadingRe.test(lines[i] ?? "")) {
|
|
135
|
+
endIdx = i;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return lines.slice(startIdx + 1, endIdx).join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Pull each `- [ ] <text>` bullet (allowing leading whitespace) from a section body. */
|
|
143
|
+
function extractAcceptanceItems(section: string): string[] {
|
|
144
|
+
const items: string[] = [];
|
|
145
|
+
for (const line of section.split(/\r?\n/)) {
|
|
146
|
+
// Require exactly `[ ]` (one space inside the brackets) — matches the
|
|
147
|
+
// standard markdown task-list convention. Lines like `- [x] foo` or
|
|
148
|
+
// `- [] foo` are intentionally NOT accepted: `x` is checked, and `[]`
|
|
149
|
+
// (no inner space) is a typo we'd rather surface than silently keep.
|
|
150
|
+
const m = line.match(/^\s*-\s+\[\s\]\s+(.+?)\s*$/);
|
|
151
|
+
if (m) items.push(m[1]);
|
|
152
|
+
}
|
|
153
|
+
return items;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Escape a literal string for safe inclusion in a RegExp constructor. */
|
|
157
|
+
function escapeRegex(s: string): string {
|
|
158
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Prompt builder
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build the LLM instruction injected at the end of an execute worker block
|
|
167
|
+
* (task / plan / phase plan-level). The worker is told to read PLAN.md,
|
|
168
|
+
* walk `git diff master..HEAD`, judge each acceptance item, judge the
|
|
169
|
+
* Goal as a whole, and append/replace a `## Status` section between the
|
|
170
|
+
* marker comments.
|
|
171
|
+
*
|
|
172
|
+
* `planPathHint` is a textual hint about where PLAN.md lives — typically the
|
|
173
|
+
* absolute path. The worker resolves it via `read`.
|
|
174
|
+
*/
|
|
175
|
+
export function buildVerificationPrompt(parsed: GoalAcceptanceParsed, planPathHint: string): string {
|
|
176
|
+
const itemsList = parsed.acceptance.map((a) => `- ${a}`).join("\n");
|
|
177
|
+
return `
|
|
178
|
+
|
|
179
|
+
GOAL VERIFICATION (mandatory before close-out):
|
|
180
|
+
|
|
181
|
+
Before reporting this work as done, verify that the diff actually
|
|
182
|
+
delivers the plan's Goal and meets every Acceptance item. This is a
|
|
183
|
+
hard gate — if it fails, you must NOT call \`soly done\`; you must
|
|
184
|
+
report the gap and stop.
|
|
185
|
+
|
|
186
|
+
PLAN.md: ${planPathHint}
|
|
187
|
+
|
|
188
|
+
Goal (verbatim from PLAN.md):
|
|
189
|
+
|
|
190
|
+
"""
|
|
191
|
+
${parsed.goal}
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
Acceptance items (verbatim, ${parsed.acceptance.length} total):
|
|
195
|
+
|
|
196
|
+
${itemsList}
|
|
197
|
+
|
|
198
|
+
Steps:
|
|
199
|
+
1. Read ${planPathHint} (use the read tool) to confirm the Goal and
|
|
200
|
+
Acceptance text.
|
|
201
|
+
2. Run \`git diff master..HEAD\` (or \`git diff origin/master..HEAD\`
|
|
202
|
+
if no local master) to see what actually changed.
|
|
203
|
+
3. For each Acceptance item above, judge met / not-met. Cite evidence
|
|
204
|
+
— file:line from the diff, or a short reasoning string.
|
|
205
|
+
4. Judge the Goal as a whole against the actual outcome.
|
|
206
|
+
5. Use the edit tool to append or replace a \`## Status\` section in
|
|
207
|
+
${planPathHint}, between the marker comments:
|
|
208
|
+
|
|
209
|
+
${STATUS_BEGIN}
|
|
210
|
+
## Status
|
|
211
|
+
|
|
212
|
+
**Goal met:** YES | NO
|
|
213
|
+
|
|
214
|
+
### Acceptance
|
|
215
|
+
- [x] <item text> — <evidence>
|
|
216
|
+
- [ ] <item text> — GAP: <what is missing>
|
|
217
|
+
|
|
218
|
+
**Verdict:** PASS | BLOCKED — gaps remain
|
|
219
|
+
${STATUS_END}
|
|
220
|
+
|
|
221
|
+
If the markers already exist in the file, replace the content
|
|
222
|
+
between them. If they don't, append the block at end of file. Do
|
|
223
|
+
NOT touch any text outside the markers.
|
|
224
|
+
6. If Verdict is BLOCKED: STOP. Do NOT call \`soly done\`. Report the
|
|
225
|
+
gap inline and list which Acceptance items remain unmet.
|
|
226
|
+
7. If Verdict is PASS: proceed to the close-out steps below.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Status section appender
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/** Pure: build the Status markdown block between the marker comments. */
|
|
237
|
+
export function buildStatusSection(report: StatusReport): string {
|
|
238
|
+
const allMet = report.goalMet && report.acceptance.every((a) => a.met);
|
|
239
|
+
const verdict = allMet ? "PASS" : "BLOCKED — gaps remain";
|
|
240
|
+
const lines: string[] = [];
|
|
241
|
+
lines.push(STATUS_BEGIN);
|
|
242
|
+
lines.push("## Status");
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push(`**Goal met:** ${report.goalMet ? "YES" : "NO"}`);
|
|
245
|
+
lines.push("");
|
|
246
|
+
lines.push("### Acceptance");
|
|
247
|
+
for (const a of report.acceptance) {
|
|
248
|
+
const box = a.met ? "[x]" : "[ ]";
|
|
249
|
+
const tag = a.met ? "— " : "— GAP: ";
|
|
250
|
+
lines.push(`- ${box} ${a.item} ${tag}${a.evidence}`);
|
|
251
|
+
}
|
|
252
|
+
lines.push("");
|
|
253
|
+
lines.push(`**Verdict:** ${verdict}`);
|
|
254
|
+
lines.push(STATUS_END);
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Find the marker block in `raw`. Returns:
|
|
260
|
+
* { begin, end } — slice indices (begin = index of STATUS_BEGIN,
|
|
261
|
+
* end = index AFTER STATUS_END), or null if either
|
|
262
|
+
* marker is missing or the order is wrong.
|
|
263
|
+
*/
|
|
264
|
+
export function findMarkerBlock(raw: string): { begin: number; end: number } | null {
|
|
265
|
+
const begin = raw.indexOf(STATUS_BEGIN);
|
|
266
|
+
const endIdx = raw.indexOf(STATUS_END);
|
|
267
|
+
if (begin === -1 || endIdx === -1 || endIdx < begin) return null;
|
|
268
|
+
return { begin, end: endIdx + STATUS_END.length };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Append or replace the `## Status` section in PLAN.md. Idempotent: a
|
|
273
|
+
* second run replaces the previous block (between marker comments)
|
|
274
|
+
* instead of duplicating content. Never touches text outside the
|
|
275
|
+
* marker block.
|
|
276
|
+
*/
|
|
277
|
+
export function appendStatus(planPath: string, report: StatusReport): void {
|
|
278
|
+
const raw = fs.readFileSync(planPath, "utf-8");
|
|
279
|
+
const section = buildStatusSection(report);
|
|
280
|
+
const block = findMarkerBlock(raw);
|
|
281
|
+
|
|
282
|
+
let next: string;
|
|
283
|
+
if (block) {
|
|
284
|
+
// Replace between markers, preserve prefix and suffix content.
|
|
285
|
+
const before = raw.slice(0, block.begin).replace(/\s+$/, "");
|
|
286
|
+
const after = raw.slice(block.end).replace(/^\s+/, "");
|
|
287
|
+
const parts = [before, section];
|
|
288
|
+
if (after.length > 0) parts.push(after);
|
|
289
|
+
next = parts.join("\n\n") + "\n";
|
|
290
|
+
} else {
|
|
291
|
+
// Append at end of file.
|
|
292
|
+
const trimmed = raw.replace(/\s+$/, "");
|
|
293
|
+
next = `${trimmed}\n\n${section}\n`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fs.writeFileSync(planPath, next, "utf-8");
|
|
297
|
+
}
|
package/workflows/inspect.ts
CHANGED
|
@@ -248,7 +248,7 @@ export function showDoctor(_cmd: unknown, state: SolyState, ui: InspectUI, confi
|
|
|
248
248
|
out.push("");
|
|
249
249
|
out.push("Warnings present — run `/soly iterations` and `/soly config` to review.");
|
|
250
250
|
}
|
|
251
|
-
|
|
251
|
+
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
// ---------------------------------------------------------------------------
|
|
@@ -287,10 +287,7 @@ export function showTodos(
|
|
|
287
287
|
}
|
|
288
288
|
const file = findTodosFile(state.solyDir);
|
|
289
289
|
if (!file) {
|
|
290
|
-
|
|
291
|
-
"soly todos: no todo file found. Install the `pi-todo` extension or write `.agents/todos.json` manually.",
|
|
292
|
-
"info",
|
|
293
|
-
);
|
|
290
|
+
|
|
294
291
|
return;
|
|
295
292
|
}
|
|
296
293
|
let parsed: { todos?: Array<{ content?: string; status?: string; activeForm?: string }> } | null = null;
|
|
@@ -301,7 +298,7 @@ export function showTodos(
|
|
|
301
298
|
return;
|
|
302
299
|
}
|
|
303
300
|
if (!parsed || !Array.isArray(parsed.todos) || parsed.todos.length === 0) {
|
|
304
|
-
|
|
301
|
+
|
|
305
302
|
return;
|
|
306
303
|
}
|
|
307
304
|
const todos = parsed.todos;
|
|
@@ -316,7 +313,7 @@ export function showTodos(
|
|
|
316
313
|
const suffix = t.status === "in_progress" && typeof t.activeForm === "string" ? ` (${t.activeForm})` : "";
|
|
317
314
|
lines.push(` ${mark} ${t.content}${suffix}`);
|
|
318
315
|
}
|
|
319
|
-
|
|
316
|
+
|
|
320
317
|
}
|
|
321
318
|
|
|
322
319
|
// ---------------------------------------------------------------------------
|
|
@@ -335,7 +332,7 @@ export function showIterations(
|
|
|
335
332
|
}
|
|
336
333
|
const iterDir = path.join(state.solyDir, "iterations");
|
|
337
334
|
if (!fs.existsSync(iterDir)) {
|
|
338
|
-
|
|
335
|
+
|
|
339
336
|
return;
|
|
340
337
|
}
|
|
341
338
|
|
|
@@ -362,7 +359,7 @@ export function showIterations(
|
|
|
362
359
|
.slice(0, limit);
|
|
363
360
|
|
|
364
361
|
if (files.length === 0) {
|
|
365
|
-
|
|
362
|
+
|
|
366
363
|
return;
|
|
367
364
|
}
|
|
368
365
|
|
|
@@ -378,7 +375,7 @@ export function showIterations(
|
|
|
378
375
|
out.push("");
|
|
379
376
|
out.push("Tip: `soly iterations 20` for more, `soly diff iterations <a> <b>` to compare two.");
|
|
380
377
|
}
|
|
381
|
-
|
|
378
|
+
|
|
382
379
|
}
|
|
383
380
|
|
|
384
381
|
function humanizeAge(ms: number): string {
|
|
@@ -444,7 +441,7 @@ export function showDiffIterations(
|
|
|
444
441
|
out.push(bodyB);
|
|
445
442
|
out.push("--- END B ---");
|
|
446
443
|
}
|
|
447
|
-
|
|
444
|
+
|
|
448
445
|
}
|
|
449
446
|
|
|
450
447
|
// ---------------------------------------------------------------------------
|
|
@@ -495,5 +492,5 @@ export function showPhaseDelete(
|
|
|
495
492
|
out.push("");
|
|
496
493
|
out.push("To restore: `mv` it back to .agents/phases/");
|
|
497
494
|
out.push("To permanently delete: `rm -rf " + dest + "`");
|
|
498
|
-
|
|
495
|
+
|
|
499
496
|
}
|
package/workflows/migrate.ts
CHANGED
|
@@ -167,6 +167,6 @@ export function buildMigrateTransform(
|
|
|
167
167
|
? skipped.map((s) => ` ! ${s.phase}: ${s.reason}`).join("\n") + "\n"
|
|
168
168
|
: "") +
|
|
169
169
|
`\nPush and PR the migration branches manually: \`git push origin <branch>\`.`;
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
return { handled: true, transformedText: notice, migrated, skipped };
|
|
172
172
|
}
|
package/workflows/new.ts
CHANGED
|
@@ -164,7 +164,7 @@ export function buildNewTransform(
|
|
|
164
164
|
const notice = branchExisted
|
|
165
165
|
? `Plan '${name}' reused on existing branch ${branchName}.\nPLAN.md was ${planFile}.\nNext: \`soly plan ${branchName}\``
|
|
166
166
|
: `Plan '${name}' scaffolded on new branch ${branchName}.\nPLAN.md: ${planFile}\nNext: \`soly plan ${branchName}\``;
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
return {
|
|
169
169
|
handled: true,
|
|
170
170
|
transformedText: notice,
|
package/workflows/quick.ts
CHANGED
|
@@ -100,7 +100,7 @@ export function showStatus(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// =============================================================================
|
|
@@ -136,7 +136,7 @@ export function showLog(cmd: SolyCommand, state: SolyState, ui: QuickUI): void {
|
|
|
136
136
|
const lines = raw.split(/\r?\n/);
|
|
137
137
|
const decisionsIdx = lines.findIndex((l) => DECISIONS_HEADER.test(l));
|
|
138
138
|
if (decisionsIdx === -1) {
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -151,7 +151,7 @@ export function showLog(cmd: SolyCommand, state: SolyState, ui: QuickUI): void {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
if (rows.length === 0) {
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -178,7 +178,7 @@ export function showLog(cmd: SolyCommand, state: SolyState, ui: QuickUI): void {
|
|
|
178
178
|
out.push(` ${r.rationale}`);
|
|
179
179
|
out.push("");
|
|
180
180
|
}
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
// =============================================================================
|
|
@@ -254,5 +254,5 @@ export async function showDiff(
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
|
|
258
258
|
}
|