@mjasnikovs/pi-task 0.13.3 → 0.13.5

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
- <img src="https://cdn.jsdelivr.net/npm/@mjasnikovs/pi-task/assets/pipeline.svg" alt="pi-task pipeline: a /task request runs through refine, research, grill, compose and critique, then the final spec is delivered to your main pi session in the same chat. Every phase boundary is persisted to .pi-tasks/TASK_NNNN.md, so the task is crash-safe and resumable." width="820"/>
3
+ <img src="https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/pipeline.svg" alt="pi-task pipeline: a /task request runs through refine, research, grill, compose and critique, then the final spec is delivered to your main pi session in the same chat. Every phase boundary is persisted to .pi-tasks/TASK_NNNN.md, so the task is crash-safe and resumable." width="820"/>
4
4
 
5
- # <img src="https://cdn.jsdelivr.net/npm/@mjasnikovs/pi-task/assets/pi-logo.svg" alt="" height="30" align="top"/> pi-task
5
+ # <img src="https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/pi-logo.svg" alt="" height="30" align="top"/> pi-task
6
6
 
7
7
  **Deterministic spec-orchestration for local models — with bundled web, docs, fetch, and worker sub-agent tools.**
8
8
 
@@ -69,7 +69,7 @@ A real feature is usually several tasks, not one. `/task-auto` is a thin planner
69
69
 
70
70
  <div align="center">
71
71
 
72
- <img src="https://cdn.jsdelivr.net/npm/@mjasnikovs/pi-task/assets/task-auto.svg" alt="/task-auto plans a feature: it clarifies the gray areas, decomposes the answers into an ordered list of task titles written to TASK_AUTO_NNNN.md, then runs each unchecked title through the full /task pipeline one at a time, ticking the box before moving on." width="820"/>
72
+ <img src="https://raw.githubusercontent.com/mjasnikovs/pi-task/main/assets/task-auto.svg" alt="/task-auto plans a feature: it clarifies the gray areas, decomposes the answers into an ordered list of task titles written to TASK_AUTO_NNNN.md, then runs each unchecked title through the full /task pipeline one at a time, ticking the box before moving on." width="820"/>
73
73
 
74
74
  </div>
75
75
 
package/dist/remote/ui.js CHANGED
@@ -200,8 +200,12 @@ export function html(wsUrl) {
200
200
  #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);
201
201
  border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;
202
202
  font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }
203
- #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: center;
203
+ #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;
204
204
  flex-wrap: wrap; }
205
+ /* Recommendation answers can be long sentences, so stack them as a readable list. */
206
+ #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }
207
+ #prompt-card .row.stacked button { flex: none; text-align: left; }
208
+ #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }
205
209
  #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;
206
210
  font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }
207
211
  #prompt-card button:hover { filter: brightness(1.08); }
@@ -209,8 +213,8 @@ export function html(wsUrl) {
209
213
  font-weight: 700; flex: 1; min-width: 160px; }
210
214
  #prompt-card button.secondary { background: var(--surface1); color: var(--text);
211
215
  flex: 1; min-width: 160px; }
212
- #prompt-card button.cancel { margin-left: auto; background: transparent; color: var(--subtext0);
213
- font-size: 12px; font-weight: 500; padding: 8px 10px; }
216
+ #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;
217
+ color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }
214
218
  #prompt-card button.cancel:hover { color: var(--red); filter: none; }
215
219
  #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }
216
220
  .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);
@@ -772,7 +776,8 @@ export function html(wsUrl) {
772
776
  return btn;
773
777
  }
774
778
 
775
- function renderButtons(buttons) {
779
+ function renderButtons(buttons, stacked) {
780
+ promptButtons.className = stacked ? 'row stacked' : 'row';
776
781
  promptButtons.innerHTML = '';
777
782
  for (let i = 0; i < buttons.length; i++) promptButtons.appendChild(buttons[i]);
778
783
  promptButtons.appendChild(makeCancelBtn());
@@ -800,11 +805,14 @@ export function html(wsUrl) {
800
805
  promptRec.style.display = 'none';
801
806
  buttons.push(makeBtn(activeRecommended, 'primary', function () { answer(activeRecommended); }));
802
807
  buttons.push(makeBtn(activeRecommended2, 'secondary', function () { answer(activeRecommended2); }));
803
- } else {
804
- // Single recommendation: show it in the green panel.
805
- promptRec.style.display = 'block';
806
- buttons.push(makeBtn('✓ Accept', 'primary', function () { answer(activeRecommended); }));
808
+ buttons.push(makeBtn('✎ Manual answer', 'secondary', function () { showManualEntry(); }));
809
+ // Answer buttons hold full sentences stack them so long text stays readable.
810
+ renderButtons(buttons, true);
811
+ return;
807
812
  }
813
+ // Single recommendation: show it in the green panel.
814
+ promptRec.style.display = 'block';
815
+ buttons.push(makeBtn('✓ Accept', 'primary', function () { answer(activeRecommended); }));
808
816
  buttons.push(makeBtn('✎ Manual answer', 'secondary', function () { showManualEntry(); }));
809
817
  renderButtons(buttons);
810
818
  }
@@ -12,7 +12,7 @@ import { parseClarifyList, deriveTitle } from './parsers.js';
12
12
  import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
13
13
  import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT } from './auto-prompts.js';
14
14
  import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
15
- import { writeTaskFile, readTaskFile, updateTaskFrontMatter } from './task-io.js';
15
+ import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath } from './task-io.js';
16
16
  import { gitCommitAll } from './auto-commit.js';
17
17
  import { runPhaseChild, USER_CANCELLED } from './child-runner.js';
18
18
  import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
@@ -217,12 +217,25 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
217
217
  active.ui.notify(`${id}: task ${next.index + 1}/${entries.length} — ${next.title}`, 'info');
218
218
  // If this entry already has a stamped inner id, it was started in a
219
219
  // previous (interrupted) run — resume it from its saved phase rather
220
- // than spawning a fresh task. Otherwise stamp the freshly-allocated id
221
- // onto the entry the moment it exists, so an interruption here is
222
- // resumable too. This mirrors /task-resume's continue-don't-restart.
220
+ // than spawning a fresh task. But the stamped inner file can be gone
221
+ // (deleted, or never written because allocation was interrupted), and
222
+ // resuming a missing file throws ENOENT deep in the runner — which used
223
+ // to take pi down. So verify the file exists and otherwise fall back to
224
+ // a fresh start. Either way an unstamped/restarted entry is (re)stamped
225
+ // the moment its inner id exists, keeping the next interruption
226
+ // resumable. This mirrors /task-resume's continue-don't-restart.
227
+ let resumeId = next.producedId;
228
+ if (resumeId) {
229
+ try {
230
+ await fsp.access(taskFilePath(cwd, resumeId));
231
+ }
232
+ catch {
233
+ resumeId = undefined;
234
+ }
235
+ }
223
236
  const res = await deps.runTask(active, cwd, next.title, {
224
- resumeId: next.producedId,
225
- onStart: next.producedId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
237
+ resumeId,
238
+ onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
226
239
  });
227
240
  active = res.ctx ?? active;
228
241
  if (res.sessionCancelled) {
@@ -251,6 +264,19 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
251
264
  }
252
265
  }
253
266
  }
267
+ catch (err) {
268
+ // Safety net: no failure inside the loop may propagate out of runAutoLoop,
269
+ // because the resume handler doesn't wrap this call and an unhandled
270
+ // rejection crashes pi outright. Convert it into a failed run + notify,
271
+ // mirroring the in-loop per-task failure path.
272
+ const msg = err instanceof Error ? err.message : String(err);
273
+ if (msg === USER_CANCELLED) {
274
+ active.ui.notify(`${id} cancelled — resume with /task-auto-resume.`, 'warning');
275
+ return;
276
+ }
277
+ await updateTaskFrontMatter(cwd, id, { state: 'failed' }).catch(() => { });
278
+ active.ui.notify(`${id} stopped: ${msg} — fix and run /task-auto-resume.`, 'error');
279
+ }
254
280
  finally {
255
281
  cancelRequested = false;
256
282
  }
@@ -233,7 +233,6 @@ export function validateSpecShape(spec) {
233
233
  // ─── Title derivation ────────────────────────────────────────────────────────
234
234
  export function deriveTitle(refined) {
235
235
  const stripBold = (s) => s.replace(/^\*+|\*+$/g, '').trim();
236
- const truncate = (s) => (s.length > 119 ? s.slice(0, 119) + '…' : s);
237
236
  const lines = refined.split('\n');
238
237
  for (let i = 0; i < lines.length; i++) {
239
238
  const stripped = stripBold(lines[i].trim().replace(/^#+\s+/, ''));
@@ -245,7 +244,7 @@ export function deriveTitle(refined) {
245
244
  const headerCheck = stripBold(line.replace(/^#+\s+/, ''));
246
245
  if (/^(CONSTRAINTS|KNOWN-UNKNOWNS)\s*:?\s*$/i.test(headerCheck))
247
246
  break;
248
- return truncate(line);
247
+ return line;
249
248
  }
250
249
  break;
251
250
  }
@@ -257,7 +256,7 @@ export function deriveTitle(refined) {
257
256
  line = stripBold(line.replace(/^#+\s+/, '')).replace(/^GOAL\s*:?\s*/i, '');
258
257
  if (line.length === 0)
259
258
  continue;
260
- return truncate(line);
259
+ return line;
261
260
  }
262
261
  return '(untitled)';
263
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
4
4
  "description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",