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 +5 -1
- package/package.json +1 -1
- package/src/guidance.ts +7 -1
- package/src/index.ts +37 -5
- package/src/review.ts +22 -7
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
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
|
-
:
|
|
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 ·
|
|
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
|
|
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
|
|
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(
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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
|
|
292
|
+
export function detectEditor(): EditorKind | undefined {
|
|
284
293
|
const explicit = process.env.AHEAD_EDITOR?.trim();
|
|
285
294
|
if (explicit === "vscode" || explicit === "code") {
|
|
286
|
-
return "
|
|
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
|
-
? "
|
|
307
|
+
? "vscode"
|
|
293
308
|
: undefined;
|
|
294
309
|
}
|
|
295
310
|
|