@danypops/papyrus 0.27.11 → 0.27.13
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/extension/src/discuss.ts +6 -2
- package/extension/src/domain-tools.ts +8 -3
- package/package.json +1 -1
- package/src/ops.ts +22 -15
package/extension/src/discuss.ts
CHANGED
|
@@ -83,9 +83,13 @@ export async function showDiscussions(ctx: ExtensionCommandContext): Promise<voi
|
|
|
83
83
|
if (choice === "Reply") {
|
|
84
84
|
const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
|
|
85
85
|
const question = `Reply to "${discussion.title}":`;
|
|
86
|
+
// Same fix as the live discuss tool: the title alone isn't the actual question -- show
|
|
87
|
+
// the most recent round's real content as context, not a bare title prompt.
|
|
88
|
+
const transcript = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.show", { id: discussion.id });
|
|
89
|
+
const context = transcript.rounds.at(-1)?.content?.trim() || undefined;
|
|
86
90
|
const answer = pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode
|
|
87
|
-
? await askQuestion(commandCtx, { question, options: pending.pendingOptions.map((title) => ({ title })), allowMultiple: pending.pendingOptionsMode === "multi" })
|
|
88
|
-
: await askQuestion(commandCtx, { question });
|
|
91
|
+
? await askQuestion(commandCtx, { question, context, options: pending.pendingOptions.map((title) => ({ title })), allowMultiple: pending.pendingOptionsMode === "multi" })
|
|
92
|
+
: await askQuestion(commandCtx, { question, context });
|
|
89
93
|
if (!answer) return; // canceled
|
|
90
94
|
await callService("discuss.reply", { id: discussion.id, actor: ACTOR, content: answer.content, ...(answer.selected ? { selected: answer.selected } : {}), source: SOURCE });
|
|
91
95
|
commandCtx.ui.notify(answer.selected ? `Selected: ${answer.selected.join(", ")}` : "Reply added.", "info");
|
|
@@ -39,20 +39,25 @@ function text(message: string, details: unknown = {}) {
|
|
|
39
39
|
* is available, never throws -- an unanswered live prompt still leaves the round it already
|
|
40
40
|
* recorded intact.
|
|
41
41
|
*/
|
|
42
|
-
async function liveAnswer(ctx: ExtensionContext, discussion: Artifact, onUpdate: AgentToolUpdateCallback | undefined, signal: AbortSignal | undefined): Promise<{ content: string; selected?: string[] } | undefined> {
|
|
42
|
+
async function liveAnswer(ctx: ExtensionContext, discussion: Artifact, latestContent: string | undefined, onUpdate: AgentToolUpdateCallback | undefined, signal: AbortSignal | undefined): Promise<{ content: string; selected?: string[] } | undefined> {
|
|
43
43
|
if (!ctx.hasUI) return undefined;
|
|
44
44
|
const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
|
|
45
45
|
const question = `Reply to "${discussion.title}":`;
|
|
46
|
+
// The discussion's title alone is often not the actual question -- a human staring at a bare
|
|
47
|
+
// "Reply to '<title>':" prompt with no visible content has no way to tell what's being asked.
|
|
48
|
+
// The just-recorded round's own content is the real question text; show it as context.
|
|
49
|
+
const context = latestContent?.trim() || undefined;
|
|
46
50
|
if (pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode) {
|
|
47
51
|
return askQuestion(ctx, {
|
|
48
52
|
question,
|
|
53
|
+
context,
|
|
49
54
|
options: pending.pendingOptions.map((title) => ({ title })),
|
|
50
55
|
allowMultiple: pending.pendingOptionsMode === "multi",
|
|
51
56
|
onUpdate,
|
|
52
57
|
signal,
|
|
53
58
|
});
|
|
54
59
|
}
|
|
55
|
-
return askQuestion(ctx, { question, onUpdate, signal });
|
|
60
|
+
return askQuestion(ctx, { question, context, onUpdate, signal });
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/**
|
|
@@ -722,7 +727,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
722
727
|
? text(`Opened discussion ${artifactLine(result.discussion)}`, createArtifactDetails("discuss.open", result.discussion))
|
|
723
728
|
: text(`Round ${result.rounds[0]?.roundNumber} added to "${result.discussion.title}"`, createArtifactDetails("discuss.reply", result.discussion));
|
|
724
729
|
if (params.live !== true) return fallback;
|
|
725
|
-
const answer = await liveAnswer(ctx, result.discussion, onUpdate, signal);
|
|
730
|
+
const answer = await liveAnswer(ctx, result.discussion, result.rounds[0]?.content, onUpdate, signal);
|
|
726
731
|
if (!answer) return fallback;
|
|
727
732
|
const answered = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.reply", {
|
|
728
733
|
id: result.discussion.id, actor: "human", content: answer.content, ...(answer.selected ? { selected: answer.selected } : {}), source: "discuss-live",
|
package/package.json
CHANGED
package/src/ops.ts
CHANGED
|
@@ -529,14 +529,17 @@ export function runGates(db: Db, artifactId: string, options: GateRunOptions = {
|
|
|
529
529
|
}
|
|
530
530
|
}
|
|
531
531
|
case "command": {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
}
|
|
532
|
+
// spawnSync + manual stdout/stderr concatenation, not execSync: execSync's return value is
|
|
533
|
+
// stdout only. Many real commands (bun test's own per-test lines and its pass/fail summary
|
|
534
|
+
// among them) write their actual output to stderr, so an execSync-based match against
|
|
535
|
+
// gate.expect saw only the first line of a banner and never the result -- every such gate
|
|
536
|
+
// failed regardless of whether the command actually passed.
|
|
537
|
+
const { spawnSync } = require_("node:child_process");
|
|
538
|
+
const result = spawnSync(gate.target, { shell: true, encoding: "utf-8", timeout: GATE_COMMAND_TIMEOUT_MS, ...(cwd ? { cwd } : {}) });
|
|
539
|
+
if (result.error) return { gate, passed: false, output: result.error.message.slice(0, GATE_OUTPUT_LIMIT) };
|
|
540
|
+
const combined = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
541
|
+
const passed = result.status === 0 && (gate.expect ? combined.includes(gate.expect) : true);
|
|
542
|
+
return { gate, passed, output: combined.slice(0, GATE_OUTPUT_LIMIT) || (result.status === 0 ? "ok" : `command exited with code ${result.status}`) };
|
|
540
543
|
}
|
|
541
544
|
case "test": {
|
|
542
545
|
const { execSync } = require_("node:child_process");
|
|
@@ -564,7 +567,7 @@ export function runGates(db: Db, artifactId: string, options: GateRunOptions = {
|
|
|
564
567
|
* indefinitely after Papyrus considers the gate "timed out". Spawning detached (its own
|
|
565
568
|
* process group) and killing the negated pid on our own timer reaches the whole tree.
|
|
566
569
|
*/
|
|
567
|
-
function executeGateCommand(command: string, timeout: number, cwd?: string): Promise<{ passed: boolean; output: string }> {
|
|
570
|
+
function executeGateCommand(command: string, timeout: number, cwd?: string): Promise<{ passed: boolean; output: string; matchable: string }> {
|
|
568
571
|
// `spawn(..., { shell: true, detached: true })` instead of the `exec()` convenience wrapper:
|
|
569
572
|
// `detached` (needed to make the shell the leader of its own process group, so the negated pid
|
|
570
573
|
// below reaches every descendant, not just the shell) is not part of Node's `exec()`/
|
|
@@ -587,17 +590,21 @@ function executeGateCommand(command: string, timeout: number, cwd?: string): Pro
|
|
|
587
590
|
child.stdout?.on("data", append);
|
|
588
591
|
child.stderr?.on("data", append);
|
|
589
592
|
|
|
590
|
-
const finish = (result: { passed: boolean; output: string }): void => {
|
|
593
|
+
const finish = (result: { passed: boolean; output: string; matchable: string }): void => {
|
|
591
594
|
if (settled) return;
|
|
592
595
|
settled = true;
|
|
593
596
|
clearTimeout(timer);
|
|
594
597
|
resolve(result);
|
|
595
598
|
};
|
|
596
599
|
|
|
597
|
-
child.on("error", (error) => finish({ passed: false, output: error.message.slice(0, GATE_OUTPUT_LIMIT) }));
|
|
600
|
+
child.on("error", (error) => finish({ passed: false, output: error.message.slice(0, GATE_OUTPUT_LIMIT), matchable: error.message }));
|
|
598
601
|
child.on("close", (code) => {
|
|
599
|
-
|
|
600
|
-
|
|
602
|
+
// `matchable` carries the full (GATE_MAX_BUFFER_BYTES-bounded) buffer so the caller's
|
|
603
|
+
// gate.expect substring check sees the whole run, not just the first GATE_OUTPUT_LIMIT
|
|
604
|
+
// characters -- a real bun test run's pass/fail summary is its last line, not its first.
|
|
605
|
+
const full = buffered.trim();
|
|
606
|
+
const output = full.slice(0, GATE_OUTPUT_LIMIT);
|
|
607
|
+
finish({ passed: code === 0, output: output || (code === 0 ? "ok" : `command exited with code ${code}`), matchable: full });
|
|
601
608
|
});
|
|
602
609
|
|
|
603
610
|
const timer = setTimeout(() => {
|
|
@@ -609,7 +616,7 @@ function executeGateCommand(command: string, timeout: number, cwd?: string): Pro
|
|
|
609
616
|
child.kill("SIGKILL");
|
|
610
617
|
}
|
|
611
618
|
}
|
|
612
|
-
finish({ passed: false, output: `gate command timed out after ${timeout}ms
|
|
619
|
+
finish({ passed: false, output: `gate command timed out after ${timeout}ms`, matchable: "" });
|
|
613
620
|
}, timeout);
|
|
614
621
|
});
|
|
615
622
|
}
|
|
@@ -651,7 +658,7 @@ export async function runGatesAsync(db: Db, artifactId: string, options: GateRun
|
|
|
651
658
|
const executed = await executeGateCommand(command, timeout, options.cwd);
|
|
652
659
|
results.push({
|
|
653
660
|
gate,
|
|
654
|
-
passed: executed.passed && (gate.expect ? executed.
|
|
661
|
+
passed: executed.passed && (gate.expect ? executed.matchable.includes(gate.expect) : true),
|
|
655
662
|
output: executed.output,
|
|
656
663
|
});
|
|
657
664
|
} else {
|