ahead-pi 0.8.3 → 0.8.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
@@ -127,7 +127,11 @@ INDEPENDENT HUMAN REVIEWS AND ACCEPTS
127
127
 
128
128
  A draft branch or draft PR may exist earlier. The handoff gate is requesting human review or marking the PR ready, not ordinary draft pushes.
129
129
 
130
- `/ahead-review` opens the first editor-neutral review workbench. It fingerprints the current Git changeset, shows the changed paths and diff in Pi, can open a selected path in VS Code when detected or configured with `AHEAD_EDITOR=vscode`, requests snapshot-bound AI findings, and opens the required human disposition or independent-review record. Any engineering change produces a new fingerprint and requires review again. `.ahead/**` evidence is excluded from that fingerprint so recording the review does not invalidate it.
130
+ `/ahead-review` opens the first editor-neutral review workbench. It fingerprints the current Git changeset, shows the changed paths and diff in Pi, can open a selected path in VS Code or Zed when detected or configured with `AHEAD_EDITOR=vscode` or `AHEAD_EDITOR=zed`, requests snapshot-bound AI findings, and opens the required human disposition or independent-review record. Any engineering change produces a new fingerprint and requires review again. `.ahead/**` evidence is excluded from that fingerprint so recording the review does not invalidate it.
131
+
132
+ VS Code is detected from its terminal environment. For a deterministic Zed terminal, add `"AHEAD_EDITOR": "zed"` under Zed's `terminal.env` settings; Zed's CLI must be installed. `AHEAD_EDITOR=none` disables external opening.
133
+
134
+ Human-owned plans and other artifacts open in that detected editor as a temporary Markdown draft. Save your work, then close only that temporary tab; AHEAD copies the draft into `.ahead/runs/` after the tab closes and the form passes validation. The host editor stays open, and saving alone does not record the artifact.
131
135
 
132
136
  ## Human commands
133
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ahead-pi",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "AHEAD workflow enforcement and context for Pi",
5
5
  "keywords": [
6
6
  "ahead",
package/src/guidance.ts CHANGED
@@ -424,7 +424,9 @@ export function nextAction(state: RunState, workflow: WorkflowDefinition): Guide
424
424
  ? "Ask AI to challenge the human option and expand alternatives"
425
425
  : state.phase.id === "plan"
426
426
  ? "Ask AI to challenge the human first-pass plan"
427
- : `Ask AI to contribute ${artifact.title}`;
427
+ : artifact.title.startsWith("AI ")
428
+ ? `Ask AI for ${lowercaseFirst(artifact.title.slice(3))}`
429
+ : `Ask AI to contribute ${artifact.title}`;
428
430
  return artifact.required
429
431
  ? { actor, label, artifactKind: artifact.kind }
430
432
  : { actor, label, artifactKind: artifact.kind, optional: true };
@@ -606,6 +608,10 @@ export function validateArtifactForm(content: string, prompts: string[]): string
606
608
  return errors;
607
609
  }
608
610
 
611
+ function lowercaseFirst(value: string): string {
612
+ return value.charAt(0).toLowerCase() + value.slice(1);
613
+ }
614
+
609
615
  function formatArtifactStatus(artifact: ArtifactState): string {
610
616
  const owner = artifact.actor === "ai" ? "AI" : artifact.actor === "human" ? "you" : "you/AI";
611
617
  return `${artifact.present ? "✓" : "○"} ${artifact.title} [${owner}]`;
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
- import { readFile } from "node:fs/promises";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import type {
5
7
  ExtensionAPI,
@@ -32,6 +34,7 @@ import {
32
34
  import { showReferenceViewer } from "./reference-viewer.js";
33
35
  import {
34
36
  collectReviewSnapshot,
37
+ detectEditor,
35
38
  extractFindingIds,
36
39
  extractReviewFingerprint,
37
40
  openInConfiguredEditor,
@@ -702,7 +705,7 @@ async function openAheadMode(
702
705
  });
703
706
  } else if (missingRequired.length === 0) {
704
707
  actions.push({
705
- label: `Continue without optional AI contribution · Accept ${state.gate.title}`,
708
+ label: `Continue without optional AI contribution · ${state.gate.title}`,
706
709
  run: async () => acceptAndContinue(ctx),
707
710
  });
708
711
  }
@@ -752,8 +755,12 @@ async function openAheadMode(
752
755
  });
753
756
  }
754
757
 
758
+ // The generic challenger appears only when the next move is the human's.
759
+ // When the primary action is already an AI contribution, the formal AI
760
+ // artifact owns that job and a second "challenge me" entry is noise.
755
761
  if (
756
762
  state.allowed_ai_capabilities.length > 0 &&
763
+ action.actor !== "ai" &&
757
764
  state.artifacts.some(
758
765
  (artifact) => artifact.present && artifact.recorded_by?.kind === "human" && artifact.path,
759
766
  )
@@ -1363,7 +1370,7 @@ async function openReviewWorkbench(pi: ExtensionAPI, ctx: ExtensionCommandContex
1363
1370
  const path = await ctx.ui.select("Open changed file", snapshot.changed_files);
1364
1371
  if (path && !openInConfiguredEditor(store.projectRoot, { path })) {
1365
1372
  ctx.ui.notify(
1366
- `No supported editor was detected. Open ${path} from the repository, or set AHEAD_EDITOR=vscode.`,
1373
+ `No supported editor was detected. Open ${path} from the repository, or set AHEAD_EDITOR=vscode or AHEAD_EDITOR=zed.`,
1367
1374
  "info",
1368
1375
  );
1369
1376
  }
@@ -1651,7 +1658,8 @@ async function recordHumanArtifact(
1651
1658
  // "failed" (timeout, unparseable output, transient error) stays silent:
1652
1659
  // the plain template is the fallback and needs no apology.
1653
1660
  }
1654
- let content = await ctx.ui.editor(
1661
+ let content = await editHumanArtifact(
1662
+ ctx,
1655
1663
  `AHEAD mode · ${artifact.title} · write in your own words`,
1656
1664
  template,
1657
1665
  );
@@ -1680,7 +1688,8 @@ async function recordHumanArtifact(
1680
1688
  ctx.ui.notify("Draft discarded. Nothing was saved.", "info");
1681
1689
  return;
1682
1690
  }
1683
- const revised = await ctx.ui.editor(
1691
+ const revised = await editHumanArtifact(
1692
+ ctx,
1684
1693
  `AHEAD mode · ${artifact.title} · finish the required fields`,
1685
1694
  content,
1686
1695
  );
@@ -1722,6 +1731,29 @@ async function recordHumanArtifact(
1722
1731
  );
1723
1732
  }
1724
1733
 
1734
+ async function editHumanArtifact(
1735
+ ctx: ExtensionCommandContext,
1736
+ title: string,
1737
+ content: string,
1738
+ ): Promise<string | undefined> {
1739
+ if (!detectEditor()) {
1740
+ return ctx.ui.editor(title, content);
1741
+ }
1742
+
1743
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "ahead-artifact-"));
1744
+ const temporaryPath = join(temporaryRoot, "artifact.md");
1745
+ try {
1746
+ await writeFile(temporaryPath, content, "utf8");
1747
+ if (openInConfiguredEditor(temporaryRoot, { path: "artifact.md" }, { wait: true })) {
1748
+ return await readFile(temporaryPath, "utf8");
1749
+ }
1750
+ ctx.ui.notify("The configured editor could not be opened; using Pi's editor instead.", "info");
1751
+ return await ctx.ui.editor(title, content);
1752
+ } finally {
1753
+ await rm(temporaryRoot, { recursive: true, force: true });
1754
+ }
1755
+ }
1756
+
1725
1757
  async function humanArtifactTemplate(
1726
1758
  store: RunStore,
1727
1759
  state: RunState,
package/src/review.ts CHANGED
@@ -13,6 +13,8 @@ export interface SourceLocation {
13
13
  column?: number;
14
14
  }
15
15
 
16
+ export type EditorKind = "vscode" | "zed";
17
+
16
18
  export interface ReviewSnapshot {
17
19
  api_version: "ahead.review-snapshot/v0.1";
18
20
  base_ref: string;
@@ -242,16 +244,23 @@ export function validateReviewDisposition(
242
244
  return errors;
243
245
  }
244
246
 
245
- export function openInConfiguredEditor(root: string, location: SourceLocation): boolean {
246
- const command = configuredEditorCommand();
247
- if (!command) {
247
+ export function openInConfiguredEditor(
248
+ root: string,
249
+ location: SourceLocation,
250
+ options: { wait?: boolean } = {},
251
+ ): boolean {
252
+ const editor = detectEditor();
253
+ if (!editor) {
248
254
  return false;
249
255
  }
250
256
  const absolute = safeRepositoryPath(root, location.path);
251
257
  const line = location.line ?? 1;
252
258
  const column = location.column ?? 1;
253
259
  const target = `${absolute}:${line}:${column}`;
254
- const result = spawnSync(command, ["--goto", target], { stdio: "ignore" });
260
+ const command = editor === "zed" ? "zed" : "code";
261
+ const wait = options.wait ? ["--wait"] : [];
262
+ const args = editor === "zed" ? [...wait, target] : [...wait, "--goto", target];
263
+ const result = spawnSync(command, args, { stdio: "ignore" });
255
264
  return !result.error && result.status === 0;
256
265
  }
257
266
 
@@ -280,16 +289,22 @@ function resolveReviewBase(root: string): string {
280
289
  return "HEAD";
281
290
  }
282
291
 
283
- function configuredEditorCommand(): string | undefined {
292
+ export function detectEditor(): EditorKind | undefined {
284
293
  const explicit = process.env.AHEAD_EDITOR?.trim();
285
294
  if (explicit === "vscode" || explicit === "code") {
286
- return "code";
295
+ return "vscode";
296
+ }
297
+ if (explicit === "zed") {
298
+ return "zed";
287
299
  }
288
300
  if (explicit === "none") {
289
301
  return undefined;
290
302
  }
303
+ if (process.env.ZED === "1" || process.env.TERM_PROGRAM === "zed") {
304
+ return "zed";
305
+ }
291
306
  return process.env.VSCODE_IPC_HOOK_CLI || process.env.TERM_PROGRAM === "vscode"
292
- ? "code"
307
+ ? "vscode"
293
308
  : undefined;
294
309
  }
295
310