poe-code 3.0.271 → 3.0.272
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/dist/cli/commands/gaslight.js +13 -1
- package/dist/cli/commands/gaslight.js.map +1 -1
- package/dist/index.js +137 -34
- package/dist/index.js.map +3 -3
- package/dist/metafile.json +1 -1
- package/package.json +1 -1
- package/packages/agent-gaslight/dist/ingest.js +64 -18
- package/packages/agent-gaslight/dist/run.js +50 -3
package/package.json
CHANGED
|
@@ -50,10 +50,28 @@ function sanitizeAgentForFileName(agent) {
|
|
|
50
50
|
function resolvePath(cwd, filePath) {
|
|
51
51
|
return path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
52
52
|
}
|
|
53
|
+
function requireNonEmptyString(value, label) {
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
if (trimmed.length === 0) {
|
|
56
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
57
|
+
}
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|
|
60
|
+
function resolveOptionalNonEmptyString(value, label) {
|
|
61
|
+
if (value === undefined) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
if (trimmed.length === 0) {
|
|
66
|
+
throw new Error(`${label} must be a non-empty string when provided.`);
|
|
67
|
+
}
|
|
68
|
+
return trimmed;
|
|
69
|
+
}
|
|
53
70
|
async function resolveOutputPath(fs, cwd, analysisAgent, outputPath) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
const normalizedOutputPath = resolveOptionalNonEmptyString(outputPath, "outputPath");
|
|
72
|
+
if (normalizedOutputPath) {
|
|
73
|
+
const absolutePath = resolvePath(cwd, normalizedOutputPath);
|
|
74
|
+
return { absolutePath, resultPath: normalizedOutputPath };
|
|
57
75
|
}
|
|
58
76
|
const configDirectory = path.join(cwd, ".poe-code");
|
|
59
77
|
const defaultPath = path.join(configDirectory, "gaslight.yaml");
|
|
@@ -77,6 +95,9 @@ function resolveSince(value) {
|
|
|
77
95
|
return milliseconds === null ? undefined : new Date(Date.now() - milliseconds);
|
|
78
96
|
}
|
|
79
97
|
if (value instanceof Date) {
|
|
98
|
+
if (!Number.isFinite(value.getTime())) {
|
|
99
|
+
throw new Error(`Invalid since date "${String(value)}".`);
|
|
100
|
+
}
|
|
80
101
|
return value;
|
|
81
102
|
}
|
|
82
103
|
const milliseconds = parseDuration(value);
|
|
@@ -93,6 +114,13 @@ async function resolveDataPath(cwd, keepDataPath) {
|
|
|
93
114
|
const resultPath = path.join(".poe-code", "ingest", `human-prompts-${process.pid}-${Date.now()}-${process.hrtime.bigint()}.md`);
|
|
94
115
|
return { absolutePath: path.join(cwd, resultPath), resultPath };
|
|
95
116
|
}
|
|
117
|
+
function resolveLimit(value) {
|
|
118
|
+
const limit = value ?? 200;
|
|
119
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
120
|
+
throw new Error("limit must be a positive integer.");
|
|
121
|
+
}
|
|
122
|
+
return limit;
|
|
123
|
+
}
|
|
96
124
|
function buildAnalysisPrompt(dataPath) {
|
|
97
125
|
return [
|
|
98
126
|
"Read this curated Markdown file of human prompts from coding-agent traces:",
|
|
@@ -111,7 +139,7 @@ function buildAnalysisPrompt(dataPath) {
|
|
|
111
139
|
"- Do not put review questions, validation checks, cleanup checks, commit checks, or release checks in `prompt`; those belong in `followups`.",
|
|
112
140
|
"- Prefer concise followups that generalize across tasks.",
|
|
113
141
|
"- Do not produce two followups for the same workflow step; merge semantic duplicates.",
|
|
114
|
-
|
|
142
|
+
'- Repeated short prompts like "commit" are evidence for one well-placed workflow check, not multiple followups.',
|
|
115
143
|
"- Order followups as a useful review sequence: quality, verification, cleanup, then commit or release when supported by the evidence.",
|
|
116
144
|
"- Do not include project secrets, file paths, names, tokens, or one-off task details.",
|
|
117
145
|
"- Preserve the user's direct style when it is reusable.",
|
|
@@ -148,8 +176,7 @@ function stripIdeSelection(value) {
|
|
|
148
176
|
if (endIndex === -1) {
|
|
149
177
|
return stripped;
|
|
150
178
|
}
|
|
151
|
-
stripped =
|
|
152
|
-
stripped.slice(0, startIndex) + stripped.slice(endIndex + closeTag.length);
|
|
179
|
+
stripped = stripped.slice(0, startIndex) + stripped.slice(endIndex + closeTag.length);
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
182
|
function normalizePromptText(value, cwd, homeDir) {
|
|
@@ -234,12 +261,7 @@ function buildRepeatedShortPromptSection(records, cwd, homeDir) {
|
|
|
234
261
|
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
235
262
|
.slice(0, 12);
|
|
236
263
|
if (repeated.length === 0) {
|
|
237
|
-
return [
|
|
238
|
-
"## Repeated short prompts",
|
|
239
|
-
"",
|
|
240
|
-
"No repeated short prompts were found.",
|
|
241
|
-
""
|
|
242
|
-
];
|
|
264
|
+
return ["## Repeated short prompts", "", "No repeated short prompts were found.", ""];
|
|
243
265
|
}
|
|
244
266
|
return [
|
|
245
267
|
"## Repeated short prompts",
|
|
@@ -418,7 +440,10 @@ function extractGeneratedConfigContent(stdout) {
|
|
|
418
440
|
async function writeGeneratedConfig(fs, content, absoluteOutputPath) {
|
|
419
441
|
const yaml = extractYamlCandidate(extractGeneratedConfigContent(content));
|
|
420
442
|
parseGaslightConfig(yaml, "generated gaslight config", { rejectExtraKeys: true });
|
|
421
|
-
|
|
443
|
+
const outputDirectory = path.dirname(absoluteOutputPath);
|
|
444
|
+
await assertNotSymlink(fs, outputDirectory, "Output directory");
|
|
445
|
+
await fs.mkdir(outputDirectory, { recursive: true });
|
|
446
|
+
await assertNotSymlink(fs, outputDirectory, "Output directory");
|
|
422
447
|
const temporaryPath = `${absoluteOutputPath}.tmp-${process.pid}-${Date.now()}`;
|
|
423
448
|
await fs.writeFile(temporaryPath, `${yaml}\n`, { encoding: "utf8" });
|
|
424
449
|
if (fs.rename) {
|
|
@@ -427,19 +452,40 @@ async function writeGeneratedConfig(fs, content, absoluteOutputPath) {
|
|
|
427
452
|
}
|
|
428
453
|
await fs.writeFile(absoluteOutputPath, `${yaml}\n`, { encoding: "utf8" });
|
|
429
454
|
}
|
|
455
|
+
async function assertNotSymlink(fs, targetPath, label) {
|
|
456
|
+
if (!fs.lstat) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const stats = await fs.lstat(targetPath);
|
|
461
|
+
if (stats.isSymbolicLink()) {
|
|
462
|
+
throw new Error(`${label} cannot be a symbolic link: ${targetPath}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (isMissingFile(error)) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
430
472
|
export async function ingestGaslight(options) {
|
|
431
473
|
const cwd = options.cwd ?? process.cwd();
|
|
432
474
|
const homeDir = options.homeDir ?? os.homedir();
|
|
433
475
|
const fs = (options.fs ?? nodeFs);
|
|
434
476
|
const spawn = options.spawn ?? defaultSpawn;
|
|
435
477
|
const collectHumanPrompts = options.collectHumanPrompts ?? collectHumanPromptsWithStats;
|
|
478
|
+
const analysisAgent = requireNonEmptyString(options.analysisAgent, "analysisAgent");
|
|
479
|
+
const model = resolveOptionalNonEmptyString(options.model, "model");
|
|
480
|
+
const limit = resolveLimit(options.limit);
|
|
481
|
+
const outputPathOption = resolveOptionalNonEmptyString(options.outputPath, "outputPath");
|
|
436
482
|
const since = resolveSince(options.since);
|
|
437
483
|
const collection = await collectHumanPrompts({
|
|
438
484
|
sources: options.sources,
|
|
439
485
|
cwd,
|
|
440
486
|
homeDir,
|
|
441
487
|
since,
|
|
442
|
-
limit
|
|
488
|
+
limit,
|
|
443
489
|
allWorkspaces: options.allWorkspaces,
|
|
444
490
|
fs
|
|
445
491
|
});
|
|
@@ -458,20 +504,20 @@ export async function ingestGaslight(options) {
|
|
|
458
504
|
try {
|
|
459
505
|
options.onEvent?.({
|
|
460
506
|
type: "analysis.started",
|
|
461
|
-
agent:
|
|
507
|
+
agent: analysisAgent,
|
|
462
508
|
dataPath: dataPath.absolutePath
|
|
463
509
|
});
|
|
464
|
-
const result = await spawn(
|
|
510
|
+
const result = await spawn(analysisAgent, {
|
|
465
511
|
prompt: buildAnalysisPrompt(dataPath.absolutePath),
|
|
466
512
|
cwd,
|
|
467
513
|
mode: "read",
|
|
468
|
-
...(
|
|
514
|
+
...(model ? { model } : {})
|
|
469
515
|
});
|
|
470
516
|
if (result.exitCode !== 0) {
|
|
471
517
|
const message = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
|
|
472
518
|
throw new Error(`Gaslight ingest analysis failed: ${message}`);
|
|
473
519
|
}
|
|
474
|
-
const outputPath = await resolveOutputPath(fs, cwd,
|
|
520
|
+
const outputPath = await resolveOutputPath(fs, cwd, analysisAgent, outputPathOption);
|
|
475
521
|
await writeGeneratedConfig(fs, result.stdout, outputPath.absolutePath);
|
|
476
522
|
options.onEvent?.({ type: "config.written", path: outputPath.resultPath });
|
|
477
523
|
return {
|
|
@@ -91,6 +91,23 @@ async function archivePlan(fs, cwd, planPath) {
|
|
|
91
91
|
}
|
|
92
92
|
return archivedPath;
|
|
93
93
|
}
|
|
94
|
+
function archivePathForPlan(cwd, planPath) {
|
|
95
|
+
const absolutePath = path.resolve(cwd, planPath);
|
|
96
|
+
return path.join(path.dirname(absolutePath), "archive", path.basename(absolutePath));
|
|
97
|
+
}
|
|
98
|
+
async function assertArchiveDestinationAvailable(fs, cwd, planPath) {
|
|
99
|
+
const archivedPath = archivePathForPlan(cwd, planPath);
|
|
100
|
+
try {
|
|
101
|
+
await fs.readFile(archivedPath, "utf8");
|
|
102
|
+
throw new Error(`Archive destination already exists: ${archivedPath}`);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (!isMissingFile(error)) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
await rejectArchiveSymlink(fs, path.dirname(archivedPath));
|
|
110
|
+
}
|
|
94
111
|
function validateInlineConfig(prompt, followups) {
|
|
95
112
|
if ((prompt === undefined) !== (followups === undefined)) {
|
|
96
113
|
throw new Error("prompt and followups must be provided together.");
|
|
@@ -103,6 +120,23 @@ function validateInlineConfig(prompt, followups) {
|
|
|
103
120
|
throw new Error("followups must be a non-empty array of non-empty strings.");
|
|
104
121
|
}
|
|
105
122
|
}
|
|
123
|
+
function requireNonEmptyString(value, label) {
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
if (trimmed.length === 0) {
|
|
126
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
127
|
+
}
|
|
128
|
+
return trimmed;
|
|
129
|
+
}
|
|
130
|
+
function resolveModel(value) {
|
|
131
|
+
if (value === undefined) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const trimmed = value.trim();
|
|
135
|
+
if (trimmed.length === 0) {
|
|
136
|
+
throw new Error("model must be a non-empty string when provided.");
|
|
137
|
+
}
|
|
138
|
+
return trimmed;
|
|
139
|
+
}
|
|
106
140
|
function resolvePlanPaths(options) {
|
|
107
141
|
if (options.planPaths.length === 0) {
|
|
108
142
|
throw new Error("Provide at least one plan path.");
|
|
@@ -112,17 +146,30 @@ function resolvePlanPaths(options) {
|
|
|
112
146
|
throw new Error("plan paths must be non-empty strings.");
|
|
113
147
|
}
|
|
114
148
|
}
|
|
115
|
-
|
|
149
|
+
const planPaths = options.planPaths.map((planPath) => planPath.trim());
|
|
150
|
+
const seen = new Map();
|
|
151
|
+
for (const planPath of planPaths) {
|
|
152
|
+
const resolvedPath = path.resolve(options.cwd ?? process.cwd(), planPath);
|
|
153
|
+
const duplicate = seen.get(resolvedPath);
|
|
154
|
+
if (duplicate !== undefined) {
|
|
155
|
+
throw new Error(`Duplicate plan path: ${duplicate}`);
|
|
156
|
+
}
|
|
157
|
+
seen.set(resolvedPath, planPath);
|
|
158
|
+
}
|
|
159
|
+
return planPaths;
|
|
116
160
|
}
|
|
117
161
|
export async function runGaslight(options) {
|
|
118
162
|
const cwd = options.cwd ?? process.cwd();
|
|
119
163
|
const homeDir = options.homeDir ?? os.homedir();
|
|
120
164
|
const fs = (options.fs ?? nodeFs);
|
|
121
165
|
const spawn = options.spawn ?? defaultSpawn;
|
|
166
|
+
const agent = requireNonEmptyString(options.agent, "agent");
|
|
167
|
+
const model = resolveModel(options.model);
|
|
122
168
|
validateInlineConfig(options.prompt, options.followups);
|
|
123
169
|
const planPaths = resolvePlanPaths(options);
|
|
124
170
|
for (const planPath of planPaths) {
|
|
125
171
|
await requirePlan(fs, cwd, planPath);
|
|
172
|
+
await assertArchiveDestinationAvailable(fs, cwd, planPath);
|
|
126
173
|
}
|
|
127
174
|
const config = options.prompt !== undefined && options.followups !== undefined
|
|
128
175
|
? { prompt: options.prompt.trim(), followups: options.followups.map((value) => value.trim()) }
|
|
@@ -146,11 +193,11 @@ export async function runGaslight(options) {
|
|
|
146
193
|
planIndex: planIndex + 1,
|
|
147
194
|
totalPlans: planPaths.length
|
|
148
195
|
});
|
|
149
|
-
const result = await spawn(
|
|
196
|
+
const result = await spawn(agent, {
|
|
150
197
|
prompt,
|
|
151
198
|
cwd,
|
|
152
199
|
mode: options.mode ?? "edit",
|
|
153
|
-
...(
|
|
200
|
+
...(model ? { model } : {}),
|
|
154
201
|
...(resumeThreadId ? { resumeThreadId } : {}),
|
|
155
202
|
...(options.signal ? { signal: options.signal } : {})
|
|
156
203
|
});
|