@plannotator/pi-extension 0.28.37 → 0.28.39
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/generated/ai/providers/ask-ai-env.ts +217 -0
- package/generated/ai/providers/claude-agent-sdk.ts +19 -0
- package/generated/ai/providers/codex-app-server.ts +13 -5
- package/generated/ai/providers/opencode-sdk.ts +13 -5
- package/generated/ai/providers/pi-events.ts +1 -1
- package/generated/ai/providers/pi-sdk-node.ts +5 -0
- package/generated/ai/providers/pi-sdk.ts +6 -2
- package/generated/ai/providers/qwen-sdk.ts +5 -1
- package/generated/call-flow-languages.ts +4 -2
- package/generated/call-flow-runtime/package-lock.json +7 -7
- package/generated/call-flow-runtime/package.json +2 -2
- package/generated/call-flow.ts +149 -32
- package/generated/child-env.ts +12 -0
- package/generated/claude-review.ts +3 -0
- package/generated/codex-review.ts +3 -0
- package/generated/guide-review.ts +3 -0
- package/generated/guide-viewer-manifest.ts +4 -4
- package/generated/inbox/connection.ts +59 -16
- package/generated/project.ts +3 -2
- package/generated/question-block.ts +2 -2
- package/generated/tour-review.ts +3 -0
- package/generated/update-check.ts +113 -14
- package/package.json +1 -1
- package/plannotator.html +452 -451
- package/review-editor.html +450 -424
- package/server/agent-jobs-ask-ai-child.test.ts +149 -0
- package/server/agent-jobs.ts +5 -1
- package/server/serverReview.ts +4 -0
- package/skills/plannotator/SKILL.md +3 -3
package/generated/call-flow.ts
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* Isolated CallDiff execution over immutable review snapshots.
|
|
4
4
|
*
|
|
5
5
|
* CallDiff is a Node-native, synchronous Tree-sitter library. Plannotator runs
|
|
6
|
-
* it in a short-lived Node 22
|
|
6
|
+
* it in a short-lived Node worker (22.18 or newer) and never imports it into
|
|
7
|
+
* Bun or Pi.
|
|
7
8
|
*/
|
|
8
9
|
import { childEnv } from "./child-env.ts";
|
|
9
10
|
import { createHash } from "node:crypto";
|
|
10
11
|
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
12
|
+
import type { Dirent } from "node:fs";
|
|
11
13
|
import { mkdtemp } from "node:fs/promises";
|
|
12
14
|
import { tmpdir } from "node:os";
|
|
13
15
|
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
@@ -34,11 +36,14 @@ import type {
|
|
|
34
36
|
ParsedCallDiffWorkerResult,
|
|
35
37
|
} from "./call-flow-types.ts";
|
|
36
38
|
|
|
37
|
-
export const CALLDIFF_VERSION = "0.
|
|
38
|
-
export const CALLDIFF_COMMIT = "
|
|
39
|
+
export const CALLDIFF_VERSION = "0.6.0";
|
|
40
|
+
export const CALLDIFF_COMMIT = "2086f6e4e9ed3c410bba7c94ca3dde77791b1956";
|
|
39
41
|
export const CALLDIFF_SOURCE_SPEC = `https://github.com/tanishqkancharla/calldiff/archive/${CALLDIFF_COMMIT}.tar.gz`;
|
|
40
|
-
export const CALLDIFF_SOURCE_INTEGRITY = "sha512-
|
|
42
|
+
export const CALLDIFF_SOURCE_INTEGRITY = "sha512-t+YCPz0WkETiwSOznwx3mLxVkEUe7DVACV+it2HJER7q/ha+K0IavN9IfBvP+7/ijzPXoUHgJIkAM/kg1O5S6A==";
|
|
41
43
|
export const CALLDIFF_TREE_SITTER_VERSION = "0.25.1";
|
|
44
|
+
/** CallDiff's own `engines.node` floor at the pinned commit (>=22.18.0). */
|
|
45
|
+
export const CALL_FLOW_MIN_NODE_VERSION = { major: 22, minor: 18 } as const;
|
|
46
|
+
const CALL_FLOW_NODE_REQUIREMENT = "Node.js 22.18 or newer";
|
|
42
47
|
const CALLDIFF_ARCHIVE_FILE = `calldiff-${CALLDIFF_COMMIT}.tar.gz`;
|
|
43
48
|
const MAX_CALLDIFF_ARCHIVE_BYTES = 20 * 1024 * 1024;
|
|
44
49
|
const MAX_PACK_ARCHIVES_BYTES = 256 * 1024 * 1024;
|
|
@@ -248,11 +253,23 @@ function hasCommittedRuntimeLock(runtimeDir: string): boolean {
|
|
|
248
253
|
}
|
|
249
254
|
}
|
|
250
255
|
|
|
251
|
-
|
|
256
|
+
/** True when a `node --version` string meets CallDiff's 22.18 floor. */
|
|
257
|
+
export function callFlowNodeVersionSupported(version: string): boolean {
|
|
258
|
+
const match = /^v?(\d+)\.(\d+)/.exec(version.trim());
|
|
259
|
+
if (!match) return false;
|
|
260
|
+
const major = Number(match[1]);
|
|
261
|
+
const minor = Number(match[2]);
|
|
262
|
+
const floor = CALL_FLOW_MIN_NODE_VERSION;
|
|
263
|
+
return major > floor.major || (major === floor.major && minor >= floor.minor);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function checkCallFlowNode(nodePath: string): Promise<{ ok: true } | { ok: false; message: string }> {
|
|
252
267
|
const result = await runCommand(nodePath, ["--version"], { timeoutMs: 3_000 });
|
|
253
|
-
const
|
|
254
|
-
if (result.exitCode !== 0 ||
|
|
255
|
-
if (
|
|
268
|
+
const found = result.stdout.trim();
|
|
269
|
+
if (result.exitCode !== 0 || !/^v\d+\.\d+/.test(found)) return { ok: false, message: "Node.js could not be started." };
|
|
270
|
+
if (!callFlowNodeVersionSupported(found)) {
|
|
271
|
+
return { ok: false, message: `Call flow needs ${CALL_FLOW_NODE_REQUIREMENT}. You have Node.js ${found}.` };
|
|
272
|
+
}
|
|
256
273
|
return { ok: true };
|
|
257
274
|
}
|
|
258
275
|
|
|
@@ -279,8 +296,8 @@ function findExecutable(name: string): string | null {
|
|
|
279
296
|
*/
|
|
280
297
|
export async function preflightCallFlowNode(): Promise<CallFlowNodePreflight> {
|
|
281
298
|
const nodePath = findExecutable("node");
|
|
282
|
-
if (!nodePath) return { ok: false, reason: "node-unavailable", message:
|
|
283
|
-
const nodeCheck = await
|
|
299
|
+
if (!nodePath) return { ok: false, reason: "node-unavailable", message: `Call flow needs ${CALL_FLOW_NODE_REQUIREMENT}, which was not found on PATH.` };
|
|
300
|
+
const nodeCheck = await checkCallFlowNode(nodePath);
|
|
284
301
|
if (!nodeCheck.ok) return { ok: false, reason: "node-version", message: nodeCheck.message };
|
|
285
302
|
return { ok: true };
|
|
286
303
|
}
|
|
@@ -288,8 +305,8 @@ export async function preflightCallFlowNode(): Promise<CallFlowNodePreflight> {
|
|
|
288
305
|
/** Resolve the pruned core and report independently installed language packs. */
|
|
289
306
|
export async function resolveCallFlowRuntime(): Promise<CallFlowRuntimeResolution> {
|
|
290
307
|
const nodePath = findExecutable("node");
|
|
291
|
-
if (!nodePath) return { ok: false, reason: "node-unavailable", message:
|
|
292
|
-
const nodeCheck = await
|
|
308
|
+
if (!nodePath) return { ok: false, reason: "node-unavailable", message: `Call flow needs ${CALL_FLOW_NODE_REQUIREMENT}, which was not found on PATH.` };
|
|
309
|
+
const nodeCheck = await checkCallFlowNode(nodePath);
|
|
293
310
|
if (!nodeCheck.ok) return { ok: false, reason: "node-version", message: nodeCheck.message };
|
|
294
311
|
|
|
295
312
|
const override = process.env.PLANNOTATOR_CALLDIFF_PATH?.trim();
|
|
@@ -686,29 +703,125 @@ function ensureGrammarCacheManifest(cacheDir: string): void {
|
|
|
686
703
|
}), "utf8");
|
|
687
704
|
}
|
|
688
705
|
|
|
689
|
-
|
|
706
|
+
/**
|
|
707
|
+
* Copy optional packs into a candidate core. Sources are tried in order (the
|
|
708
|
+
* current runtime first, then folders left by older pins); a pack crosses only
|
|
709
|
+
* when its exact committed lock marker matches and it loads through the
|
|
710
|
+
* candidate core. Packs are pinned independently of CallDiff, so an upgrade
|
|
711
|
+
* keeps the user's installed languages instead of downloading them again.
|
|
712
|
+
*/
|
|
713
|
+
export async function preserveValidatedLanguagePacks(
|
|
690
714
|
nodePath: string,
|
|
691
|
-
|
|
715
|
+
sourceRuntimeDirs: readonly string[],
|
|
692
716
|
candidateRuntimeDir: string,
|
|
693
|
-
): Promise<
|
|
717
|
+
): Promise<CallFlowLanguageId[]> {
|
|
694
718
|
const candidateCache = getCallFlowGrammarCacheDir(candidateRuntimeDir);
|
|
695
719
|
ensureGrammarCacheManifest(candidateCache);
|
|
720
|
+
const preserved: CallFlowLanguageId[] = [];
|
|
696
721
|
for (const language of CALL_FLOW_LANGUAGES) {
|
|
697
|
-
if (language.kind !== "pack"
|
|
722
|
+
if (language.kind !== "pack") continue;
|
|
698
723
|
const packId = language.id as Exclude<CallFlowLanguageId, "javascript-typescript">;
|
|
699
|
-
const currentRoot = getLanguagePackageRoot(currentRuntimeDir, language);
|
|
700
724
|
const candidateRoot = getLanguagePackageRoot(candidateRuntimeDir, language);
|
|
701
|
-
|
|
702
|
-
|
|
725
|
+
for (const sourceRuntimeDir of sourceRuntimeDirs) {
|
|
726
|
+
if (!isLanguageInstalled(sourceRuntimeDir, language, true)) continue;
|
|
727
|
+
mkdirSync(dirname(candidateRoot), { recursive: true });
|
|
728
|
+
cpSync(getLanguagePackageRoot(sourceRuntimeDir, language), candidateRoot, { recursive: true, force: true });
|
|
729
|
+
try {
|
|
730
|
+
await validatePrunedLanguage(nodePath, candidateRuntimeDir, candidateCache, [language.id]);
|
|
731
|
+
const marker = getLanguageLockMarker(candidateRuntimeDir, language.id);
|
|
732
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
733
|
+
writeFileSync(marker, runtimeJson(CALL_FLOW_PACK_LOCKS[packId]), "utf8");
|
|
734
|
+
preserved.push(language.id);
|
|
735
|
+
break;
|
|
736
|
+
} catch {
|
|
737
|
+
// A pack whose files no longer load is not independently verified and
|
|
738
|
+
// must not cross the atomic core repair boundary.
|
|
739
|
+
removeDirectoryStrict(candidateRoot);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return preserved;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Folder names Plannotator itself creates for a managed core: the published
|
|
748
|
+
* `calldiff-<version>` and the `.previous-<pid>-<ms>` backup a core swap
|
|
749
|
+
* leaves when it is interrupted. Nothing else in vendor/call-flow matches.
|
|
750
|
+
*/
|
|
751
|
+
const MANAGED_CORE_DIR_PATTERN = /^calldiff-(\d+)\.(\d+)\.(\d+)(\.previous-\d+-\d+)?$/;
|
|
752
|
+
|
|
753
|
+
function compareVersionTriples(a: readonly number[], b: readonly number[]): number {
|
|
754
|
+
for (let index = 0; index < 3; index += 1) {
|
|
755
|
+
if (a[index] !== b[index]) return a[index] - b[index];
|
|
756
|
+
}
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Managed core folders under this data dir that no current or newer
|
|
762
|
+
* Plannotator uses: cores of an older CallDiff pin, and interrupted-swap
|
|
763
|
+
* backups of an older or the current pin. A newer pin's folder and backups
|
|
764
|
+
* are kept, so downgrading and upgrading again finds it still installed.
|
|
765
|
+
*/
|
|
766
|
+
export function listStaleCallFlowRuntimeDirs(dataDir = getPlannotatorDataDir()): string[] {
|
|
767
|
+
const currentDir = getCallFlowManagedRuntimeDir(dataDir);
|
|
768
|
+
const parent = dirname(currentDir);
|
|
769
|
+
const current = CALLDIFF_VERSION.split(".").map(Number);
|
|
770
|
+
let entries: Dirent[];
|
|
771
|
+
try {
|
|
772
|
+
entries = readdirSync(parent, { withFileTypes: true });
|
|
773
|
+
} catch {
|
|
774
|
+
return [];
|
|
775
|
+
}
|
|
776
|
+
const stale: string[] = [];
|
|
777
|
+
for (const entry of entries) {
|
|
778
|
+
// Dirent types come from lstat: a symlink is never followed or removed.
|
|
779
|
+
if (!entry.isDirectory()) continue;
|
|
780
|
+
const match = MANAGED_CORE_DIR_PATTERN.exec(entry.name);
|
|
781
|
+
if (!match) continue;
|
|
782
|
+
const isBackup = match[4] !== undefined;
|
|
783
|
+
const version = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
784
|
+
const order = compareVersionTriples(version, current);
|
|
785
|
+
if (order < 0 || (isBackup && order === 0)) stale.push(join(parent, entry.name));
|
|
786
|
+
}
|
|
787
|
+
return stale.sort();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* A stale core is renamed to this dot-prefixed name before it is deleted. No
|
|
792
|
+
* Plannotator version resolves a dot-prefixed folder, so a delete cut short
|
|
793
|
+
* (a killed process, a busy file on Windows) never leaves a half-deleted
|
|
794
|
+
* `calldiff-<x.y.z>` that an older Plannotator would take as installed.
|
|
795
|
+
*/
|
|
796
|
+
const STALE_TOMBSTONE_PATTERN = /^\.calldiff-stale-\d+-\d+-\d+$/;
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Remove stale managed cores once the current pin is published. Runs only
|
|
800
|
+
* under the install lease and only after a successful install, so a failed
|
|
801
|
+
* upgrade keeps the previous folder for a downgrade. Each folder is first
|
|
802
|
+
* renamed to a tombstone (atomic); one that cannot be renamed (in use on
|
|
803
|
+
* Windows) stays whole and is retried by the next install. Tombstones,
|
|
804
|
+
* including any an earlier interrupted delete left, are then removed.
|
|
805
|
+
*/
|
|
806
|
+
export function removeStaleCallFlowRuntimeDirs(dataDir = getPlannotatorDataDir()): void {
|
|
807
|
+
const parent = dirname(getCallFlowManagedRuntimeDir(dataDir));
|
|
808
|
+
const stamp = `${process.pid}-${Date.now()}`;
|
|
809
|
+
listStaleCallFlowRuntimeDirs(dataDir).forEach((path, index) => {
|
|
703
810
|
try {
|
|
704
|
-
|
|
705
|
-
const marker = getLanguageLockMarker(candidateRuntimeDir, language.id);
|
|
706
|
-
mkdirSync(dirname(marker), { recursive: true });
|
|
707
|
-
writeFileSync(marker, runtimeJson(CALL_FLOW_PACK_LOCKS[packId]), "utf8");
|
|
811
|
+
renameSync(path, join(parent, `.calldiff-stale-${stamp}-${index}`));
|
|
708
812
|
} catch {
|
|
709
|
-
//
|
|
710
|
-
|
|
711
|
-
|
|
813
|
+
// Left whole under its own name; the next install tries again.
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
let entries: Dirent[];
|
|
817
|
+
try {
|
|
818
|
+
entries = readdirSync(parent, { withFileTypes: true });
|
|
819
|
+
} catch {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
for (const entry of entries) {
|
|
823
|
+
if (entry.isDirectory() && STALE_TOMBSTONE_PATTERN.test(entry.name)) {
|
|
824
|
+
removeDirectoryBestEffort(join(parent, entry.name));
|
|
712
825
|
}
|
|
713
826
|
}
|
|
714
827
|
}
|
|
@@ -734,13 +847,14 @@ async function installCallFlowRuntimeUnlocked(
|
|
|
734
847
|
const nodePath = findExecutable("node");
|
|
735
848
|
const npmPath = findExecutable("npm");
|
|
736
849
|
if (!nodePath || !npmPath) {
|
|
737
|
-
return { ok: false, status: "failed", runtimeDir, message:
|
|
850
|
+
return { ok: false, status: "failed", runtimeDir, message: `Call flow needs ${CALL_FLOW_NODE_REQUIREMENT} and npm on PATH.` };
|
|
738
851
|
}
|
|
739
|
-
const nodeCheck = await
|
|
852
|
+
const nodeCheck = await checkCallFlowNode(nodePath);
|
|
740
853
|
if (!nodeCheck.ok) return { ok: false, status: "failed", runtimeDir, message: nodeCheck.message };
|
|
741
854
|
|
|
742
855
|
const existing = await resolveCallFlowRuntime();
|
|
743
856
|
if (existing.ok && existing.runtime.runtimeDir === runtimeDir) {
|
|
857
|
+
removeStaleCallFlowRuntimeDirs();
|
|
744
858
|
return { ok: true, status: "already-installed", runtimeDir, languageId: CALL_FLOW_CORE_LANGUAGE_ID, message: `Call-flow core already installed at ${runtimeDir}.` };
|
|
745
859
|
}
|
|
746
860
|
const runtimeParent = dirname(runtimeDir);
|
|
@@ -803,10 +917,11 @@ async function installCallFlowRuntimeUnlocked(
|
|
|
803
917
|
await validatePrunedLanguage(nodePath, installDir, getCallFlowGrammarCacheDir(installDir), [CALL_FLOW_CORE_LANGUAGE_ID]);
|
|
804
918
|
const sizeAfterPruneBytes = measureCallFlowDirectoryBytes(installDir);
|
|
805
919
|
|
|
806
|
-
// A core repair preserves only packs whose exact marker,
|
|
807
|
-
// and native payload all still validate against the
|
|
920
|
+
// A core repair or upgrade preserves only packs whose exact marker,
|
|
921
|
+
// package version, and native payload all still validate against the
|
|
922
|
+
// candidate core. An upgrade takes them from the older pin's folder.
|
|
808
923
|
const nextCache = getCallFlowGrammarCacheDir(installDir);
|
|
809
|
-
await preserveValidatedLanguagePacks(nodePath, runtimeDir, installDir);
|
|
924
|
+
await preserveValidatedLanguagePacks(nodePath, [runtimeDir, ...listStaleCallFlowRuntimeDirs()], installDir);
|
|
810
925
|
ensureGrammarCacheManifest(nextCache);
|
|
811
926
|
|
|
812
927
|
if (existsSync(runtimeDir)) renameSync(runtimeDir, backupDir);
|
|
@@ -817,6 +932,8 @@ async function installCallFlowRuntimeUnlocked(
|
|
|
817
932
|
throw error;
|
|
818
933
|
}
|
|
819
934
|
removeDirectoryBestEffort(backupDir);
|
|
935
|
+
// Only now that the new pin is published do older pins' folders go.
|
|
936
|
+
removeStaleCallFlowRuntimeDirs();
|
|
820
937
|
return { ok: true, status: "installed", runtimeDir, languageId: CALL_FLOW_CORE_LANGUAGE_ID, sizeBeforePruneBytes, sizeAfterPruneBytes, message: `Installed the pruned CallDiff ${CALLDIFF_VERSION} core at ${runtimeDir}.` };
|
|
821
938
|
} catch (error) {
|
|
822
939
|
return {
|
|
@@ -861,7 +978,7 @@ async function installCallFlowLanguagePackUnlocked(
|
|
|
861
978
|
const nodePath = findExecutable("node");
|
|
862
979
|
const npmPath = findExecutable("npm");
|
|
863
980
|
if (!nodePath || !npmPath) {
|
|
864
|
-
return { ok: false, status: "failed", runtimeDir, message:
|
|
981
|
+
return { ok: false, status: "failed", runtimeDir, message: `Call flow language support needs ${CALL_FLOW_NODE_REQUIREMENT} and npm on PATH.` };
|
|
865
982
|
}
|
|
866
983
|
const runtime = await resolveCallFlowRuntime();
|
|
867
984
|
if (!runtime.ok || !runtime.runtime.managed || runtime.runtime.runtimeDir !== runtimeDir) {
|
package/generated/child-env.ts
CHANGED
|
@@ -37,6 +37,18 @@ export function childEnv(extra?: Readonly<ChildEnv>): ChildEnv {
|
|
|
37
37
|
return env;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Whether this process runs inside an agent Plannotator's Ask AI started:
|
|
42
|
+
* `PLANNOTATOR_ASK_AI_CHILD` is exactly "1" (set by `askAiChildEnv` in
|
|
43
|
+
* packages/ai/providers/ask-ai-env.ts). There, Plannotator's own integration
|
|
44
|
+
* stands down: the Inbox MCP shim lists no tools, the plan-review hook exits
|
|
45
|
+
* 0, and the OpenCode plugin registers nothing. Any other value, or none, is
|
|
46
|
+
* a person's session and changes nothing.
|
|
47
|
+
*/
|
|
48
|
+
export function isAskAiChild(env: Readonly<ChildEnv> = process.env): boolean {
|
|
49
|
+
return env.PLANNOTATOR_ASK_AI_CHILD === "1";
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
/**
|
|
41
53
|
* The live PATH, for `Bun.which(command, { PATH: childPath() })`: without the
|
|
42
54
|
* option Bun.which searches the startup PATH.
|
|
@@ -329,6 +329,9 @@ export const CLAUDE_JOB_DISALLOWED_TOOLS: readonly string[] = [
|
|
|
329
329
|
"Bash(git grep *)", "Bash(git ls-remote *)", "Bash(gh api *)", "Bash(glab api *)",
|
|
330
330
|
// jj: config injection (global flags, accepted anywhere) and external tools.
|
|
331
331
|
"Bash(jj --config*)", "Bash(jj * --config*)", "Bash(jj * --tool*)",
|
|
332
|
+
// Plannotator's Inbox MCP server, by name, as Ask AI does (claude-agent-sdk.ts).
|
|
333
|
+
// `--strict-mcp-config` already loads no MCP server; this holds whatever the transport.
|
|
334
|
+
"mcp__plannotator-inbox",
|
|
332
335
|
];
|
|
333
336
|
|
|
334
337
|
/** `--tools` / `--allowedTools` / `--disallowedTools` for a Claude agent job. */
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ResolvedReviewProfile,
|
|
18
18
|
} from "./review-profiles.ts";
|
|
19
19
|
import { classifyFindingPlacement } from "./external-annotation.ts";
|
|
20
|
+
import { codexAskAiConfigArgs, readCodexConfigTexts } from "./ai/providers/ask-ai-env.ts";
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Debug log — only active when PLANNOTATOR_DEBUG is set
|
|
@@ -217,6 +218,8 @@ export async function buildCodexCommand(options: CodexCommandOptions): Promise<s
|
|
|
217
218
|
...(model ? ["-m", model] : []),
|
|
218
219
|
...(reasoningEffort ? ["-c", `model_reasoning_effort=${reasoningEffort}`] : []),
|
|
219
220
|
...(fastMode ? ["-c", "service_tier=fast"] : []),
|
|
221
|
+
// The Inbox MCP server off: Codex hands a stdio MCP server none of the job's env.
|
|
222
|
+
...codexAskAiConfigArgs(readCodexConfigTexts(cwd)),
|
|
220
223
|
"exec",
|
|
221
224
|
"--output-schema", schemaPath,
|
|
222
225
|
"-o", outputPath,
|
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { mkdir, writeFile, readFile, unlink } from "node:fs/promises";
|
|
5
5
|
import { getPlannotatorDataDir } from "./data-dir.ts";
|
|
6
|
+
import { codexAskAiConfigArgs, readCodexConfigTexts } from "./ai/providers/ask-ai-env.ts";
|
|
6
7
|
import { loadConfig, resolveClaudeSandbox, resolveCursorSandbox } from "./config.ts";
|
|
7
8
|
import { claudeJobIsolationArgs, claudeJobToolArgs, findClaudeStructuredOutput, type ClaudeJobCommandOptions } from "./claude-review.ts";
|
|
8
9
|
import {
|
|
@@ -156,6 +157,8 @@ export async function buildGuideCodexCommand(options: {
|
|
|
156
157
|
...(model ? ["-m", model] : []),
|
|
157
158
|
...(reasoningEffort ? ["-c", `model_reasoning_effort=${reasoningEffort}`] : []),
|
|
158
159
|
...(fastMode ? ["-c", "service_tier=fast"] : []),
|
|
160
|
+
// The Inbox MCP server off: Codex hands a stdio MCP server none of the job's env.
|
|
161
|
+
...codexAskAiConfigArgs(readCodexConfigTexts(cwd)),
|
|
159
162
|
"exec",
|
|
160
163
|
"--output-schema", schemaPath,
|
|
161
164
|
"-o", outputPath,
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
import type { GuideViewerAssets } from "./guide-format.ts";
|
|
7
7
|
|
|
8
8
|
export const GUIDE_VIEWER_MANIFEST: Omit<GuideViewerAssets, "baseUrl"> = {
|
|
9
|
-
js: "viewer.
|
|
10
|
-
css: "viewer.
|
|
11
|
-
jsIntegrity: "sha384-
|
|
12
|
-
cssIntegrity: "sha384-
|
|
9
|
+
js: "viewer.CdCl9A4y.js",
|
|
10
|
+
css: "viewer.DwkeqVw0.css",
|
|
11
|
+
jsIntegrity: "sha384-2X50K3TjsKUyCt69GjPLTwRrZcYV0LS4+5Nv93J3b8gE1sFjbXaviLKYZJTZb3HS",
|
|
12
|
+
cssIntegrity: "sha384-xdeDB20PiSkAJ7ueRdgFndT6bub5IF/tjxEaq7rwDqCBe1U2gH2B7GHtIr5R59/F",
|
|
13
13
|
langs: {
|
|
14
14
|
"astro": "chunks/astro.Ts5EKq2l.js",
|
|
15
15
|
"c": "chunks/c.BIGW1oBm.js",
|
|
@@ -66,15 +66,17 @@ export type InboxFilledArguments = Record<(typeof INBOX_FILLED_ARGUMENTS)[number
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* The one filled argument an agent may set itself: the repository or folder
|
|
69
|
-
* a call is about, when it is not the session
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* folder
|
|
69
|
+
* a call is about, when it is not the folder the session started in (a
|
|
70
|
+
* message about another repository). Optional in the agent's schema; absent,
|
|
71
|
+
* the connection fills the session's folder (the Claude Code mod: where the
|
|
72
|
+
* session started, which a shell `cd` never moves). The connection resolves a
|
|
73
|
+
* relative path against the current working folder and refuses a path that
|
|
74
|
+
* is not an existing folder before anything reaches the Inbox.
|
|
74
75
|
*/
|
|
75
76
|
export const INBOX_AGENT_PROJECT_PATH = 'project_path'
|
|
76
77
|
|
|
77
|
-
export const INBOX_AGENT_PROJECT_PATH_DESCRIPTION =
|
|
78
|
+
export const INBOX_AGENT_PROJECT_PATH_DESCRIPTION =
|
|
79
|
+
'The repository or folder this is about; defaults to the folder your session started in. Pass it only when the message is about another repository or folder.'
|
|
78
80
|
|
|
79
81
|
/** Why an agent's `project_path` is refused before any folder is looked at, or null when its shape passes. */
|
|
80
82
|
export function inboxProjectPathShapeRefusal(value: unknown): string | null {
|
|
@@ -94,9 +96,33 @@ export function inboxProjectPathShapeRefusal(value: unknown): string | null {
|
|
|
94
96
|
*/
|
|
95
97
|
export const INBOX_WAKES_META_KEY = 'ai.plannotator/inbox-wakes'
|
|
96
98
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The `_meta` key a connection sets on a send (`send_message`,
|
|
101
|
+
* `submit_guide`) to the absolute folder the agent works in now, when that is
|
|
102
|
+
* not the `project_path` it filled: the Claude Code mod files a session's
|
|
103
|
+
* messages under the folder it started in, while a shell `cd` moves the
|
|
104
|
+
* agent. Relative `attachments`, and the body's relative images and links
|
|
105
|
+
* (the message's `base_path`), then resolve there, where they resolved before
|
|
106
|
+
* the project stopped following the `cd`. Never sent with a `project_path`
|
|
107
|
+
* the agent named (its relative paths stay relative to it). An older Inbox
|
|
108
|
+
* ignores it; the mod also makes relative attachments absolute itself.
|
|
109
|
+
*/
|
|
110
|
+
export const INBOX_WORKING_FOLDER_META_KEY = 'ai.plannotator/working-folder'
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The `tools/call` params for an Inbox tool call: `wakes` when this session's
|
|
114
|
+
* replies arrive as turns, `workingFolder` (INBOX_WORKING_FOLDER_META_KEY)
|
|
115
|
+
* when the agent works outside the filled `project_path`.
|
|
116
|
+
*/
|
|
117
|
+
export function inboxToolCallParams(
|
|
118
|
+
call: { name: string; arguments: Record<string, unknown> },
|
|
119
|
+
options: { wakes: boolean; workingFolder?: string },
|
|
120
|
+
): Record<string, unknown> {
|
|
121
|
+
const meta: Record<string, unknown> = {
|
|
122
|
+
...(options.wakes ? { [INBOX_WAKES_META_KEY]: true } : {}),
|
|
123
|
+
...(options.workingFolder ? { [INBOX_WORKING_FOLDER_META_KEY]: options.workingFolder } : {}),
|
|
124
|
+
}
|
|
125
|
+
return { name: call.name, arguments: call.arguments, ...(Object.keys(meta).length > 0 ? { _meta: meta } : {}) }
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
/**
|
|
@@ -284,6 +310,17 @@ export const INBOX_SESSION_THREADS_CHANNEL = "Their decisions stay in these thre
|
|
|
284
310
|
export const INBOX_USE_RULE =
|
|
285
311
|
"Use it for a decision only they can make (a :::question block; go on with other work), finished long work (a short summary, files in `attachments`), or when they ask."
|
|
286
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Separate work, separate threads (the owner, 2026-10-29: an agent given new
|
|
315
|
+
* work in its chat kept posting into an old thread on a related subject). The
|
|
316
|
+
* owner's sentence, word for word, without more instruction around it: the
|
|
317
|
+
* `thread` field says how. Fixed text, one wording everywhere: the agent
|
|
318
|
+
* tool's lead and the Inbox's server instructions (INBOX_AGENT_RULES), the
|
|
319
|
+
* reminder, the note, `plannotator inbox --help`, the skill and every
|
|
320
|
+
* `/plannotator-inbox` briefing (tests/inbox-guidance-parity.test.ts).
|
|
321
|
+
*/
|
|
322
|
+
export const INBOX_THREAD_RULE = "One thread per work stream, unless the person says otherwise."
|
|
323
|
+
|
|
287
324
|
/**
|
|
288
325
|
* Follow through, without pinging (the owner, 2026-10-27: an agent promised
|
|
289
326
|
* the person pictures "coming" and left them unsent for an hour, held
|
|
@@ -306,11 +343,16 @@ export const INBOX_SUBAGENT_RULE = "If another agent started you, report to it i
|
|
|
306
343
|
* the agent tool's lead and the Inbox's server instructions carry it whole,
|
|
307
344
|
* so the two can never word them differently.
|
|
308
345
|
*/
|
|
309
|
-
export const INBOX_AGENT_RULES = `${INBOX_USE_RULE} ${INBOX_CHANNEL_RULE} ${INBOX_FOLLOW_THROUGH_RULE} ${INBOX_SUBAGENT_RULE}`
|
|
346
|
+
export const INBOX_AGENT_RULES = `${INBOX_USE_RULE} ${INBOX_THREAD_RULE} ${INBOX_CHANNEL_RULE} ${INBOX_FOLLOW_THROUGH_RULE} ${INBOX_SUBAGENT_RULE}`
|
|
310
347
|
|
|
311
|
-
/**
|
|
348
|
+
/**
|
|
349
|
+
* What the agent tool says, before the per-action lines. Fixed text. Room for
|
|
350
|
+
* INBOX_THREAD_RULE under Claude Code's 2,048 characters came from this first
|
|
351
|
+
* sentence's ", without blocking this session", restored in shorter form as
|
|
352
|
+
* "nothing blocks" by shortening the help line, and from get_guide_brief's lead.
|
|
353
|
+
*/
|
|
312
354
|
export const INBOX_TOOL_LEAD =
|
|
313
|
-
`The Plannotator Inbox on this machine: message the person
|
|
355
|
+
`The Plannotator Inbox on this machine: message the person, get their answer later; nothing blocks. ${INBOX_AGENT_RULES} Set \`action\` to a tool below, with its fields.`
|
|
314
356
|
|
|
315
357
|
/**
|
|
316
358
|
* Fixed text: how a reply comes back. The connection delivers the person's
|
|
@@ -327,7 +369,7 @@ export const INBOX_TOOL_WAKE_NOTE =
|
|
|
327
369
|
* examples), so an agent that needs more reads it once instead of every
|
|
328
370
|
* request carrying it.
|
|
329
371
|
*/
|
|
330
|
-
export const INBOX_TOOL_HELP_NOTE = "Every field and
|
|
372
|
+
export const INBOX_TOOL_HELP_NOTE = "Every field and question syntax: `plannotator inbox --help`."
|
|
331
373
|
|
|
332
374
|
/**
|
|
333
375
|
* Fixed text, one line: the agent tool's description carries it while the
|
|
@@ -350,7 +392,7 @@ export const INBOX_AGENT_ACTION_LEADS: Readonly<Record<string, string>> = {
|
|
|
350
392
|
// What a read shows lives on `thread_id` (a field description does not count toward the 2,048).
|
|
351
393
|
read_thread: 'Read a thread (thread_id); with no id, list your threads (asked_by "anyone": all).',
|
|
352
394
|
search: 'Find messages by words, older ones too (read-only).',
|
|
353
|
-
get_guide_brief: "How to write a guided review
|
|
395
|
+
get_guide_brief: "How to write a guided review, with a worked submit_guide call.",
|
|
354
396
|
card: "A Board card for work that outlasts this reply: create it, then move it and say where it is as the work moves.",
|
|
355
397
|
}
|
|
356
398
|
|
|
@@ -415,14 +457,14 @@ export const INBOX_AGENT_FIELD_DESCRIPTIONS: Readonly<Record<string, Readonly<Re
|
|
|
415
457
|
* the send, before anything about the chat, so it reads as about the Inbox.
|
|
416
458
|
*/
|
|
417
459
|
export const INBOX_REMINDER_TEXT =
|
|
418
|
-
`Plannotator Inbox is on: when you finish long work, or need a decision only the person can make, send it with plannotator_inbox send_message and keep going. ${INBOX_FOLLOW_THROUGH_RULE} Their reply comes back here. A decision in the Inbox stays there: when the person writes here, say what waits and link its thread, but never also ask it in the chat with a blocking question tool (AskUserQuestion or similar). Run \`plannotator inbox --help\` for how it works.`
|
|
460
|
+
`Plannotator Inbox is on: when you finish long work, or need a decision only the person can make, send it with plannotator_inbox send_message and keep going. ${INBOX_FOLLOW_THROUGH_RULE} ${INBOX_THREAD_RULE} Their reply comes back here. A decision in the Inbox stays there: when the person writes here, say what waits and link its thread, but never also ask it in the chat with a blocking question tool (AskUserQuestion or similar). Run \`plannotator inbox --help\` for how it works.`
|
|
419
461
|
|
|
420
462
|
/**
|
|
421
463
|
* The same line for a host without the wake (OpenCode 1): the reply does not
|
|
422
464
|
* come back by itself there, so it names wait_for_reply instead.
|
|
423
465
|
*/
|
|
424
466
|
export const INBOX_REMINDER_TEXT_NO_WAKE =
|
|
425
|
-
`Plannotator Inbox is on: when you finish long work, or need a decision only the person can make, send it with plannotator_inbox send_message and keep going. ${INBOX_FOLLOW_THROUGH_RULE} Call wait_for_reply when you need their answer. A decision in the Inbox stays there: when the person writes here, say what waits and link its thread, but never also ask it in the chat with a blocking question tool (AskUserQuestion or similar). Run \`plannotator inbox --help\` for how it works.`
|
|
467
|
+
`Plannotator Inbox is on: when you finish long work, or need a decision only the person can make, send it with plannotator_inbox send_message and keep going. ${INBOX_FOLLOW_THROUGH_RULE} ${INBOX_THREAD_RULE} Call wait_for_reply when you need their answer. A decision in the Inbox stays there: when the person writes here, say what waits and link its thread, but never also ask it in the chat with a blocking question tool (AskUserQuestion or similar). Run \`plannotator inbox --help\` for how it works.`
|
|
426
468
|
|
|
427
469
|
/**
|
|
428
470
|
* The reminder naming the tool as the host registered it. Claude Code defers
|
|
@@ -463,6 +505,7 @@ export const INBOX_COMMAND_BRIEFING = [
|
|
|
463
505
|
'The person opened their Plannotator Inbox with /plannotator-inbox and has its address. The Plannotator Inbox is one window on this computer where you leave the person messages, `:::question` blocks they answer with a click, and files. They answer when they can. Nothing blocks: you send, then keep working or end your turn.',
|
|
464
506
|
'',
|
|
465
507
|
`- ${INBOX_USE_RULE}`,
|
|
508
|
+
`- ${INBOX_THREAD_RULE}`,
|
|
466
509
|
'- Send with a `plannotator_inbox` tool when you have one (it can be listed with a prefix and may need loading through tool search first): `action` `send_message`, with a markdown `body`. Otherwise call `send_message` on the Inbox\'s MCP server, `plannotator inbox mcp`.',
|
|
467
510
|
"- send_message's result says whether the person's reply arrives in this session by itself. When it does, do not wait or poll for it: end your turn or go on with other work. When it does not, call wait_for_reply when you need the answer.",
|
|
468
511
|
`- ${INBOX_CHANNEL_RULE}`,
|
package/generated/project.ts
CHANGED
|
@@ -22,8 +22,9 @@ export function sanitizeTag(name: string): string | null {
|
|
|
22
22
|
.replace(/[\s_]+/g, "-") // spaces/underscores -> hyphens
|
|
23
23
|
.replace(/[^a-z0-9-]/g, "") // remove special chars
|
|
24
24
|
.replace(/-+/g, "-") // collapse multiple hyphens
|
|
25
|
-
.replace(
|
|
26
|
-
.slice(0, 30)
|
|
25
|
+
.replace(/^-/, "") // trim leading hyphen
|
|
26
|
+
.slice(0, 30) // max 30 chars
|
|
27
|
+
.replace(/-$/, ""); // trim trailing hyphen, after the cut so the result is idempotent
|
|
27
28
|
|
|
28
29
|
return sanitized.length >= 2 ? sanitized : null;
|
|
29
30
|
}
|
|
@@ -1890,8 +1890,8 @@ Recommended: Local only, purged after 30 days
|
|
|
1890
1890
|
- Add a screenshot (\`\`) for anything visual.
|
|
1891
1891
|
- Link a file that shows it better (a plan, a mock): \`[the mock](mock.html)\`. In the Inbox, attach it too; the link opens it beside the thread.
|
|
1892
1892
|
- Choices: \`- [ ] label\`, optionally \`- [ ] label — why\`. The reviewer can also answer "Other", add a note, or skip.
|
|
1893
|
-
- \`Recommended: <label>\` marks yours (\`A, B and C\` on \`:::question-multi\`); other text
|
|
1894
|
-
- Ask only what you cannot decide alone, about 8 a round
|
|
1893
|
+
- \`Recommended: <label>\` marks yours (\`A, B and C\` on \`:::question-multi\`); other text is one more answer, tagged Recommended. Keep A, B, C order.
|
|
1894
|
+
- Ask only what you cannot decide alone, about 8 a round. Do not ask rhetorical questions or questions the codebase answers.
|
|
1895
1895
|
- \`- [x]\` marks a settled choice. On resubmit, check the chosen answer, or drop the block and state the decision in prose.
|
|
1896
1896
|
- Leave blank lines between the parts so the block reads well on GitHub.
|
|
1897
1897
|
- Each answer comes back under its question (\`### Q2. <question> (line N)\`) as \`Answer: <choice>\`, marked \`(your recommendation)\` when the reviewer took yours, or as \`Other: …\`, free text in a quote, or \`Skipped\`, plus any \`Note:\`. A question you marked \`- [x]\` is settled and only comes back if the reviewer changed it or added a note.
|
package/generated/tour-review.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { mkdir, writeFile, readFile, unlink } from "node:fs/promises";
|
|
5
5
|
import { getPlannotatorDataDir } from "./data-dir.ts";
|
|
6
|
+
import { codexAskAiConfigArgs, readCodexConfigTexts } from "./ai/providers/ask-ai-env.ts";
|
|
6
7
|
import type { DiffType } from "./review-core.ts";
|
|
7
8
|
import { getPRPlatformCapabilities, type PRMetadata } from "./pr-provider.ts";
|
|
8
9
|
import { buildWorkspacePromptContextLines, getLocalDiffInstruction, type WorkspaceReviewPromptContext } from "./agent-review-message.ts";
|
|
@@ -449,6 +450,8 @@ export async function buildTourCodexCommand(options: {
|
|
|
449
450
|
...(model ? ["-m", model] : []),
|
|
450
451
|
...(reasoningEffort ? ["-c", `model_reasoning_effort=${reasoningEffort}`] : []),
|
|
451
452
|
...(fastMode ? ["-c", "service_tier=fast"] : []),
|
|
453
|
+
// The Inbox MCP server off: Codex hands a stdio MCP server none of the job's env.
|
|
454
|
+
...codexAskAiConfigArgs(readCodexConfigTexts(cwd)),
|
|
452
455
|
"exec",
|
|
453
456
|
"--output-schema", schemaPath,
|
|
454
457
|
"-o", outputPath,
|