pi-goal-list-loop-audit 0.26.9 → 0.27.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.
@@ -37,6 +37,36 @@ export function truncate(s: string, max: number): string {
37
37
  return s.length <= max ? s : s.slice(0, Math.max(0, max - 1)) + "…";
38
38
  }
39
39
 
40
+ /**
41
+ * Word-wrap to `width`, capped at `maxLines` (v0.27.1). A pause is the one
42
+ * state where the FULL text matters — the reason often carries a decision
43
+ * the user must make (dedup choices, impossible-verdict narrowing), and a
44
+ * 60-char truncate hid it. Over-long words are hard-split; when the cap
45
+ * cuts content the last line ends with "…" (the pause-time notification
46
+ * and /goal status always carry the full text).
47
+ */
48
+ export function wrap(s: string, width: number, maxLines: number): string[] {
49
+ const norm = s.replace(/\s+/g, " ").trim();
50
+ const words = norm.split(" ").filter(Boolean);
51
+ const all: string[] = [];
52
+ let cur = "";
53
+ for (let w of words) {
54
+ const next = cur ? `${cur} ${w}` : w;
55
+ if (next.length <= width) { cur = next; continue; }
56
+ if (cur) all.push(cur);
57
+ while (w.length > width) { all.push(w.slice(0, width)); w = w.slice(width); }
58
+ cur = w;
59
+ }
60
+ if (cur) all.push(cur);
61
+ if (all.length === 0) all.push("");
62
+ if (all.length <= maxLines) return all;
63
+ const out = all.slice(0, maxLines);
64
+ // The last kept line already fits within width — truncate() would leave it
65
+ // unmarked, so force the ellipsis to signal "more in /goal status".
66
+ out[maxLines - 1] = out[maxLines - 1]!.slice(0, Math.max(0, width - 1)) + "…";
67
+ return out;
68
+ }
69
+
40
70
  /**
41
71
  * Width-aware truncation budget (v0.22.2). The hardcoded caps are FLOORS for
42
72
  * narrow terminals; when the terminal is wider, lines may use the available
@@ -191,8 +221,29 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
191
221
  return lines;
192
222
  }
193
223
  if (g.status === "paused" && g.pauseReason) {
194
- lines.push(`├─ ${paint(theme, pauseIsError(g) ? "error" : "warning", truncate(g.pauseReason, budgetFor(width, 3, 60)))}`);
195
- if (g.pauseSuggestedAction) lines.push(`└─ ${paint(theme, "dim", truncate(g.pauseSuggestedAction, budgetFor(width, 3, 60)))}`);
224
+ const isErr = pauseIsError(g);
225
+ const budget = budgetFor(width, 3, 60);
226
+ // v0.27.1: wrap reason + suggested action over up to 3 lines each
227
+ // (see wrap()); before, both were truncated at ~60 chars and the actual
228
+ // question in a decision-pause never reached the user.
229
+ wrap(g.pauseReason, budget, 3).forEach((w, i) => {
230
+ lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, isErr ? "error" : "warning", w)}`);
231
+ });
232
+ // v0.27.1: what survives the pause — the first question at a pause is
233
+ // "did I lose the work?". Answer it on the card.
234
+ const spent: string[] = [];
235
+ const tokUsed = g.usage?.tokensUsed ?? 0;
236
+ if (tokUsed > 0) spent.push(`${fmtTokens(tokUsed)} tok spent`);
237
+ const audits = g.auditHistory?.length ?? 0;
238
+ if (audits > 0) spent.push(`${audits} audit${audits === 1 ? "" : "s"}`);
239
+ const savedLine = `saved${spent.length > 0 ? ` — ${spent.join(" · ")}` : ""} · resumes exactly here`;
240
+ if (g.pauseSuggestedAction) {
241
+ lines.push(`├─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
242
+ const wrapped = wrap(g.pauseSuggestedAction, budget, 3);
243
+ wrapped.forEach((w, i) => lines.push(`${i === wrapped.length - 1 ? "└─" : "│ "} ${paint(theme, "dim", w)}`));
244
+ } else {
245
+ lines.push(`└─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
246
+ }
196
247
  return lines;
197
248
  }
198
249
  const next = nextPending(g);
@@ -2254,8 +2254,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2254
2254
  pauseReason: p.reason,
2255
2255
  pauseSuggestedAction: p.suggestedAction,
2256
2256
  }, ctx);
2257
- ctx.ui.notify(`Goal paused: ${p.reason}`, "info");
2258
- notifyExternal(ctx, `Goal paused: ${p.reason.slice(0, 120)}`);
2257
+ // v0.27.1: surface the FULL pause contract — reason AND suggested
2258
+ // action. Before, the action only appeared in /goal status and the
2259
+ // widget truncated both at ~60 chars, so decision-pauses ("choose a
2260
+ // or b") reached the user as an unreadable fragment.
2261
+ ctx.ui.notify(`Goal paused: ${p.reason}${p.suggestedAction ? `\n\n→ ${p.suggestedAction}` : ""}`, "info");
2262
+ notifyExternal(ctx, `Goal paused: ${(p.suggestedAction ? `${p.reason} → ${p.suggestedAction}` : p.reason).slice(0, 200)}`);
2259
2263
  return { content: [{ type: "text", text: "Goal paused. /goal resume to continue." }], details: {} };
2260
2264
  },
2261
2265
  }));
@@ -2852,66 +2856,93 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2852
2856
  const v = p.value === undefined ? fallback : String(p.value);
2853
2857
  return `${v} [${p.source}]`;
2854
2858
  };
2859
+ const settings = loadSettings(ctx.cwd);
2860
+ // v0.27.0: every option on ONE screen, grouped into sections, each row
2861
+ // carrying `label — value [provenance] — what it does` so the menu is
2862
+ // also the documentation. Header rows are no-ops when selected.
2863
+ const rows = [
2864
+ "── Keep-going ──",
2865
+ `Auto-resume on load — ${show("autoResume", "default")} — on: resume on session load too · off: never · default: hold on load, resume on reload/fork`,
2866
+ `Auto-accept drafts — ${show("autoAcceptDrafts", "(off)")} — on: goal/loop drafts activate without the Confirm dialog (unattended rigs)`,
2867
+ `Aggressive mode — ${show("aggressiveMode", "(off)")} — flips DEFAULTS toward keep-going (autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs); explicit per-key settings still win`,
2868
+ "── Auditor ──",
2869
+ `Auditor model — ${show("auditorModel", "(pi session model)")} — provider/model override for the isolated auditor`,
2870
+ `Auditor thinking — ${show("auditorThinkingLevel", "(session, floor high)")} — thinking level for the auditor session`,
2871
+ `Audit cap — ${show("auditCap", "(5)")} — pause the goal after N consecutive disapprovals (0 = unlimited)`,
2872
+ `Audit feedback chars — ${show("auditFeedbackChars", "(full report)")} — cap the executor-visible disapproval report (0 = full report)`,
2873
+ `Quota retry minutes — ${show("quotaRetryMinutes", `(${DEFAULT_QUOTA_RETRY_MINUTES})`)} — auto-retry a quota-exhausted auditor after N minutes`,
2874
+ "── Stall brakes ──",
2875
+ `Wedge alert minutes — ${show("wedgeAlertMinutes", `(${WEDGE_ALERT_DEFAULT_MINUTES})`)} — hung-command alert while the session is busy (0 = off)`,
2876
+ `Stuck max interventions — ${show("stuckMaxInterventions", "(5)")} — consecutive stuck interventions before a loop stops`,
2877
+ `Stall escalation refires — ${show("stallEscalationRefires", "(5)")} — heartbeat refires with no turn before the goal pauses / loop stops (0 = never)`,
2878
+ "── Subagents ──",
2879
+ `Subagent model strategy — ${show("subagentModelStrategy", "(inherit-parent)")} — inherit-parent shares your session model+quota; agent-default pins haiku for Explore`,
2880
+ `Subagent Explore pin — ${settings.subagentModelOverrides?.Explore ?? "(follows strategy)"} — provider/model pin; always wins over strategy`,
2881
+ `Subagent Plan pin — ${settings.subagentModelOverrides?.Plan ?? "(follows strategy)"} — provider/model pin; always wins over strategy`,
2882
+ `Subagent general-purpose pin — ${settings.subagentModelOverrides?.["general-purpose"] ?? "(follows strategy)"} — provider/model pin; always wins over strategy`,
2883
+ "── Other ──",
2884
+ `Notify command — ${show("notifyCmd", "(off)")} — desktop push command; the event message is passed as $1`,
2885
+ `Token limit per goal — ${show("tokenLimit", "(off)")} — per-goal token budget; pause when exceeded (0 = off)`,
2886
+ `Reviewer config… — open the reviewer menu (post-completion follow-up enqueuer: mode, triggers, cascade, caps)`,
2887
+ "Done",
2888
+ ];
2855
2889
  let choice: string | undefined;
2856
2890
  try {
2857
2891
  choice = await ctx.ui.select(
2858
2892
  `pi-goal-list-loop-audit settings — global: ${globalSettingsPath()}`,
2859
- [
2860
- `Auditor model override — ${show("auditorModel", "(pi session model)")}`,
2861
- `Auditor thinking — ${show("auditorThinkingLevel", "(session, floor high)")}`,
2862
- `Notify command — ${show("notifyCmd", "(off)")}`,
2863
- `Token limit per goal — ${show("tokenLimit", "(off)")}`,
2864
- `Wedge alert minutes — ${show("wedgeAlertMinutes", `(${WEDGE_ALERT_DEFAULT_MINUTES}m default)`)}`,
2865
- `Aggressive mode — ${show("aggressiveMode", "(off)")}`,
2866
- `Quota retry minutes — ${show("quotaRetryMinutes", `(${DEFAULT_QUOTA_RETRY_MINUTES}m default)`)}`,
2867
- `Stuck max interventions — ${show("stuckMaxInterventions", "(5 default)")}`,
2868
- `Subagent model strategy — ${show("subagentModelStrategy", "(inherit-parent)")}`,
2869
- `Subagent Explore model pin — ${loadSettings(ctx.cwd).subagentModelOverrides?.Explore ?? "(follows strategy)"}`,
2870
- `Subagent Plan model pin — ${loadSettings(ctx.cwd).subagentModelOverrides?.Plan ?? "(follows strategy)"}`,
2871
- `Subagent general-purpose model pin — ${loadSettings(ctx.cwd).subagentModelOverrides?.["general-purpose"] ?? "(follows strategy)"}`,
2872
- `Audit feedback characters — ${show("auditFeedbackChars", "(full report)")}`,
2873
- "Done",
2874
- ],
2893
+ rows,
2875
2894
  );
2876
2895
  } catch {
2877
2896
  return;
2878
2897
  }
2879
2898
  if (!choice || choice === "Done") return;
2899
+ if (choice.startsWith("──")) continue; // section header — no-op
2880
2900
  try {
2881
- if (choice.startsWith("Auditor model")) {
2901
+ if (choice.startsWith("Auto-resume")) {
2902
+ const v = await ctx.ui.select("Auto-resume goals/loops on session start", [
2903
+ "default — HOLD when a session is loaded (popup shows what waits); auto-resume on reload/fork so machinery never strands work",
2904
+ "on — auto-resume on EVERY session start (unattended rigs)",
2905
+ "off — never auto-resume; always wait for an explicit resume",
2906
+ ]);
2907
+ if (v) saveSettings("global", ctx.cwd, { autoResume: v.startsWith("on") ? true : v.startsWith("off") ? false : undefined });
2908
+ } else if (choice.startsWith("Auto-accept drafts")) {
2909
+ const v = await ctx.ui.select("Auto-accept goal/loop drafts", [
2910
+ "off — the Confirm dialog gates every draft",
2911
+ "on — drafts activate immediately, no Confirm (unattended rigs)",
2912
+ ]);
2913
+ if (v) saveSettings("global", ctx.cwd, { autoAcceptDrafts: v.startsWith("on") ? true : undefined });
2914
+ } else if (choice.startsWith("Aggressive mode")) {
2915
+ const v = await ctx.ui.select("Aggressive mode (flips DEFAULTS toward keep-going — explicit per-key settings still win)", [
2916
+ "off — current behavior: pause at the audit cap, wedge alerts on, manual resume",
2917
+ "on — autoResume, audit cap 10, stuck max 10, wedge alerts off, quota auto-retry, cap disapprovals become a TODO list and the goal KEEPS GOING",
2918
+ ]);
2919
+ if (v) {
2920
+ saveSettings("global", ctx.cwd, { aggressiveMode: v.startsWith("on") });
2921
+ ctx.ui.notify(`Aggressive mode ${v.startsWith("on") ? "ON — goals keep going past the audit cap; objections become TODOs" : "off"}.`, "info");
2922
+ }
2923
+ } else if (choice.startsWith("Auditor model")) {
2882
2924
  const v = await ctx.ui.input("Auditor model override", "provider/model-id — empty keeps the pi session model");
2883
2925
  if (v !== undefined) saveSettings("global", ctx.cwd, { auditorModel: v.trim() || undefined });
2884
2926
  } else if (choice.startsWith("Auditor thinking")) {
2885
2927
  const v = await ctx.ui.select("Auditor thinking level", ["off", "minimal", "low", "medium", "high", "xhigh"]);
2886
2928
  if (v) saveSettings("global", ctx.cwd, { auditorThinkingLevel: v as Settings["auditorThinkingLevel"] });
2887
- } else if (choice.startsWith("Notify command")) {
2888
- const v = await ctx.ui.input("Notify command the event message is passed as $1", "e.g. a desktop-notification or push command; empty = off");
2889
- if (v !== undefined) saveSettings("global", ctx.cwd, { notifyCmd: v.trim() || undefined });
2890
- } else if (choice.startsWith("Token limit")) {
2891
- const v = await ctx.ui.input("Per-goal token budget", "non-negative integer; 0 or empty = off (no cap)");
2929
+ } else if (choice.startsWith("Audit cap")) {
2930
+ const v = await ctx.ui.input("Consecutive auditor disapprovals before the goal pauses", "non-negative integer; 0 = unlimited, empty = default 5");
2892
2931
  if (v !== undefined) {
2893
2932
  const n = Number.parseInt(v.trim(), 10);
2894
- if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { tokenLimit: n });
2895
- else if (!v.trim()) saveSettings("global", ctx.cwd, { tokenLimit: undefined });
2933
+ if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { auditCap: n });
2934
+ else if (!v.trim()) saveSettings("global", ctx.cwd, { auditCap: undefined });
2896
2935
  else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
2897
2936
  }
2898
- } else if (choice.startsWith("Wedge alert")) {
2899
- const v = await ctx.ui.input("Wedge alert threshold (minutes)", "non-negative integer; 0 = off, empty = default 30");
2937
+ } else if (choice.startsWith("Audit feedback")) {
2938
+ const v = await ctx.ui.input("Auditor feedback returned to the executor (characters)", "non-negative integer cap; 0 or empty = full report (default)");
2900
2939
  if (v !== undefined) {
2901
- const n = Number.parseInt(v.trim(), 10);
2902
- if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: n });
2903
- else if (!v.trim()) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: undefined });
2940
+ const raw = v.trim();
2941
+ const n = Number(raw);
2942
+ if (/^\d+$/.test(raw) && Number.isSafeInteger(n)) saveSettings("global", ctx.cwd, { auditFeedbackChars: n });
2943
+ else if (!v.trim()) saveSettings("global", ctx.cwd, { auditFeedbackChars: undefined });
2904
2944
  else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
2905
2945
  }
2906
- } else if (choice.startsWith("Aggressive mode")) {
2907
- const v = await ctx.ui.select("Aggressive mode (flips DEFAULTS toward keep-going — explicit per-key settings still win)", [
2908
- "off — current behavior: pause at the audit cap, wedge alerts on, manual resume",
2909
- "on — autoResume, audit cap 10, stuck max 10, wedge alerts off, quota auto-retry, cap disapprovals become a TODO list and the goal KEEPS GOING",
2910
- ]);
2911
- if (v) {
2912
- saveSettings("global", ctx.cwd, { aggressiveMode: v.startsWith("on") });
2913
- ctx.ui.notify(`Aggressive mode ${v.startsWith("on") ? "ON — goals keep going past the audit cap; objections become TODOs" : "off"}.`, "info");
2914
- }
2915
2946
  } else if (choice.startsWith("Quota retry minutes")) {
2916
2947
  const v = await ctx.ui.input("Minutes before auto-retrying a quota-exhausted auditor", `positive integer; empty = default ${DEFAULT_QUOTA_RETRY_MINUTES}`);
2917
2948
  if (v !== undefined) {
@@ -2920,6 +2951,14 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2920
2951
  else if (!v.trim()) saveSettings("global", ctx.cwd, { quotaRetryMinutes: undefined });
2921
2952
  else ctx.ui.notify(`Not a positive integer: ${v}`, "warning");
2922
2953
  }
2954
+ } else if (choice.startsWith("Wedge alert")) {
2955
+ const v = await ctx.ui.input("Wedge alert threshold (minutes)", "non-negative integer; 0 = off, empty = default 30");
2956
+ if (v !== undefined) {
2957
+ const n = Number.parseInt(v.trim(), 10);
2958
+ if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: n });
2959
+ else if (!v.trim()) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: undefined });
2960
+ else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
2961
+ }
2923
2962
  } else if (choice.startsWith("Stuck max interventions")) {
2924
2963
  const v = await ctx.ui.input("Consecutive stuck interventions before a loop stops", "positive integer; empty = default 5 (10 under aggressiveMode)");
2925
2964
  if (v !== undefined) {
@@ -2928,6 +2967,14 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2928
2967
  else if (!v.trim()) saveSettings("global", ctx.cwd, { stuckMaxInterventions: undefined });
2929
2968
  else ctx.ui.notify(`Not a positive integer: ${v}`, "warning");
2930
2969
  }
2970
+ } else if (choice.startsWith("Stall escalation")) {
2971
+ const v = await ctx.ui.input("Heartbeat refires without a turn before the goal pauses / loop stops", "non-negative integer; 0 = never escalate, empty = default 5");
2972
+ if (v !== undefined) {
2973
+ const n = Number.parseInt(v.trim(), 10);
2974
+ if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { stallEscalationRefires: n });
2975
+ else if (!v.trim()) saveSettings("global", ctx.cwd, { stallEscalationRefires: undefined });
2976
+ else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
2977
+ }
2931
2978
  } else if (choice.startsWith("Subagent model strategy")) {
2932
2979
  const v = await ctx.ui.select("Subagent model (pi-subagents default agents)", [
2933
2980
  "inherit-parent — subagents share your session model + its quota pool (fixes separate-provider 403s; search agents may run on a pricier model)",
@@ -2938,8 +2985,8 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2938
2985
  saveSettings("global", ctx.cwd, { subagentModelStrategy: strategy });
2939
2986
  ctx.ui.notify("Subagent model strategy saved — applies to NEW pi sessions (pi-subagents registers agents at session start).", "info");
2940
2987
  }
2941
- } else if (/^Subagent (Explore|Plan|general-purpose) model pin/.test(choice)) {
2942
- const agentType = choice.match(/^Subagent (Explore|Plan|general-purpose) model pin/)![1]!;
2988
+ } else if (/^Subagent (Explore|Plan|general-purpose) pin/.test(choice)) {
2989
+ const agentType = choice.match(/^Subagent (Explore|Plan|general-purpose) pin/)![1]!;
2943
2990
  const v = await ctx.ui.input(`Model pin for ${agentType} subagents`, "provider/model-id e.g. minimax/MiniMax-M3 — always wins over strategy; empty = follow strategy");
2944
2991
  if (v !== undefined) {
2945
2992
  const current = loadSettings(ctx.cwd).subagentModelOverrides ?? {};
@@ -2949,15 +2996,19 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2949
2996
  saveSettings("global", ctx.cwd, { subagentModelOverrides: Object.keys(next).length > 0 ? next : undefined });
2950
2997
  ctx.ui.notify(`${agentType} model pin saved — applies to NEW pi sessions.`, "info");
2951
2998
  }
2952
- } else if (choice.startsWith("Audit feedback")) {
2953
- const v = await ctx.ui.input("Auditor feedback returned to the executor (characters)", "non-negative integer cap; 0 or empty = full report (default)");
2999
+ } else if (choice.startsWith("Notify command")) {
3000
+ const v = await ctx.ui.input("Notify command the event message is passed as $1", "e.g. a desktop-notification or push command; empty = off");
3001
+ if (v !== undefined) saveSettings("global", ctx.cwd, { notifyCmd: v.trim() || undefined });
3002
+ } else if (choice.startsWith("Token limit")) {
3003
+ const v = await ctx.ui.input("Per-goal token budget", "non-negative integer; 0 or empty = off (no cap)");
2954
3004
  if (v !== undefined) {
2955
- const raw = v.trim();
2956
- const n = Number(raw);
2957
- if (/^\d+$/.test(raw) && Number.isSafeInteger(n)) saveSettings("global", ctx.cwd, { auditFeedbackChars: n });
2958
- else if (!v.trim()) saveSettings("global", ctx.cwd, { auditFeedbackChars: undefined });
3005
+ const n = Number.parseInt(v.trim(), 10);
3006
+ if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { tokenLimit: n });
3007
+ else if (!v.trim()) saveSettings("global", ctx.cwd, { tokenLimit: undefined });
2959
3008
  else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
2960
3009
  }
3010
+ } else if (choice.startsWith("Reviewer config")) {
3011
+ await cmdReviewerSettings(ctx);
2961
3012
  }
2962
3013
  } catch {
2963
3014
  return;
@@ -3161,6 +3212,8 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
3161
3212
  fmt("aggressiveMode", "aggressiveMode"),
3162
3213
  fmt("quotaRetryMinutes", "quotaRetryMinutes"),
3163
3214
  fmt("stuckMaxInterventions", "stuckMaxInterventions"),
3215
+ fmt("stallEscalationRefires", "stallEscalation"),
3216
+ fmt("wedgeAlertMinutes", "wedgeAlert"),
3164
3217
  // v0.25.6: effective per-type subagent model resolution.
3165
3218
  ...["Explore", "Plan", "general-purpose"].map(
3166
3219
  (t) => `subagent ${t}: ${resolveEffectiveSubagentModel(t, loadSettings(ctx.cwd), (ctx.model as any)?.id ? `${(ctx.model as any).provider}/${(ctx.model as any).id}` : undefined)}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.26.9",
3
+ "version": "0.27.1",
4
4
  "description": "Goal. Loop. Audit. Done. — a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor — only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",