@danypops/papyrus 0.25.0 → 0.27.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/extension/src/active-task-continuation.ts +1 -1
- package/extension/src/artifact-browser.ts +4 -4
- package/extension/src/discuss-ask-layout.ts +193 -0
- package/extension/src/discuss-ask-view.ts +1125 -0
- package/extension/src/discuss.ts +8 -20
- package/extension/src/docs.ts +2 -2
- package/extension/src/domain-tools.ts +59 -38
- package/extension/src/index.ts +37 -9
- package/extension/src/playbooks.ts +2 -2
- package/extension/src/rules.ts +1 -1
- package/extension/src/skills.ts +4 -4
- package/extension/src/tasks.ts +20 -9
- package/extension/src/tool-rendering/artifact-card.ts +1 -1
- package/package.json +1 -1
- package/src/constants.ts +0 -3
- package/src/domain-services.ts +9 -9
- package/src/task-context.ts +4 -4
- package/extension/src/discussion-picker.ts +0 -148
package/extension/src/discuss.ts
CHANGED
|
@@ -16,10 +16,10 @@ import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-a
|
|
|
16
16
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
17
17
|
import type { DiscussionAndRounds } from "../../src/discussion-service.ts";
|
|
18
18
|
import { readDiscussionExtra } from "../../src/domain/discussion.ts";
|
|
19
|
+
import { askQuestion } from "./discuss-ask-view.ts";
|
|
19
20
|
import { showArtifactBrowser } from "./artifact-browser.ts";
|
|
20
21
|
import { DISCUSSION_STATE_PRESENTATION, DOC_STATUS_PRESENTATION } from "./artifact-status-presentation.ts";
|
|
21
22
|
import { discussionRoundCountOf, discussionStateOf, showDiscussionDetailView } from "./discussion-detail-view.ts";
|
|
22
|
-
import { pickDiscussionOptions } from "./discussion-picker.ts";
|
|
23
23
|
import { callService } from "./service-client.ts";
|
|
24
24
|
|
|
25
25
|
const SOURCE = "discuss-tui";
|
|
@@ -82,25 +82,13 @@ export async function showDiscussions(ctx: ExtensionCommandContext): Promise<voi
|
|
|
82
82
|
}
|
|
83
83
|
if (choice === "Reply") {
|
|
84
84
|
const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
const { selected } = result;
|
|
94
|
-
const elaboration = await commandCtx.ui.input("Elaborate (optional):", selected.join(", "));
|
|
95
|
-
if (elaboration === undefined) return; // canceled
|
|
96
|
-
await callService("discuss.reply", { id: discussion.id, actor: ACTOR, content: elaboration || selected.join(", "), selected, source: SOURCE });
|
|
97
|
-
commandCtx.ui.notify(`Selected: ${selected.join(", ")}`, "info");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const content = await commandCtx.ui.input("Reply:", "");
|
|
101
|
-
if (!content) return;
|
|
102
|
-
await callService("discuss.reply", { id: discussion.id, actor: ACTOR, content, source: SOURCE });
|
|
103
|
-
commandCtx.ui.notify("Round added.", "info");
|
|
85
|
+
const question = `Reply to "${discussion.title}":`;
|
|
86
|
+
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 });
|
|
89
|
+
if (!answer) return; // canceled
|
|
90
|
+
await callService("discuss.reply", { id: discussion.id, actor: ACTOR, content: answer.content, ...(answer.selected ? { selected: answer.selected } : {}), source: SOURCE });
|
|
91
|
+
commandCtx.ui.notify(answer.selected ? `Selected: ${answer.selected.join(", ")}` : "Reply added.", "info");
|
|
104
92
|
return;
|
|
105
93
|
}
|
|
106
94
|
if (choice === "Defer") {
|
package/extension/src/docs.ts
CHANGED
|
@@ -45,13 +45,13 @@ export async function showDocs(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
45
45
|
const relation = await commandCtx.ui.select("Relation", DOC_RELATIONS);
|
|
46
46
|
if (!relation) return;
|
|
47
47
|
await callService("docs.link", { id: document.id, relation, target_id: targetId });
|
|
48
|
-
commandCtx.ui.notify(`Linked ${document.
|
|
48
|
+
commandCtx.ui.notify(`Linked "${document.title}" via ${relation}`, "info");
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
const operation = choice === "Activate" ? "docs.activate" : choice === "Archive" ? "docs.archive" : choice === "Reopen" ? "docs.reopen" : undefined;
|
|
52
52
|
if (operation) {
|
|
53
53
|
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: document.id });
|
|
54
|
-
commandCtx.ui.notify(`${updated.
|
|
54
|
+
commandCtx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
});
|
|
@@ -9,7 +9,7 @@ import type { TaskCompletion, TaskGraph } from "../../src/task-service.ts";
|
|
|
9
9
|
import type { SkillWorkflowRunResult } from "../../src/skill-execution.ts";
|
|
10
10
|
import type { DiscussionAndRounds } from "../../src/discussion-service.ts";
|
|
11
11
|
import { readDiscussionExtra, type DiscussionRound } from "../../src/domain/discussion.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { askQuestion } from "./discuss-ask-view.ts";
|
|
13
13
|
import type { OperationName } from "../../src/service.ts";
|
|
14
14
|
import { emitTaskFocusEvent } from "./task-focus-events.ts";
|
|
15
15
|
import { sessionSecretField } from "./session-identity.ts";
|
|
@@ -32,23 +32,25 @@ function text(message: string, details: unknown = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* live:true's synchronous half:
|
|
36
|
-
* the just-created round posed a structured choice,
|
|
37
|
-
* "ask" covers both a completely open question and a
|
|
38
|
-
* Returns undefined on cancel or when no interactive UI
|
|
39
|
-
* unanswered live prompt still leaves the round it already
|
|
35
|
+
* live:true's synchronous half: reuses the same Discuss-owned ask UI (discuss-ask-view.ts) the
|
|
36
|
+
* /discuss TUI's own "Reply" action uses when the just-created round posed a structured choice,
|
|
37
|
+
* or a plain freeform prompt otherwise -- so "ask" covers both a completely open question and a
|
|
38
|
+
* choice tied to this specific Discussion. Returns undefined on cancel or when no interactive UI
|
|
39
|
+
* is available, never throws -- an unanswered live prompt still leaves the round it already
|
|
40
|
+
* recorded intact.
|
|
40
41
|
*/
|
|
41
42
|
async function liveAnswer(ctx: ExtensionContext, discussion: Artifact): Promise<{ content: string; selected?: string[] } | undefined> {
|
|
42
43
|
if (!ctx.hasUI) return undefined;
|
|
43
44
|
const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
|
|
45
|
+
const question = `Reply to "${discussion.title}":`;
|
|
44
46
|
if (pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
return askQuestion(ctx, {
|
|
48
|
+
question,
|
|
49
|
+
options: pending.pendingOptions.map((title) => ({ title })),
|
|
50
|
+
allowMultiple: pending.pendingOptionsMode === "multi",
|
|
51
|
+
});
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
return content ? { content } : undefined;
|
|
53
|
+
return askQuestion(ctx, { question });
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -69,6 +71,15 @@ export function artifactLines(artifacts: Artifact[]): string[] {
|
|
|
69
71
|
return artifacts.map((artifact) => (titleCounts.get(artifact.title)! > 1 ? `${artifactLine(artifact)} (${artifact.id})` : artifactLine(artifact)));
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/** Resolves internal ids for model text; ids resurface only when equal titles need disambiguation. */
|
|
75
|
+
async function artifactLabelsById(ids: readonly string[]): Promise<Map<string, string>> {
|
|
76
|
+
const uniqueIds = [...new Set(ids)];
|
|
77
|
+
const artifacts = (await Promise.all(uniqueIds.map((id) => callService<Record<string, unknown>, Artifact | null>("artifact.show", { id })))).filter((artifact): artifact is Artifact => artifact !== null);
|
|
78
|
+
const titleCounts = new Map<string, number>();
|
|
79
|
+
for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
|
|
80
|
+
return new Map(artifacts.map((artifact) => [artifact.id, titleCounts.get(artifact.title)! > 1 ? `${artifact.title} (${artifact.id})` : artifact.title]));
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
/**
|
|
73
84
|
* Exact, case-insensitive, trimmed title match against an already-fetched candidate set. Throws
|
|
74
85
|
* a clear "not found" or "ambiguous -- use id" error rather than guessing at a fuzzy match -- id
|
|
@@ -132,15 +143,15 @@ async function resolveNameArrayField(
|
|
|
132
143
|
* Returns null when action is neither, so callers fall through to their own dispatch.
|
|
133
144
|
*/
|
|
134
145
|
async function handleArtifactRemoveRestore(action: unknown, params: Record<string, unknown>): Promise<ReturnType<typeof text> | null> {
|
|
135
|
-
// Trashed/restored
|
|
136
|
-
//
|
|
137
|
-
//
|
|
146
|
+
// Trashed/restored artifacts stay directly showable, so known identities render by title on
|
|
147
|
+
// either side of the action. An unresolved explicit id stays in structured/error channels;
|
|
148
|
+
// normal model text does not turn that backend key into the artifact's public name.
|
|
138
149
|
const titleOf = async (): Promise<string> => {
|
|
139
150
|
try {
|
|
140
151
|
const artifact = await callService<Record<string, unknown>, Artifact | null>("artifact.show", { id: params["id"] });
|
|
141
|
-
return artifact ? `"${artifact.title}"` :
|
|
152
|
+
return artifact ? `"${artifact.title}"` : "unknown artifact";
|
|
142
153
|
} catch {
|
|
143
|
-
return
|
|
154
|
+
return "unknown artifact";
|
|
144
155
|
}
|
|
145
156
|
};
|
|
146
157
|
if (action === "remove") {
|
|
@@ -218,17 +229,26 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
218
229
|
// ever holds this extension's own registered session anyway (see session-identity.ts).
|
|
219
230
|
const resolvedSessionId = params.session_id ?? ctx.sessionManager.getSessionId();
|
|
220
231
|
const baseRequest = { project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "pi-tool", session_id: resolvedSessionId, ...sessionSecretField(resolvedSessionId as string) };
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
232
|
+
// Resolve the graph root first: every other name lookup must use the caller's final
|
|
233
|
+
// project/scope/root selection, otherwise `scope: all|graph` silently collapses back
|
|
234
|
+
// to the current project and forces callers to reach for an id.
|
|
235
|
+
await resolveNameFields(params, [
|
|
236
|
+
{ nameKey: "root_task_name", idKey: "root_task_id", listOperation: "tasks.list", baseRequest: { ...baseRequest, scope: "project" } },
|
|
237
|
+
]);
|
|
238
|
+
const resolutionRequest = {
|
|
239
|
+
...baseRequest,
|
|
240
|
+
...(params.scope === undefined ? {} : { scope: params.scope }),
|
|
241
|
+
...(params.root_task_id === undefined ? {} : { root_task_id: params.root_task_id }),
|
|
242
|
+
};
|
|
243
|
+
// The daemon remains keyed by stable ids; the agent facade resolves names against the
|
|
244
|
+
// exact requested view before dispatching those internal ids.
|
|
224
245
|
await resolveNameFields(params, [
|
|
225
|
-
{ nameKey: "name", idKey: "id", listOperation: "tasks.list", baseRequest },
|
|
226
|
-
{ nameKey: "dependency_name", idKey: "dependency_id", listOperation: "tasks.list", baseRequest },
|
|
227
|
-
{ nameKey: "parent_name", idKey: "parent_id", listOperation: "tasks.list", baseRequest },
|
|
228
|
-
{ nameKey: "child_name", idKey: "child_id", listOperation: "tasks.list", baseRequest },
|
|
229
|
-
{ nameKey: "root_task_name", idKey: "root_task_id", listOperation: "tasks.list", baseRequest },
|
|
246
|
+
{ nameKey: "name", idKey: "id", listOperation: "tasks.list", baseRequest: resolutionRequest },
|
|
247
|
+
{ nameKey: "dependency_name", idKey: "dependency_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
|
|
248
|
+
{ nameKey: "parent_name", idKey: "parent_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
|
|
249
|
+
{ nameKey: "child_name", idKey: "child_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
|
|
230
250
|
]);
|
|
231
|
-
await resolveNameArrayField(params, "depends_on_names", "depends_on", "tasks.list",
|
|
251
|
+
await resolveNameArrayField(params, "depends_on_names", "depends_on", "tasks.list", resolutionRequest);
|
|
232
252
|
const request = { ...params, ...baseRequest };
|
|
233
253
|
if (action === "create") {
|
|
234
254
|
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create", request);
|
|
@@ -294,17 +314,19 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
294
314
|
const byId = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
295
315
|
const titleCounts = new Map<string, number>();
|
|
296
316
|
for (const node of plan.nodes) titleCounts.set(node.title, (titleCounts.get(node.title) ?? 0) + 1);
|
|
317
|
+
const nodeLabel = (id: string): string => {
|
|
318
|
+
const node = byId.get(id);
|
|
319
|
+
if (!node) return "unknown task";
|
|
320
|
+
return (titleCounts.get(node.title) ?? 0) > 1 ? `${node.title} (${node.id})` : node.title;
|
|
321
|
+
};
|
|
297
322
|
const lines = plan.layers.flatMap((layer, index) => [
|
|
298
323
|
`Layer ${index + 1}`,
|
|
299
324
|
...layer.map((id) => {
|
|
300
325
|
const node = byId.get(id);
|
|
301
|
-
|
|
302
|
-
return (titleCounts.get(node.title) ?? 0) > 1
|
|
303
|
-
? ` [${node.state}] ${node.title} (${node.id})`
|
|
304
|
-
: ` [${node.state}] ${node.title}`;
|
|
326
|
+
return ` [${node?.state ?? "unknown"}] ${nodeLabel(id)}`;
|
|
305
327
|
}),
|
|
306
328
|
]);
|
|
307
|
-
if (plan.cycleIds.length > 0) lines.push(`Invalid cycle: ${plan.cycleIds.join(", ")}`);
|
|
329
|
+
if (plan.cycleIds.length > 0) lines.push(`Invalid cycle: ${plan.cycleIds.map(nodeLabel).join(", ")}`);
|
|
308
330
|
const output = lines.join("\n") || "No tasks in execution plan.";
|
|
309
331
|
return text(output, createPreviewDetails("tasks.plan", "Task execution plan", output));
|
|
310
332
|
}
|
|
@@ -318,8 +340,9 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
318
340
|
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}${item.reason ? ` — ${item.reason}` : ""}`).join("\n");
|
|
319
341
|
const focused = result.focused ? `\nActive: ${artifactLine(result.focused)}` : "";
|
|
320
342
|
const blockedLines = artifactLines(result.blocked.map((entry) => entry.artifact));
|
|
343
|
+
const dependencyLabels = await artifactLabelsById(result.blocked.flatMap((entry) => entry.dependencyIds));
|
|
321
344
|
const blocked = result.blocked.length > 0
|
|
322
|
-
? `\nBlocked: ${result.blocked.map((entry, index) => `${blockedLines[index]} waits for ${entry.dependencyIds.join(", ")}`).join("; ")}`
|
|
345
|
+
? `\nBlocked: ${result.blocked.map((entry, index) => `${blockedLines[index]} waits for ${entry.dependencyIds.map((id) => dependencyLabels.get(id) ?? "unknown task").join(", ")}`).join("; ")}`
|
|
323
346
|
: "";
|
|
324
347
|
const output = `${result.completed ? "Completed" : "Rejected"}: ${artifactLine(result.artifact)}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`;
|
|
325
348
|
return text(output, createPreviewDetails("tasks.complete", "Task completion", output));
|
|
@@ -618,16 +641,14 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
618
641
|
const execution = run.execution.nodes.map((node) => (runTitleCounts.get(node.title) ?? 0) > 1
|
|
619
642
|
? ` [${node.state}] ${node.title} (${node.id})`
|
|
620
643
|
: ` [${node.state}] ${node.title}`).join("\n");
|
|
621
|
-
// Root task titles are free here (already present in execution.nodes); created docs/rules
|
|
622
|
-
// are a different kind not covered by this run's own execution nodes, so those still list by
|
|
623
|
-
// id below -- fetching their titles would mean an extra round-trip per artifact.
|
|
624
644
|
const nodeById = new Map(run.execution.nodes.map((node) => [node.id, node]));
|
|
625
|
-
const rootLabels = run.rootTaskIds.map((id) => nodeById.get(id)?.title ??
|
|
645
|
+
const rootLabels = run.rootTaskIds.map((id) => nodeById.get(id)?.title ?? "unknown task");
|
|
646
|
+
const createdLabels = await artifactLabelsById([...run.created.docs, ...run.created.rules]);
|
|
626
647
|
return text([
|
|
627
648
|
`Created Skill run ${run.runId}: ${run.created.tasks.length} tasks, ${run.created.rules.length} rules, ${run.created.docs.length} docs.`,
|
|
628
649
|
`Ready roots: ${rootLabels.join(", ") || "none"}.`,
|
|
629
|
-
`Context docs: ${run.created.docs.join(", ") || "none"}.`,
|
|
630
|
-
`Scoped rules: ${run.created.rules.join(", ") || "none"}.`,
|
|
650
|
+
`Context docs: ${run.created.docs.map((id) => createdLabels.get(id) ?? "unknown document").join(", ") || "none"}.`,
|
|
651
|
+
`Scoped rules: ${run.created.rules.map((id) => createdLabels.get(id) ?? "unknown rule").join(", ") || "none"}.`,
|
|
631
652
|
...(execution ? ["Execution:", execution] : []),
|
|
632
653
|
].join("\n"), createInvocationDetails("skills.run", run.runId, {
|
|
633
654
|
tasks: run.created.tasks,
|
package/extension/src/index.ts
CHANGED
|
@@ -47,6 +47,27 @@ function text(value: string, details: unknown = {}) {
|
|
|
47
47
|
return { content: [{ type: "text" as const, text: modelContent.text }], details };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function artifactTextLabel(artifact: Artifact): string {
|
|
51
|
+
return `[${artifact.kind}|${artifact.status}] ${artifact.title}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function artifactTextLines(artifacts: readonly Artifact[]): string[] {
|
|
55
|
+
const titleCounts = new Map<string, number>();
|
|
56
|
+
for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
|
|
57
|
+
return artifacts.map((artifact) => titleCounts.get(artifact.title)! > 1
|
|
58
|
+
? `${artifactTextLabel(artifact)} (${artifact.id})`
|
|
59
|
+
: artifactTextLabel(artifact));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resolves graph protocol ids into model-facing names; equal titles retain ids only to disambiguate. */
|
|
63
|
+
async function artifactNamesById(ids: readonly string[]): Promise<Map<string, string>> {
|
|
64
|
+
const uniqueIds = [...new Set(ids)];
|
|
65
|
+
const artifacts = (await Promise.all(uniqueIds.map((id) => callService<Record<string, unknown>, Artifact | null>("artifact.show", { id })))).filter((artifact): artifact is Artifact => artifact !== null);
|
|
66
|
+
const titleCounts = new Map<string, number>();
|
|
67
|
+
for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
|
|
68
|
+
return new Map(artifacts.map((artifact) => [artifact.id, titleCounts.get(artifact.title)! > 1 ? `${artifact.title} (${artifact.id})` : artifact.title]));
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
// ---------------------------------------------------------------------------
|
|
51
72
|
// Task widget (TodoOverlay pattern from rpiv-todo: factory form, requestRender)
|
|
52
73
|
// ---------------------------------------------------------------------------
|
|
@@ -272,7 +293,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
272
293
|
...params,
|
|
273
294
|
...(params.kind === "task" ? { project_root: params.project_root ?? ctx.cwd } : {}),
|
|
274
295
|
});
|
|
275
|
-
return text(`Created ${a
|
|
296
|
+
return text(`Created ${artifactTextLabel(a)}`, createArtifactDetails("artifact.create", a));
|
|
276
297
|
} catch (e) {
|
|
277
298
|
throw new Error(`papyrus_create failed: ${e instanceof Error ? e.message : e}`);
|
|
278
299
|
}
|
|
@@ -295,7 +316,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
295
316
|
try {
|
|
296
317
|
const rows = await callService<Record<string, unknown>, Artifact[]>("artifact.query", { ...params, limit: params.limit ?? 50 });
|
|
297
318
|
if (rows.length === 0) return text("No artifacts found.", createArtifactListDetails("artifact.query", rows));
|
|
298
|
-
const lines = rows.map((
|
|
319
|
+
const lines = artifactTextLines(rows).map((line, index) => `${index + 1}. ${line}`);
|
|
299
320
|
return text(`${rows.length} artifact(s):\n\n${lines.join("\n")}`, createArtifactListDetails("artifact.query", rows));
|
|
300
321
|
} catch (e) {
|
|
301
322
|
throw new Error(`papyrus_query failed: ${e instanceof Error ? e.message : e}`);
|
|
@@ -332,12 +353,15 @@ export default async function (pi: ExtensionAPI) {
|
|
|
332
353
|
try {
|
|
333
354
|
if (params.action === "link") {
|
|
334
355
|
await callService("graph.link", { from: params.from!, relation: params.relation!, to: params.to! });
|
|
335
|
-
const
|
|
356
|
+
const names = await artifactNamesById([params.from!, params.to!]);
|
|
357
|
+
const output = `Linked "${names.get(params.from!) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to!) ?? "unknown artifact"}"`;
|
|
336
358
|
return text(output, createPreviewDetails("graph.link", "Artifact relationship", output));
|
|
337
359
|
}
|
|
338
360
|
if (params.action === "unlink") {
|
|
339
361
|
const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from: params.from!, relation: params.relation!, to: params.to! });
|
|
340
|
-
const
|
|
362
|
+
const names = await artifactNamesById([params.from!, params.to!]);
|
|
363
|
+
const relationship = `"${names.get(params.from!) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to!) ?? "unknown artifact"}"`;
|
|
364
|
+
const output = result.removed ? `Unlinked ${relationship}` : `No such relationship: ${relationship}`;
|
|
341
365
|
return text(output, createPreviewDetails("graph.unlink", "Artifact relationship", output));
|
|
342
366
|
}
|
|
343
367
|
if (params.action === "tree") {
|
|
@@ -351,22 +375,25 @@ export default async function (pi: ExtensionAPI) {
|
|
|
351
375
|
if (!a) throw new Error(`artifact ${root} not found`);
|
|
352
376
|
const edges = a.edges ?? [];
|
|
353
377
|
if (edges.length === 0) return text(`${a.title} — no edges`, createGraphDetails("graph.tree", [a], []));
|
|
378
|
+
const names = await artifactNamesById(edges.flatMap((edge) => [edge.from, edge.to]));
|
|
354
379
|
return text(
|
|
355
|
-
`Subgraph from ${a.title} (${edges.length} edges):\n\n${edges.map((edge
|
|
380
|
+
`Subgraph from ${a.title} (${edges.length} edges):\n\n${edges.map((edge) => ` "${names.get(edge.from) ?? "unknown artifact"}" --${edge.relation}--> "${names.get(edge.to) ?? "unknown artifact"}"`).join("\n")}`,
|
|
356
381
|
createGraphDetails("graph.tree", [a], edges),
|
|
357
382
|
);
|
|
358
383
|
}
|
|
359
384
|
if (params.action === "status") {
|
|
360
385
|
const a = await callService<Record<string, unknown>, Artifact | null>("graph.status", { id: params.id!, status: params.status! });
|
|
361
386
|
if (!a) throw new Error(`artifact ${params.id} not found`);
|
|
362
|
-
return text(`Updated ${a.
|
|
387
|
+
return text(`Updated "${a.title}" → [${a.status}]`, createArtifactDetails("graph.status", a));
|
|
363
388
|
}
|
|
364
389
|
if (params.action === "history") {
|
|
365
390
|
const page = await callService<Record<string, unknown>, { events: Array<Record<string, unknown>> }>("graph.history", {
|
|
366
391
|
id: params.id, actor: params.actor, session_id: params.session_id, since: params.since, limit: params.limit,
|
|
367
392
|
});
|
|
368
393
|
if (page.events.length === 0) return text("No recorded events.", createPreviewDetails("graph.history", "Mutation event log", "No recorded events."));
|
|
369
|
-
const
|
|
394
|
+
const eventIds = page.events.map((event) => event["artifactId"]).filter((id): id is string => typeof id === "string");
|
|
395
|
+
const names = await artifactNamesById(eventIds);
|
|
396
|
+
const output = page.events.map((event) => `${event["occurredAt"]} "${typeof event["artifactId"] === "string" ? names.get(event["artifactId"]) ?? "unknown artifact" : "unknown artifact"}" ${event["type"]} · ${event["actor"]}/${event["source"]}`).join("\n");
|
|
370
397
|
return text(output, createPreviewDetails("graph.history", "Mutation event log", output));
|
|
371
398
|
}
|
|
372
399
|
throw new Error(`unknown action: ${params.action}; use link, tree, status, or history`);
|
|
@@ -397,12 +424,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
397
424
|
max_nodes: params.max_nodes,
|
|
398
425
|
});
|
|
399
426
|
if (!a) throw new Error(`artifact ${params.id} not found`);
|
|
400
|
-
let out = `${a
|
|
427
|
+
let out = `${artifactTextLabel(a)}\n\n${a.body}`;
|
|
401
428
|
if (Object.keys(a.extra).length > 0) {
|
|
402
429
|
out += `\n\nMetadata:\n${formatMetadata(a.extra).map((line) => ` ${line}`).join("\n")}`;
|
|
403
430
|
}
|
|
404
431
|
if (a.edges?.length) {
|
|
405
|
-
|
|
432
|
+
const names = await artifactNamesById(a.edges.flatMap((edge) => [edge.from, edge.to]));
|
|
433
|
+
out += `\n\nEdges:\n${a.edges.map((edge) => ` "${names.get(edge.from) ?? "unknown artifact"}" --${edge.relation}--> "${names.get(edge.to) ?? "unknown artifact"}"`).join("\n")}`;
|
|
406
434
|
}
|
|
407
435
|
if (params.run_gates) {
|
|
408
436
|
const results = await callService<Record<string, unknown>, GateResult[]>("gates.run", { id: params.id });
|
|
@@ -86,12 +86,12 @@ export async function showPlaybooks(ctx: ExtensionCommandContext): Promise<void>
|
|
|
86
86
|
const relation = await commandCtx.ui.select("Relation", PLAYBOOK_RELATIONS);
|
|
87
87
|
if (!relation) return;
|
|
88
88
|
await callService("graph.link", { from: playbook.id, relation, to: targetId });
|
|
89
|
-
commandCtx.ui.notify(`Linked ${playbook.
|
|
89
|
+
commandCtx.ui.notify(`Linked "${playbook.title}" via ${relation}`, "info");
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
const operation = choice === "Disable" ? "playbooks.disable" : "playbooks.enable";
|
|
93
93
|
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: playbook.id });
|
|
94
|
-
commandCtx.ui.notify(`${updated.
|
|
94
|
+
commandCtx.ui.notify(`${updated.title} \u2192 [${updated.status}]`, "info");
|
|
95
95
|
},
|
|
96
96
|
});
|
|
97
97
|
}
|
package/extension/src/rules.ts
CHANGED
|
@@ -44,7 +44,7 @@ export async function showRules(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
44
44
|
} else {
|
|
45
45
|
const operation = choice === "Disable" ? "rules.disable" : "rules.enable";
|
|
46
46
|
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: rule.id });
|
|
47
|
-
commandCtx.ui.notify(`${updated.
|
|
47
|
+
commandCtx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
});
|
package/extension/src/skills.ts
CHANGED
|
@@ -32,11 +32,11 @@ export function skillRowMeta(skill: Artifact): string {
|
|
|
32
32
|
|
|
33
33
|
export function skillInvocationPrompt(skill: Artifact): string {
|
|
34
34
|
if (skill.subtype === "artifact-template") {
|
|
35
|
-
return [`Create an artifact using Papyrus template \"${skill.title}\".`, `
|
|
35
|
+
return [`Create an artifact using Papyrus template \"${skill.title}\".`, `template_name: ${skill.title}`, "Ask for or infer the title and all required template fields, then call the skills domain tool with action=instantiate."].join("\n");
|
|
36
36
|
}
|
|
37
37
|
if (skill.subtype === "workflow") {
|
|
38
38
|
return [
|
|
39
|
-
`Run Papyrus workflow Skill \"${skill.title}\"
|
|
39
|
+
`Run Papyrus workflow Skill \"${skill.title}\".`,
|
|
40
40
|
"Collect its required arguments, then call the skills domain tool with action=run.",
|
|
41
41
|
].join("\n");
|
|
42
42
|
}
|
|
@@ -44,7 +44,7 @@ export function skillInvocationPrompt(skill: Artifact): string {
|
|
|
44
44
|
const steps = strings(skill.extra["steps"]);
|
|
45
45
|
const tools = strings(skill.extra["tools"]);
|
|
46
46
|
return [
|
|
47
|
-
`Apply Papyrus skill \"${skill.title}\"
|
|
47
|
+
`Apply Papyrus skill \"${skill.title}\".`,
|
|
48
48
|
`Trigger: ${trigger}`,
|
|
49
49
|
...(skill.body ? [`Context: ${skill.body}`] : []),
|
|
50
50
|
...(steps.length > 0 ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
|
|
@@ -120,7 +120,7 @@ export async function showSkills(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
120
120
|
} else {
|
|
121
121
|
const operation = choice === "Disable" ? "skills.disable" : "skills.enable";
|
|
122
122
|
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: skill.id });
|
|
123
|
-
commandCtx.ui.notify(`${updated.
|
|
123
|
+
commandCtx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
126
|
});
|
package/extension/src/tasks.ts
CHANGED
|
@@ -32,6 +32,12 @@ const STATUS_ACTIONS: Record<string, string[]> = {
|
|
|
32
32
|
|
|
33
33
|
type TaskRow = Artifact;
|
|
34
34
|
|
|
35
|
+
function taskChoiceLabels(tasks: readonly Artifact[]): string[] {
|
|
36
|
+
const titleCounts = new Map<string, number>();
|
|
37
|
+
for (const task of tasks) titleCounts.set(task.title, (titleCounts.get(task.title) ?? 0) + 1);
|
|
38
|
+
return tasks.map((task) => titleCounts.get(task.title)! > 1 ? `${task.title} (${task.id})` : task.title);
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
export interface TaskHierarchyRow {
|
|
36
42
|
task: TaskRow;
|
|
37
43
|
depth: number;
|
|
@@ -101,9 +107,10 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
101
107
|
if (scope === "graph") {
|
|
102
108
|
const projectGraph = await loadTaskGraph(ctx.cwd, sessionId, "project");
|
|
103
109
|
const roots = projectGraph.rootIds.map((id) => projectGraph.nodes.find((node) => node.task.id === id)?.task).filter((task): task is Artifact => task !== undefined);
|
|
104
|
-
const
|
|
110
|
+
const rootLabels = taskChoiceLabels(roots);
|
|
111
|
+
const selected = await ctx.ui.select("Focused root or epic", rootLabels);
|
|
105
112
|
if (!selected) continue;
|
|
106
|
-
rootTaskId = roots.
|
|
113
|
+
rootTaskId = roots[rootLabels.indexOf(selected)]?.id;
|
|
107
114
|
if (!rootTaskId) continue;
|
|
108
115
|
}
|
|
109
116
|
await callService("tasks.set_scope", { project_root: ctx.cwd, scope, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) });
|
|
@@ -131,17 +138,20 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
131
138
|
|
|
132
139
|
if (choice === "Remove dependency" || choice === "Remove from parent") {
|
|
133
140
|
const relatedIds = choice === "Remove dependency" ? node!.dependencyIds : node!.parentIds;
|
|
134
|
-
const
|
|
141
|
+
const relatedTasks = relatedIds.map((relatedId) => graph.nodes.find((entry) => entry.task.id === relatedId)?.task).filter((task): task is Artifact => task !== undefined);
|
|
142
|
+
const relatedTitles = taskChoiceLabels(relatedTasks);
|
|
135
143
|
const selected = await ctx.ui.select(choice === "Remove dependency" ? "Remove which dependency?" : "Remove from which parent?", relatedTitles);
|
|
136
144
|
if (!selected) continue;
|
|
137
|
-
const
|
|
145
|
+
const relatedTask = relatedTasks[relatedTitles.indexOf(selected)];
|
|
146
|
+
if (!relatedTask) continue;
|
|
147
|
+
const relatedId = relatedTask.id;
|
|
138
148
|
try {
|
|
139
149
|
if (choice === "Remove dependency") {
|
|
140
150
|
await callService("tasks.undepend", { id: action.row.id, dependency_id: relatedId, actor: "user", source: "tasks-tui", session_id: sessionId });
|
|
141
|
-
ctx.ui.notify(`Removed dependency on ${
|
|
151
|
+
ctx.ui.notify(`Removed dependency on ${relatedTask.title}`, "info");
|
|
142
152
|
} else {
|
|
143
153
|
await callService("tasks.uncontain", { parent_id: relatedId, child_id: action.row.id, actor: "user", source: "tasks-tui", session_id: sessionId });
|
|
144
|
-
ctx.ui.notify(`Removed from parent ${
|
|
154
|
+
ctx.ui.notify(`Removed from parent ${relatedTask.title}`, "info");
|
|
145
155
|
}
|
|
146
156
|
} catch (error) {
|
|
147
157
|
ctx.ui.notify(`Relationship removal failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
@@ -222,19 +232,20 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
222
232
|
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target}`).join("\n");
|
|
223
233
|
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}`).join("\n");
|
|
224
234
|
const focused = result.focused ? `\nActive: ${result.focused.title}` : "";
|
|
235
|
+
const taskById = new Map(graph.nodes.map((entry) => [entry.task.id, entry.task]));
|
|
225
236
|
const blocked = result.blocked.length > 0
|
|
226
|
-
? `\nWaiting: ${result.blocked.map((entry) => `${entry.artifact.title} needs ${entry.dependencyIds.join(", ")}`).join("; ")}`
|
|
237
|
+
? `\nWaiting: ${result.blocked.map((entry) => `${entry.artifact.title} needs ${entry.dependencyIds.map((id) => taskById.get(id)?.title ?? "unknown task").join(", ")}`).join("; ")}`
|
|
227
238
|
: "";
|
|
228
239
|
ctx.ui.notify(
|
|
229
240
|
result.completed
|
|
230
|
-
? `Completed ${result.artifact.
|
|
241
|
+
? `Completed ${result.artifact.title}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`
|
|
231
242
|
: `Review rejected${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`,
|
|
232
243
|
result.completed ? "info" : "warning",
|
|
233
244
|
);
|
|
234
245
|
} else {
|
|
235
246
|
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: action.row.id, actor: "user", source: "tasks-tui", session_id: sessionId });
|
|
236
247
|
action.row.status = updated.status;
|
|
237
|
-
ctx.ui.notify(`${updated.
|
|
248
|
+
ctx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
|
|
238
249
|
}
|
|
239
250
|
} catch (error) {
|
|
240
251
|
ctx.ui.notify(`Task action failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
@@ -85,7 +85,7 @@ export class ArtifactCard implements Component {
|
|
|
85
85
|
const status = `${statusGlyph(artifact.status)} ${artifact.status}`;
|
|
86
86
|
const header = [
|
|
87
87
|
this.theme.fg("toolTitle", this.theme.bold(`${kindGlyph(artifact.kind)} ${artifact.kind.toUpperCase()}`)),
|
|
88
|
-
this.theme.fg("accent", artifact.id),
|
|
88
|
+
...(this.expanded ? [this.theme.fg("accent", artifact.id)] : []),
|
|
89
89
|
this.theme.fg(statusColor(artifact.status), status),
|
|
90
90
|
].join(" ");
|
|
91
91
|
const lines = [truncateToWidth(header, safeWidth)];
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -175,9 +175,6 @@ export const DISCUSSION_ACTOR_MAX_LENGTH = 128;
|
|
|
175
175
|
export const DISCUSSION_OPTIONS_MIN_COUNT = 2;
|
|
176
176
|
export const DISCUSSION_OPTIONS_MAX_COUNT = 10;
|
|
177
177
|
export const DISCUSSION_OPTION_MAX_LENGTH = 200;
|
|
178
|
-
/** The multi-select picker's idle auto-cancel countdown and its render tick (also drives the cursor-row blink). Single-select has no equivalent -- it delegates to Pi's own native ctx.ui.select, whose input loop this package does not control. */
|
|
179
|
-
export const DISCUSSION_PICKER_IDLE_TIMEOUT_MS = 30_000;
|
|
180
|
-
export const DISCUSSION_PICKER_TICK_MS = 500;
|
|
181
178
|
/** Bounds for the generic graph projection protocol (external bounded contexts). */
|
|
182
179
|
export const GRAPH_PROJECTION_MAX_ARTIFACTS_PER_BATCH = 500;
|
|
183
180
|
export const GRAPH_PROJECTION_MAX_EDGES_PER_BATCH = 1_000;
|
package/src/domain-services.ts
CHANGED
|
@@ -443,7 +443,7 @@ export function updateSkill(artifacts: ArtifactStore, id: string, input: UpdateS
|
|
|
443
443
|
|
|
444
444
|
function skillInvocationBody(skill: Artifact): string {
|
|
445
445
|
if (skill.subtype === "artifact-template") {
|
|
446
|
-
return `Create an artifact using Papyrus template "${skill.title}".\
|
|
446
|
+
return `Create an artifact using Papyrus template "${skill.title}".\ntemplate_name: ${skill.title}\nAsk for or infer all required template fields, then call the skills domain tool instantiate action.`;
|
|
447
447
|
}
|
|
448
448
|
if (skill.subtype === "workflow") {
|
|
449
449
|
const definition = validateSkillDefinition(skill.extra["definition"]);
|
|
@@ -451,7 +451,7 @@ function skillInvocationBody(skill: Artifact): string {
|
|
|
451
451
|
.filter(([, input]) => input.required && input.default === undefined)
|
|
452
452
|
.map(([name]) => name);
|
|
453
453
|
return [
|
|
454
|
-
`Run Papyrus workflow Skill "${skill.title}"
|
|
454
|
+
`Run Papyrus workflow Skill "${skill.title}".`,
|
|
455
455
|
`Required arguments: ${required.length > 0 ? required.join(", ") : "none"}.`,
|
|
456
456
|
"Call the skills domain tool with action=run and arguments after collecting required values.",
|
|
457
457
|
].join("\n");
|
|
@@ -460,7 +460,7 @@ function skillInvocationBody(skill: Artifact): string {
|
|
|
460
460
|
const steps = Array.isArray(skill.extra["steps"]) ? skill.extra["steps"].filter((step): step is string => typeof step === "string") : [];
|
|
461
461
|
const tools = Array.isArray(skill.extra["tools"]) ? skill.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
|
|
462
462
|
return [
|
|
463
|
-
`Apply Papyrus skill "${skill.title}"
|
|
463
|
+
`Apply Papyrus skill "${skill.title}".`,
|
|
464
464
|
`Trigger: ${trigger}`,
|
|
465
465
|
...(skill.body ? [`Context: ${skill.body}`] : []),
|
|
466
466
|
...(steps.length ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
|
|
@@ -492,16 +492,16 @@ export function skillInvocation(artifacts: ArtifactStore, id: string, visited: S
|
|
|
492
492
|
const target = artifacts.get(edge.to);
|
|
493
493
|
if (!target) continue; // dangling edge -- defensive, should not happen
|
|
494
494
|
if (target.kind !== "skill") {
|
|
495
|
-
linkedArtifactLines.push(`- ${edge.relation} ${target.kind} "${target.title}"
|
|
495
|
+
linkedArtifactLines.push(`- ${edge.relation} ${target.kind} "${target.title}"`);
|
|
496
496
|
continue;
|
|
497
497
|
}
|
|
498
498
|
if (visited.has(target.id)) {
|
|
499
|
-
linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}"
|
|
499
|
+
linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}" -- already invoked above in this chain, not repeated.`);
|
|
500
500
|
} else if (depth + 1 > SKILL_INVOCATION_MAX_CALL_DEPTH) {
|
|
501
|
-
linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}"
|
|
501
|
+
linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}" -- call depth limit reached, invoke it separately.`);
|
|
502
502
|
} else {
|
|
503
503
|
const nested = skillInvocation(artifacts, target.id, visited, depth + 1);
|
|
504
|
-
linkedSkillSections.push(`Also invoke linked skill (${edge.relation}) "${target.title}"
|
|
504
|
+
linkedSkillSections.push(`Also invoke linked skill (${edge.relation}) "${target.title}":\n${nested}`);
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
if (linkedArtifactLines.length > 0) {
|
|
@@ -647,7 +647,7 @@ export function playbookInvocation(artifacts: ArtifactStore, id: string, provide
|
|
|
647
647
|
});
|
|
648
648
|
const missingRequired = declaredArguments.filter((argument) => argument.required && provided[argument.name] === undefined);
|
|
649
649
|
const sections = [[
|
|
650
|
-
`Apply Papyrus playbook "${playbook.title}"
|
|
650
|
+
`Apply Papyrus playbook "${playbook.title}".`,
|
|
651
651
|
`Trigger: ${trigger}`,
|
|
652
652
|
...(playbook.body ? [`Context: ${playbook.body}`] : []),
|
|
653
653
|
...(argumentLines.length > 0 ? ["Arguments:", ...argumentLines] : []),
|
|
@@ -659,7 +659,7 @@ export function playbookInvocation(artifacts: ArtifactStore, id: string, provide
|
|
|
659
659
|
].join("\n")];
|
|
660
660
|
const edges = artifacts.relationships({ artifactIds: [id] }).filter((edge) => edge.from === id).slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
|
|
661
661
|
const linkedLines = edges
|
|
662
|
-
.map((edge) => { const target = artifacts.get(edge.to); return target ? `- ${edge.relation} ${target.kind} "${target.title}"
|
|
662
|
+
.map((edge) => { const target = artifacts.get(edge.to); return target ? `- ${edge.relation} ${target.kind} "${target.title}"` : undefined; })
|
|
663
663
|
.filter((line): line is string => line !== undefined);
|
|
664
664
|
if (linkedLines.length > 0) sections.push(["Linked context (query Papyrus for full detail before proceeding):", ...linkedLines].join("\n"));
|
|
665
665
|
return sections.join("\n\n");
|
package/src/task-context.ts
CHANGED
|
@@ -29,7 +29,7 @@ function renderCurrent(task: Artifact): string[] {
|
|
|
29
29
|
const desired = task.body.trim() || task.title;
|
|
30
30
|
const gates = gatesFrom(task);
|
|
31
31
|
return [
|
|
32
|
-
`Current: ${task.title}
|
|
32
|
+
`Current: ${task.title}`,
|
|
33
33
|
`Desired: ${desired}`,
|
|
34
34
|
`Verify: ${gates.length > 0 ? gates.map(renderGate).join("; ") : "inspect the desired outcome; no automated gates configured"}`,
|
|
35
35
|
];
|
|
@@ -56,7 +56,7 @@ function deferredBlockingDiscussions(artifacts: ArtifactStore, activeTaskId: str
|
|
|
56
56
|
if (!inScope(edge.to, activeTaskId, taskIds)) continue;
|
|
57
57
|
const blockedTask = artifacts.get(edge.to);
|
|
58
58
|
if (!blockedTask || blockedTask.status === "done" || blockedTask.status === "canceled") continue;
|
|
59
|
-
lines.push(`${discussion.title}
|
|
59
|
+
lines.push(`${discussion.title} -- blocks "${blockedTask.title}"`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
return lines;
|
|
@@ -77,8 +77,8 @@ export function taskContext(artifacts: ArtifactStore, activeTaskId?: string, tas
|
|
|
77
77
|
const rejected = open.filter((task) => task.status === "rejected").slice(0, TASK_CONTEXT_REJECTED_LIMIT);
|
|
78
78
|
const lines = tasks.length > 0 ? [`Progress: ${done}/${tasks.length} done`] : [];
|
|
79
79
|
for (const task of current) lines.push(...renderCurrent(task));
|
|
80
|
-
if (next) lines.push(`Next: ${next.title}
|
|
81
|
-
if (rejected.length > 0) lines.push(`Rejected: ${rejected.map((task) =>
|
|
80
|
+
if (next) lines.push(`Next: ${next.title}`);
|
|
81
|
+
if (rejected.length > 0) lines.push(`Rejected: ${rejected.map((task) => task.title).join(", ")}`);
|
|
82
82
|
if (deferredDiscussions.length > 0) {
|
|
83
83
|
lines.push("", "Deferred discussions blocking this scope -- resume and re-surface these, do not leave them dormant:");
|
|
84
84
|
for (const line of deferredDiscussions) lines.push(`• ${line}`);
|