@ilya-lesikov/pi-pi 0.10.0 → 0.12.0
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/3p/pi-subagents/package.json +0 -1
- package/3p/pi-subagents/src/agent-manager.ts +1 -0
- package/3p/pi-subagents/src/index.ts +1 -0
- package/3p/pi-subagents/src/types.ts +8 -0
- package/extensions/orchestrator/agents/advisor.ts +2 -1
- package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/constraints.test.ts +43 -10
- package/extensions/orchestrator/agents/constraints.ts +26 -5
- package/extensions/orchestrator/agents/deep-debugger.ts +7 -6
- package/extensions/orchestrator/agents/explore.ts +1 -1
- package/extensions/orchestrator/agents/librarian.ts +1 -1
- package/extensions/orchestrator/agents/planner.ts +1 -1
- package/extensions/orchestrator/agents/prompts.test.ts +135 -1
- package/extensions/orchestrator/agents/reviewer.ts +2 -2
- package/extensions/orchestrator/agents/task.ts +6 -2
- package/extensions/orchestrator/agents/tool-routing.ts +22 -6
- package/extensions/orchestrator/assumptions.test.ts +77 -0
- package/extensions/orchestrator/assumptions.ts +85 -0
- package/extensions/orchestrator/cbm.ts +4 -1
- package/extensions/orchestrator/cbm.which.test.ts +92 -0
- package/extensions/orchestrator/command-handlers.ts +9 -1
- package/extensions/orchestrator/config.test.ts +1 -3
- package/extensions/orchestrator/config.ts +3 -4
- package/extensions/orchestrator/context.test.ts +2 -7
- package/extensions/orchestrator/context.ts +3 -3
- package/extensions/orchestrator/doctor.test.ts +35 -0
- package/extensions/orchestrator/doctor.ts +4 -2
- package/extensions/orchestrator/event-handlers.more.test.ts +185 -0
- package/extensions/orchestrator/event-handlers.test.ts +22 -2
- package/extensions/orchestrator/event-handlers.ts +156 -18
- package/extensions/orchestrator/flant-infra.more.test.ts +55 -0
- package/extensions/orchestrator/flant-infra.test.ts +0 -3
- package/extensions/orchestrator/flant-infra.ts +17 -7
- package/extensions/orchestrator/integration.test.ts +355 -173
- package/extensions/orchestrator/orchestrator.ts +3 -9
- package/extensions/orchestrator/phases/brainstorm.test.ts +25 -7
- package/extensions/orchestrator/phases/brainstorm.ts +35 -71
- package/extensions/orchestrator/phases/implementation.test.ts +22 -0
- package/extensions/orchestrator/phases/implementation.ts +6 -0
- package/extensions/orchestrator/phases/machine.test.ts +0 -28
- package/extensions/orchestrator/phases/machine.ts +0 -38
- package/extensions/orchestrator/phases/planning.test.ts +27 -1
- package/extensions/orchestrator/phases/planning.ts +1 -1
- package/extensions/orchestrator/phases/review-task.test.ts +7 -0
- package/extensions/orchestrator/phases/review-task.ts +1 -1
- package/extensions/orchestrator/phases/review.test.ts +47 -10
- package/extensions/orchestrator/phases/review.ts +36 -25
- package/extensions/orchestrator/pp-menu.from.test.ts +65 -0
- package/extensions/orchestrator/pp-menu.leaves.test.ts +17 -0
- package/extensions/orchestrator/pp-menu.test.ts +106 -49
- package/extensions/orchestrator/pp-menu.ts +280 -134
- package/extensions/orchestrator/pp-state-tools.ts +1 -1
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +2 -2
- package/extensions/orchestrator/rate-limit-fallback.ts +1 -1
- package/extensions/orchestrator/state.test.ts +17 -17
- package/extensions/orchestrator/state.ts +35 -12
- package/package.json +6 -3
- package/scripts/lib/smoke-resolve.mjs +99 -0
- package/scripts/postinstall.mjs +93 -0
- package/scripts/test-package.sh +62 -0
- package/scripts/postinstall.sh +0 -18
|
@@ -138,7 +138,7 @@ function validateStateContent(taskDir: string, absolute: string, content: string
|
|
|
138
138
|
const res = validatePlan(content);
|
|
139
139
|
return res.ok
|
|
140
140
|
? { ok: true }
|
|
141
|
-
: { ok: false, errors: res.errors, hint: "Keep exactly: # Plan, ## Scope, ## Checklist (items need 'Done when:'), ## Blockers (optional)." };
|
|
141
|
+
: { ok: false, errors: res.errors, hint: "Keep exactly: # Plan, ## Scope, ## Checklist (items need 'Done when:'), ## Pattern constraints (optional), ## Blockers (optional)." };
|
|
142
142
|
}
|
|
143
143
|
return { ok: true };
|
|
144
144
|
}
|
|
@@ -30,14 +30,14 @@ import { handleMainRateLimit, handleSubagentRateLimit } from "./rate-limit-fallb
|
|
|
30
30
|
|
|
31
31
|
function makeOrchestrator() {
|
|
32
32
|
return {
|
|
33
|
-
active: { state: { phase: "
|
|
33
|
+
active: { state: { phase: "implement" } },
|
|
34
34
|
activeTaskToken: 1,
|
|
35
35
|
subFallbackActive: false,
|
|
36
36
|
subFallbackDialogPending: false,
|
|
37
37
|
subFallbackPendingDecision: false,
|
|
38
38
|
subFallbackModelId: null as string | null,
|
|
39
39
|
subSwitchBackTimer: null as any,
|
|
40
|
-
config: { agents: { orchestrators: {
|
|
40
|
+
config: { agents: { orchestrators: { implement: { thinking: "high" } } } },
|
|
41
41
|
switchModel: vi.fn().mockResolvedValue(true),
|
|
42
42
|
sendUserMessageWhenIdle: vi.fn(),
|
|
43
43
|
cancelPendingRetry: vi.fn(),
|
|
@@ -160,7 +160,7 @@ async function activateFallback(
|
|
|
160
160
|
function currentThinking(orchestrator: Orchestrator): string {
|
|
161
161
|
const phase = orchestrator.active?.state.phase;
|
|
162
162
|
const orchestrators = orchestrator.config?.agents?.orchestrators as Record<string, { thinking?: string }> | undefined;
|
|
163
|
-
const key = phase === "
|
|
163
|
+
const key = phase === "brainstorm" || phase === "review" || phase === "quick" ? phase : "implement";
|
|
164
164
|
return orchestrators?.[key]?.thinking ?? "high";
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -52,15 +52,6 @@ describe("createTask", () => {
|
|
|
52
52
|
expect(raw.endsWith("\n")).toBe(true);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it("uses debug as initial phase for debug", () => {
|
|
56
|
-
const cwd = makeCwd();
|
|
57
|
-
const taskDir = createTask(cwd, "debug", "Fix timeout issue");
|
|
58
|
-
const state = loadTask(taskDir);
|
|
59
|
-
expect(state.phase).toBe("debug");
|
|
60
|
-
expect(state.step).toBe("llm_work");
|
|
61
|
-
expect(state.reviewCycle).toBeNull();
|
|
62
|
-
expect(state.reviewPass).toBe(0);
|
|
63
|
-
});
|
|
64
55
|
|
|
65
56
|
it("uses brainstorm as initial phase for brainstorm", () => {
|
|
66
57
|
const cwd = makeCwd();
|
|
@@ -103,6 +94,16 @@ describe("loadTask", () => {
|
|
|
103
94
|
|
|
104
95
|
expect(() => loadTask(taskDir)).toThrowError(/Failed to parse .*state\.json:/);
|
|
105
96
|
});
|
|
97
|
+
|
|
98
|
+
it("throws for a legacy debug-phase task", () => {
|
|
99
|
+
const cwd = makeCwd();
|
|
100
|
+
const taskDir = createTask(cwd, "implement", "Legacy debug");
|
|
101
|
+
const state = loadTask(taskDir);
|
|
102
|
+
(state as any).phase = "debug";
|
|
103
|
+
saveTask(taskDir, state);
|
|
104
|
+
|
|
105
|
+
expect(() => loadTask(taskDir)).toThrowError(/Debug tasks are no longer supported/);
|
|
106
|
+
});
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
describe("saveTask", () => {
|
|
@@ -134,18 +135,18 @@ describe("listTasks", () => {
|
|
|
134
135
|
it("lists only non-done tasks and respects type filter", () => {
|
|
135
136
|
const cwd = makeCwd();
|
|
136
137
|
const implementTask = createTask(cwd, "implement", "Implement feature");
|
|
137
|
-
const
|
|
138
|
+
const reviewTask = createTask(cwd, "review", "Review crash");
|
|
138
139
|
const brainstormTask = createTask(cwd, "brainstorm", "Idea storm");
|
|
139
140
|
|
|
140
|
-
const doneState = loadTask(
|
|
141
|
+
const doneState = loadTask(reviewTask);
|
|
141
142
|
doneState.phase = "done";
|
|
142
|
-
saveTask(
|
|
143
|
+
saveTask(reviewTask, doneState);
|
|
143
144
|
|
|
144
145
|
const all = listTasks(cwd);
|
|
145
146
|
const allDirs = all.map((t) => t.dir);
|
|
146
147
|
expect(allDirs).toContain(implementTask);
|
|
147
148
|
expect(allDirs).toContain(brainstormTask);
|
|
148
|
-
expect(allDirs).not.toContain(
|
|
149
|
+
expect(allDirs).not.toContain(reviewTask);
|
|
149
150
|
|
|
150
151
|
const brainstormOnly = listTasks(cwd, "brainstorm");
|
|
151
152
|
expect(brainstormOnly).toHaveLength(1);
|
|
@@ -384,7 +385,7 @@ describe("getActiveTask", () => {
|
|
|
384
385
|
it("returns null when multiple unlocked tasks exist (ambiguous)", () => {
|
|
385
386
|
const cwd = makeCwd();
|
|
386
387
|
createTask(cwd, "implement", "First abandoned");
|
|
387
|
-
createTask(cwd, "
|
|
388
|
+
createTask(cwd, "review", "Second abandoned");
|
|
388
389
|
vi.spyOn(lockfile, "checkSync").mockReturnValue(false);
|
|
389
390
|
|
|
390
391
|
expect(getActiveTask(cwd)).toBeNull();
|
|
@@ -393,7 +394,7 @@ describe("getActiveTask", () => {
|
|
|
393
394
|
it("returns the single unlocked task when others are locked", () => {
|
|
394
395
|
const cwd = makeCwd();
|
|
395
396
|
const lockedTask = createTask(cwd, "implement", "Locked by other");
|
|
396
|
-
const unlockedTask = createTask(cwd, "
|
|
397
|
+
const unlockedTask = createTask(cwd, "review", "Abandoned");
|
|
397
398
|
|
|
398
399
|
vi.spyOn(lockfile, "checkSync").mockImplementation((path: string, _options?: { stale?: number }) => {
|
|
399
400
|
if (path === join(lockedTask, "state.json")) return true;
|
|
@@ -434,7 +435,7 @@ describe("getActiveTaskStatus", () => {
|
|
|
434
435
|
it("reports ambiguous with all tasks when multiple are unlocked", () => {
|
|
435
436
|
const cwd = makeCwd();
|
|
436
437
|
createTask(cwd, "implement", "First");
|
|
437
|
-
createTask(cwd, "
|
|
438
|
+
createTask(cwd, "review", "Second");
|
|
438
439
|
vi.spyOn(lockfile, "checkSync").mockReturnValue(false);
|
|
439
440
|
|
|
440
441
|
const status = getActiveTaskStatus(cwd);
|
|
@@ -447,7 +448,6 @@ describe("getFirstPhase", () => {
|
|
|
447
448
|
it("returns expected first phase for each task type", () => {
|
|
448
449
|
expect(getFirstPhase("implement")).toBe("brainstorm");
|
|
449
450
|
expect(getFirstPhase("brainstorm")).toBe("brainstorm");
|
|
450
|
-
expect(getFirstPhase("debug")).toBe("debug");
|
|
451
451
|
expect(getFirstPhase("review")).toBe("review");
|
|
452
452
|
expect(getFirstPhase("quick")).toBe("quick");
|
|
453
453
|
});
|
|
@@ -28,14 +28,13 @@ export interface AutonomousConfig {
|
|
|
28
28
|
phases: Record<string, AutonomousPhaseConfig>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export type TaskType = "implement" | "
|
|
31
|
+
export type TaskType = "implement" | "brainstorm" | "review" | "quick";
|
|
32
32
|
|
|
33
33
|
export type ImplementPhase = "brainstorm" | "plan" | "implement" | "done";
|
|
34
|
-
export type DebugPhase = "debug" | "plan" | "implement" | "done";
|
|
35
34
|
export type BrainstormPhase = "brainstorm" | "plan" | "implement" | "done";
|
|
36
35
|
export type ReviewPhase = "review" | "plan" | "implement" | "done";
|
|
37
36
|
export type QuickPhase = "quick";
|
|
38
|
-
export type Phase = ImplementPhase |
|
|
37
|
+
export type Phase = ImplementPhase | BrainstormPhase | ReviewPhase | QuickPhase;
|
|
39
38
|
|
|
40
39
|
export interface TaskState {
|
|
41
40
|
phase: Phase;
|
|
@@ -76,10 +75,24 @@ export interface TaskState {
|
|
|
76
75
|
// the guided/autonomous transition or the autonomous terminal handoff), so the
|
|
77
76
|
// hooks never run twice for the same completion.
|
|
78
77
|
afterImplementRan?: boolean;
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
78
|
+
// State-freshness reconcile markers (see maybePromptReconcile). Absent on legacy
|
|
79
|
+
// tasks. reconcilePending forces a re-prompt after a pause/complete that could
|
|
80
|
+
// not reconcile in-line.
|
|
81
|
+
reconcilePromptedPhase?: string;
|
|
82
|
+
reconciledPhase?: string;
|
|
83
|
+
reconcilePending?: boolean;
|
|
84
|
+
// Per-repo Plannotator review cursor. repoPaths is the ordered set of repos to
|
|
85
|
+
// review; index is read only by the single-repo path (retained for
|
|
86
|
+
// backward-compatible deserialization of older cursors). For multi-repo reviews
|
|
87
|
+
// the user drives repo selection via a status picker; `status` records each
|
|
88
|
+
// repo's outcome (absent = unreviewed) and `feedback` holds pending
|
|
89
|
+
// changes-requested feedback not yet applied. Undefined = no active loop.
|
|
90
|
+
plannotatorCursor?: {
|
|
91
|
+
repoPaths: string[];
|
|
92
|
+
index: number;
|
|
93
|
+
status?: Record<string, "approved" | "changes-requested" | "fixes-applied">;
|
|
94
|
+
feedback?: Record<string, string>;
|
|
95
|
+
};
|
|
83
96
|
// The phase the task was in when it was marked done. Recorded so the Resume
|
|
84
97
|
// "reopen a done task" flow (#2) can restore the actual last working phase
|
|
85
98
|
// (done itself carries no phase history). Absent on legacy done tasks.
|
|
@@ -102,7 +115,6 @@ function taskStatePath(taskDir: string): string {
|
|
|
102
115
|
|
|
103
116
|
export function getFirstPhase(type: TaskType): Exclude<Phase, "done"> {
|
|
104
117
|
if (type === "implement" || type === "brainstorm") return "brainstorm";
|
|
105
|
-
if (type === "debug") return "debug";
|
|
106
118
|
if (type === "review") return "review";
|
|
107
119
|
return "quick";
|
|
108
120
|
}
|
|
@@ -115,7 +127,7 @@ export function getEffectiveMode(state: TaskState): TaskMode | undefined {
|
|
|
115
127
|
// drives them, so they never inherit the task's autonomous mode even when one is set.
|
|
116
128
|
// plan/implement are autonomous-capable regardless of whether they are the task's initial phase.
|
|
117
129
|
export function getEffectivePhaseMode(state: TaskState): TaskMode {
|
|
118
|
-
if (state.phase === "brainstorm" || state.phase === "
|
|
130
|
+
if (state.phase === "brainstorm" || state.phase === "review") return "guided";
|
|
119
131
|
return getEffectiveMode(state) ?? "guided";
|
|
120
132
|
}
|
|
121
133
|
|
|
@@ -162,8 +174,19 @@ export function createTask(cwd: string, type: TaskType, description: string, mod
|
|
|
162
174
|
export function loadTask(taskDir: string): TaskState {
|
|
163
175
|
const sp = taskStatePath(taskDir);
|
|
164
176
|
const raw = readFileSync(sp, "utf-8");
|
|
177
|
+
let parsed: TaskState;
|
|
178
|
+
try {
|
|
179
|
+
parsed = JSON.parse(raw) as TaskState;
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
throw new Error(`Failed to parse ${sp}: ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
if ((parsed as any).phase === "debug") {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"Debug tasks are no longer supported; use /pp → Implement → From to reuse their artifacts.",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
165
188
|
try {
|
|
166
|
-
const state =
|
|
189
|
+
const state = parsed;
|
|
167
190
|
if ((state as any).phase === "planning") {
|
|
168
191
|
state.phase = "plan";
|
|
169
192
|
}
|
|
@@ -203,7 +226,7 @@ export function listTasks(cwd: string, typeOrOptions?: TaskType | ListTasksOptio
|
|
|
203
226
|
const base = stateDir(cwd);
|
|
204
227
|
if (!existsSync(base)) return [];
|
|
205
228
|
|
|
206
|
-
const types: TaskType[] = options.type ? [options.type] : ["implement", "
|
|
229
|
+
const types: TaskType[] = options.type ? [options.type] : ["implement", "brainstorm", "review", "quick"];
|
|
207
230
|
const results: TaskInfo[] = [];
|
|
208
231
|
|
|
209
232
|
for (const t of types) {
|
|
@@ -343,7 +366,7 @@ function firstMarkdownContent(path: string): string | null {
|
|
|
343
366
|
export function taskFullName(taskDir: string, state: TaskState): string {
|
|
344
367
|
let desc = state.description ?? "";
|
|
345
368
|
|
|
346
|
-
if (["implement", "
|
|
369
|
+
if (["implement", "brainstorm", "review", "quick"].includes(desc)) {
|
|
347
370
|
const fallback =
|
|
348
371
|
firstMarkdownContent(join(taskDir, "USER_REQUEST.md")) ??
|
|
349
372
|
firstMarkdownContent(join(taskDir, "RESEARCH.md")) ??
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ilya-lesikov/pi-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -33,11 +33,12 @@
|
|
|
33
33
|
"AGENTS.md"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
-
"postinstall": "
|
|
36
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
37
37
|
"test": "vitest run --exclude '3p/**'",
|
|
38
38
|
"test:watch": "vitest --exclude '3p/**'",
|
|
39
39
|
"test:3p": "bash scripts/test-3p.sh",
|
|
40
|
-
"test:
|
|
40
|
+
"test:package": "bash scripts/test-package.sh",
|
|
41
|
+
"test:all": "vitest run --exclude '3p/**' && bash scripts/test-3p.sh && bash scripts/test-package.sh",
|
|
41
42
|
"coverage": "vitest run --exclude '3p/**' --coverage"
|
|
42
43
|
},
|
|
43
44
|
"pi": {
|
|
@@ -53,8 +54,10 @@
|
|
|
53
54
|
"dependencies": {
|
|
54
55
|
"@joplin/turndown-plugin-gfm": "^1.0.64",
|
|
55
56
|
"@pierre/diffs": "^1.2.0",
|
|
57
|
+
"croner": "^10.0.1",
|
|
56
58
|
"effect": "^3.0.0",
|
|
57
59
|
"minimatch": "^10.0.0",
|
|
60
|
+
"nanoid": "^5.0.0",
|
|
58
61
|
"pino": "^10.3.1",
|
|
59
62
|
"proper-lockfile": "^4.1.2",
|
|
60
63
|
"turndown": "^7.2.0",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Runs from INSIDE the installed package directory (import.meta.url is under the package), so
|
|
2
|
+
// dynamic import() of a bare specifier resolves through the installed artifact's own
|
|
3
|
+
// node_modules chain — the same resolution pi performs at extension load. Exits non-zero if a
|
|
4
|
+
// registered extension path is missing or a required runtime specifier fails to load.
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const PKG_DIR = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
function fail(msg) {
|
|
12
|
+
console.error(` ✗ ${msg}`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8"));
|
|
17
|
+
|
|
18
|
+
const extEntries = pkg?.pi?.extensions ?? [];
|
|
19
|
+
if (extEntries.length === 0) fail("package.json has no pi.extensions entries");
|
|
20
|
+
for (const entry of extEntries) {
|
|
21
|
+
if (existsSync(resolve(PKG_DIR, entry))) {
|
|
22
|
+
console.log(` ✓ extension present: ${entry}`);
|
|
23
|
+
} else {
|
|
24
|
+
fail(`registered extension path missing from artifact: ${entry}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Bare specifiers pi itself supplies to loaded extensions — never mirrored into dependencies.
|
|
29
|
+
const peerSupplied = new Set(Object.keys(pkg.peerDependencies ?? {}));
|
|
30
|
+
|
|
31
|
+
function isBuiltin(spec) {
|
|
32
|
+
return spec.startsWith("node:");
|
|
33
|
+
}
|
|
34
|
+
function isRelative(spec) {
|
|
35
|
+
return spec.startsWith(".") || spec.startsWith("/");
|
|
36
|
+
}
|
|
37
|
+
function packageName(spec) {
|
|
38
|
+
const parts = spec.split("/");
|
|
39
|
+
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectTsFiles(dir) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const name of readdirSync(dir)) {
|
|
45
|
+
const full = join(dir, name);
|
|
46
|
+
const st = statSync(full);
|
|
47
|
+
if (st.isDirectory()) out.push(...collectTsFiles(full));
|
|
48
|
+
else if (name.endsWith(".ts")) out.push(full);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Scope the scan to the pi-subagents source only. Other shipped 3p sources reference SDK
|
|
54
|
+
// specifiers behind lazy `await import()` guards that are intentionally NOT root dependencies;
|
|
55
|
+
// scanning them would produce a permanently failing test.
|
|
56
|
+
const SCAN_DIR = join(PKG_DIR, "3p", "pi-subagents", "src");
|
|
57
|
+
if (!existsSync(SCAN_DIR)) {
|
|
58
|
+
fail(`expected shipped source dir missing: 3p/pi-subagents/src`);
|
|
59
|
+
process.exit(process.exitCode ?? 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Value (non-type) import/export ... from "spec" and dynamic import("spec"). `import type` /
|
|
63
|
+
// `export type` statements are type-only and erased at build, so they carry no runtime requirement.
|
|
64
|
+
const FROM_RE = /(^|\n)\s*(import|export)\s+(?!type\b)(?:[^;]*?\sfrom\s+)?["']([^"']+)["']/g;
|
|
65
|
+
const DYN_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
66
|
+
|
|
67
|
+
const required = new Set();
|
|
68
|
+
for (const file of collectTsFiles(SCAN_DIR)) {
|
|
69
|
+
const src = readFileSync(file, "utf8");
|
|
70
|
+
for (const m of src.matchAll(FROM_RE)) addSpec(m[3]);
|
|
71
|
+
for (const m of src.matchAll(DYN_RE)) addSpec(m[1]);
|
|
72
|
+
}
|
|
73
|
+
function addSpec(spec) {
|
|
74
|
+
if (isBuiltin(spec) || isRelative(spec)) return;
|
|
75
|
+
const name = packageName(spec);
|
|
76
|
+
if (peerSupplied.has(name)) return;
|
|
77
|
+
required.add(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Floor: these two are the runtime imports that broke consumer installs; assert them explicitly
|
|
81
|
+
// even if the extractor's shape ever changes.
|
|
82
|
+
required.add("croner");
|
|
83
|
+
required.add("nanoid");
|
|
84
|
+
|
|
85
|
+
const specs = [...required].sort();
|
|
86
|
+
console.log(` runtime specifiers to resolve: ${specs.join(", ")}`);
|
|
87
|
+
|
|
88
|
+
for (const spec of specs) {
|
|
89
|
+
try {
|
|
90
|
+
await import(spec);
|
|
91
|
+
console.log(` ✓ resolved: ${spec}`);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
fail(`cannot resolve '${spec}' from installed package: ${err?.message ?? err}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (process.exitCode) {
|
|
98
|
+
console.error(" → the root package is missing a runtime dependency its shipped extensions need.");
|
|
99
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Vendor-source decision: on non-Windows we invoke the existing bash `vendor.sh` UNCHANGED so
|
|
2
|
+
// Linux/CI stay byte-for-byte identical; on Windows we SKIP the bash vendor step (a documented
|
|
3
|
+
// Windows limitation) and rely on the npm-pack HTML fallback below. vendor.sh is still the single
|
|
4
|
+
// source used by `npm run build`/CI — this script does not fork its logic.
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdtempSync, rmSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { gunzipSync } from "node:zlib";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const REPO_ROOT = resolve(SCRIPT_DIR, "..");
|
|
14
|
+
const PI_EXT_DIR = join(REPO_ROOT, "3p", "pi-plannotator", "apps", "pi-extension");
|
|
15
|
+
const isWindows = process.platform === "win32";
|
|
16
|
+
|
|
17
|
+
function fail(msg) {
|
|
18
|
+
console.error(` ✗ ${msg}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runVendor() {
|
|
23
|
+
if (isWindows) {
|
|
24
|
+
console.log(" ⚠ skipping plannotator vendor step on Windows (bash vendor.sh unavailable)");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const res = spawnSync("bash", [join(PI_EXT_DIR, "vendor.sh")], { cwd: REPO_ROOT, stdio: "inherit" });
|
|
28
|
+
if (res.status !== 0) fail(`vendor.sh failed with exit code ${res.status ?? "unknown"}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function htmlPresent() {
|
|
32
|
+
return existsSync(join(PI_EXT_DIR, "plannotator.html")) && existsSync(join(PI_EXT_DIR, "review-editor.html"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Extract a specific file from an npm tarball (gzipped POSIX/ustar tar) using only
|
|
36
|
+
// Node builtins — no external `tar` executable, which is not guaranteed on native
|
|
37
|
+
// Windows. Returns the file's bytes, or null when the entry is absent.
|
|
38
|
+
function extractFromTar(tarBytes, wantPath) {
|
|
39
|
+
let off = 0;
|
|
40
|
+
while (off + 512 <= tarBytes.length) {
|
|
41
|
+
const header = tarBytes.subarray(off, off + 512);
|
|
42
|
+
const name = header.subarray(0, 100).toString("utf-8").replace(/\0.*$/, "");
|
|
43
|
+
if (name === "") break;
|
|
44
|
+
const size = parseInt(header.subarray(124, 136).toString("utf-8").replace(/\0.*$/, "").trim() || "0", 8);
|
|
45
|
+
const dataStart = off + 512;
|
|
46
|
+
if (name === wantPath) return tarBytes.subarray(dataStart, dataStart + size);
|
|
47
|
+
off = dataStart + Math.ceil(size / 512) * 512;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Run `npm pack` without depending on PATH/PATHEXT resolution of `npm`, which on
|
|
53
|
+
// Windows is `npm.cmd` and cannot be spawned by execFileSync without a shell
|
|
54
|
+
// (Node rejects .cmd without shell:true). In a lifecycle script npm exports
|
|
55
|
+
// npm_execpath (the CLI's JS entry); run it with the current node. Fall back to a
|
|
56
|
+
// shell-resolved `npm`/`npm.cmd` only if that env var is somehow absent.
|
|
57
|
+
function runNpmPack(destDir) {
|
|
58
|
+
const npmCli = process.env.npm_execpath;
|
|
59
|
+
if (npmCli) {
|
|
60
|
+
execFileSync(process.execPath, [npmCli, "pack", "@plannotator/pi-extension", "--pack-destination", destDir, "--silent"], {
|
|
61
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
66
|
+
execFileSync(npmCmd, ["pack", "@plannotator/pi-extension", "--pack-destination", destDir, "--silent"], {
|
|
67
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
68
|
+
shell: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractHtmlFallback() {
|
|
73
|
+
const tmp = mkdtempSync(join(tmpdir(), "pi-pi-plannotator-"));
|
|
74
|
+
try {
|
|
75
|
+
runNpmPack(tmp);
|
|
76
|
+
const tgz = readdirSync(tmp).find((name) => name.endsWith(".tgz"));
|
|
77
|
+
if (!tgz) fail("npm pack produced no tarball for @plannotator/pi-extension");
|
|
78
|
+
const tarBytes = gunzipSync(readFileSync(join(tmp, tgz)));
|
|
79
|
+
for (const file of ["plannotator.html", "review-editor.html"]) {
|
|
80
|
+
const bytes = extractFromTar(tarBytes, `package/${file}`);
|
|
81
|
+
if (!bytes) fail(`@plannotator/pi-extension tarball is missing ${file}`);
|
|
82
|
+
writeFileSync(join(PI_EXT_DIR, file), bytes);
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
runVendor();
|
|
90
|
+
|
|
91
|
+
if (htmlPresent()) process.exit(0);
|
|
92
|
+
|
|
93
|
+
extractHtmlFallback();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Release-layout smoke test: pack the root package, install the tarball into a throwaway
|
|
3
|
+
# directory OUTSIDE this checkout, and verify the shipped extension artifact resolves its
|
|
4
|
+
# runtime imports from the installed tree alone.
|
|
5
|
+
#
|
|
6
|
+
# Why this exists: the root `npm test` excludes 3p/** and the vendored suites run inside
|
|
7
|
+
# 3p/*/node_modules, so a bare import that a vendored extension needs but the root package
|
|
8
|
+
# forgets to declare is invisible in the dev checkout (its node_modules still has the package).
|
|
9
|
+
# It only surfaces after a clean `npm install` by a consumer — exactly the croner/nanoid
|
|
10
|
+
# regression this guards against. Resolving bare specifiers from the installed package tree is
|
|
11
|
+
# the only shape that reproduces that failure.
|
|
12
|
+
#
|
|
13
|
+
# Registry access is required (npm install of the tarball fetches deps, runs the root
|
|
14
|
+
# postinstall, and npm v7+ auto-installs peerDependencies). Offline CI: warm the npm cache and
|
|
15
|
+
# export a --prefer-offline-friendly registry mirror.
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
cd "$(dirname "$0")/.."
|
|
18
|
+
ROOT="$(pwd)"
|
|
19
|
+
|
|
20
|
+
TMPDIR_SMOKE="$(mktemp -d)"
|
|
21
|
+
trap 'rm -rf "$TMPDIR_SMOKE"' EXIT
|
|
22
|
+
|
|
23
|
+
echo "▶ packing root package"
|
|
24
|
+
PACK_JSON="$(npm pack --json --pack-destination "$TMPDIR_SMOKE" 2>/dev/null)"
|
|
25
|
+
TARBALL="$(node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s);process.stdout.write(a[0].filename)})' <<<"$PACK_JSON")"
|
|
26
|
+
if [ -z "$TARBALL" ] || [ ! -f "$TMPDIR_SMOKE/$TARBALL" ]; then
|
|
27
|
+
echo "ERROR: npm pack did not produce a tarball (got: '$TARBALL')." >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
echo " tarball: $TARBALL"
|
|
31
|
+
|
|
32
|
+
INSTALL_DIR="$TMPDIR_SMOKE/install"
|
|
33
|
+
mkdir -p "$INSTALL_DIR"
|
|
34
|
+
( cd "$INSTALL_DIR" && npm init -y >/dev/null 2>&1 )
|
|
35
|
+
|
|
36
|
+
echo "▶ installing tarball into a clean consumer directory"
|
|
37
|
+
if ! ( cd "$INSTALL_DIR" && npm install "$TMPDIR_SMOKE/$TARBALL" --no-audit --no-fund 2>&1 ); then
|
|
38
|
+
echo "ERROR: installing the packed tarball failed." >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
PKG_NAME="$(node -e 'process.stdout.write(require("./package.json").name)')"
|
|
43
|
+
PKG_DIR="$INSTALL_DIR/node_modules/$PKG_NAME"
|
|
44
|
+
if [ ! -d "$PKG_DIR" ]; then
|
|
45
|
+
echo "ERROR: installed package not found at $PKG_DIR" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Run the resolution check from a helper placed INSIDE the installed package, so bare-specifier
|
|
50
|
+
# resolution provably walks the artifact's own node_modules hierarchy (not this checkout's).
|
|
51
|
+
CHECK="$PKG_DIR/__smoke-resolve.mjs"
|
|
52
|
+
cp "$ROOT/scripts/lib/smoke-resolve.mjs" "$CHECK"
|
|
53
|
+
|
|
54
|
+
echo "▶ resolving shipped extension runtime imports from the install tree"
|
|
55
|
+
( cd "$PKG_DIR" && node "$CHECK" )
|
|
56
|
+
STATUS=$?
|
|
57
|
+
|
|
58
|
+
if [ $STATUS -ne 0 ]; then
|
|
59
|
+
echo "✗ package smoke test FAILED" >&2
|
|
60
|
+
exit $STATUS
|
|
61
|
+
fi
|
|
62
|
+
echo "✓ package smoke test passed"
|
package/scripts/postinstall.sh
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
cd "$(dirname "$0")/.."
|
|
4
|
-
|
|
5
|
-
bash 3p/pi-plannotator/apps/pi-extension/vendor.sh
|
|
6
|
-
|
|
7
|
-
PI_EXT_DIR="3p/pi-plannotator/apps/pi-extension"
|
|
8
|
-
if [ -f "$PI_EXT_DIR/plannotator.html" ] && [ -f "$PI_EXT_DIR/review-editor.html" ]; then
|
|
9
|
-
exit 0
|
|
10
|
-
fi
|
|
11
|
-
|
|
12
|
-
TMPDIR=$(mktemp -d)
|
|
13
|
-
trap 'rm -rf "$TMPDIR"' EXIT
|
|
14
|
-
|
|
15
|
-
npm pack @plannotator/pi-extension --pack-destination "$TMPDIR" --silent 2>/dev/null
|
|
16
|
-
tar -xzf "$TMPDIR"/*.tgz -C "$TMPDIR"
|
|
17
|
-
cp "$TMPDIR/package/plannotator.html" "$PI_EXT_DIR/plannotator.html"
|
|
18
|
-
cp "$TMPDIR/package/review-editor.html" "$PI_EXT_DIR/review-editor.html"
|