@danypops/papyrus 0.21.6 → 0.22.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/README.md +10 -6
- package/extension/src/discussion-picker.ts +9 -4
- package/extension/src/docs.ts +10 -1
- package/extension/src/domain-tools.ts +42 -16
- package/extension/src/rules.ts +9 -2
- package/extension/src/skills.ts +9 -1
- package/package.json +1 -1
- package/src/authority-registry.ts +1 -1
- package/src/cli.ts +30 -3
- package/src/constants.ts +5 -0
- package/src/domain/artifact.ts +24 -0
- package/src/domain-services.ts +88 -4
- package/src/modules/docs.ts +5 -2
- package/src/modules/rules.ts +5 -2
- package/src/modules/skills.ts +5 -2
- package/src/service.ts +4 -0
package/README.md
CHANGED
|
@@ -119,9 +119,9 @@ Agent-facing domain tools own lifecycle invariants and sit above this store API:
|
|
|
119
119
|
|
|
120
120
|
- **`tasks`** — create/update/list/show/plan, manage the singleton active focus, replace evidence-bearing checklists, hierarchy/dependencies, lifecycle transitions, non-blocking gates, and review completion that focuses one deterministic ready successor without claiming effort
|
|
121
121
|
- **`notes`** — capture/list/show deferred human intent, mark it consumed, promote it to an existing Task/Doc/Rule/Skill, or archive it with an explicit disposition
|
|
122
|
-
- **`docs`** — create/list/show, activate/archive/reopen, and document-safe graph links; Note mutations remain behind the Notes facade
|
|
123
|
-
- **`rules`** — create/list/show/preview, enable/disable, and attach governance gates to tasks
|
|
124
|
-
- **`skills`** — create/list/show/invoke/run, enable/disable, create compatibility templates, and atomically instantiate parameterized workflow runs
|
|
122
|
+
- **`docs`** — create/update/list/show, activate/archive/reopen, and document-safe graph links; Note mutations remain behind the Notes facade
|
|
123
|
+
- **`rules`** — create/update/list/show/preview, enable/disable, and attach governance gates to tasks
|
|
124
|
+
- **`skills`** — create/update/list/show/invoke/run, enable/disable, create compatibility templates, and atomically instantiate parameterized workflow runs
|
|
125
125
|
|
|
126
126
|
Every tool operation is registered in the daemon’s `/api/v1/ops` registry; parity is verified in tests. The task consumer uses the `tasks.graph` operation, which returns task nodes with explicit parent, child, and dependency IDs rather than leaking SQLite rows or asking the UI to reconstruct relationships.
|
|
127
127
|
|
|
@@ -131,14 +131,18 @@ Internally, application services depend on the `ArtifactStore` and `GateRunner`
|
|
|
131
131
|
|
|
132
132
|
Every agent domain tool (tasks, docs, rules, skills, notes, discuss) addresses its artifacts by `name` (the exact title) wherever `id` would otherwise be required -- `dependency_name`/`parent_name`/`child_name`/`root_task_name`/`depends_on_names` (tasks), `target_name` (docs link, searches every kind since a link target can be any of them), `task_name` (rules gate, discuss block/unblock), `template_name` (skills instantiate), and `blocks_task_names` (discuss open) are the name-based equivalents of their `*_id` counterparts. Resolution is an exact, case-insensitive, trimmed title match scoped like a plain list call; an unmatched or ambiguous name fails with a clear error (ambiguous names list the real ids, since that's the one point disambiguation genuinely needs them). Results returned to the agent likewise lead with name and status, never id, unless two artifacts in the same result share a title -- id is a backend implementation detail, not a conversational handle. `id` itself still works exactly as before for every action, in every tool.
|
|
133
133
|
|
|
134
|
+
### Mutability
|
|
135
|
+
|
|
136
|
+
Tasks, Docs, Rules, and Skills all support first-class `update` (title/body/labels, at least one required) alongside creation -- a Doc's body is no longer immutable once created. Every update is bounded the same way creation is (Rules keep their own stricter combined condition+action+body ceiling; Docs/Skills share Tasks' own length bounds) and recorded on the artifact's append-only mutation history, queryable via `graph.history`. An artifact carrying a `source:<system>` label (e.g. `source:web-spider` on an ingested page) is a read-only projection from a system Papyrus doesn't own the source of; updating one is refused with a clear error rather than silently forking it -- capture a correction as a new linked Doc instead until a write-back capability to that system exists. Notes stay behind their own facade for any content change, same as every other Notes mutation.
|
|
137
|
+
|
|
134
138
|
## Interactive frontends
|
|
135
139
|
|
|
136
140
|
- `/tasks` — project/focused-graph scope, task lifecycle, append-only history, gates, dependencies, and nested metadata
|
|
137
141
|
- `/note <request>` — directly capture one project-scoped deferred request without creating a Task
|
|
138
142
|
- `/notes` — searchable project Notes inbox with consume, promote, and disposition-aware archive actions
|
|
139
|
-
- `/docs` — searchable non-Note documents, lifecycle, details, and graph links
|
|
140
|
-
- `/rules` — severity/condition rows, exact injection preview, enable/disable, and task gating
|
|
141
|
-
- `/skills` — trigger/tools rows, invocation into the editor, and artifact templates
|
|
143
|
+
- `/docs` — searchable non-Note documents, lifecycle, details, edit, and graph links
|
|
144
|
+
- `/rules` — severity/condition rows, exact injection preview, edit, enable/disable, and task gating
|
|
145
|
+
- `/skills` — trigger/tools rows, edit, invocation into the editor, and artifact templates
|
|
142
146
|
|
|
143
147
|
All frontends use daemon-backed domain operations; none opens SQLite from the Pi process. **Show details** opens a bounded navigable view across Tasks, Notes, Docs, Rules, legacy Skills, templates, and workflow Skills. User-authored bodies render as width-aware Markdown with headings, emphasis, links, quotes, lists, tables, inline/fenced code, syntax highlighting, and every color/decorative style derived dynamically from the active Pi theme. Generated lifecycle, metadata, checklist, gate, history, and relationship sections keep explicit semantic theme colors. `↑/↓` scrolls, `←/→` pans wide relationships, and Esc returns to the browser; non-interactive clients receive stable source text.
|
|
144
148
|
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* anywhere in @earendil-works/pi-coding-agent or pi-tui (checked both) -- so this is a small,
|
|
7
7
|
* genuinely domain-specific checkbox-list component, not a generic library replacement.
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
9
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
11
11
|
import type { DiscussionOptionsMode } from "../../src/domain/discussion.ts";
|
|
12
12
|
|
|
13
13
|
/** Toggle with space, confirm with enter (refuses an empty confirm -- at least one pick is required), cancel with escape. */
|
|
14
|
-
async function pickMultiple(ctx:
|
|
14
|
+
async function pickMultiple(ctx: ExtensionContext, title: string, options: string[]): Promise<string[] | undefined> {
|
|
15
15
|
return ctx.ui.custom<string[] | undefined>((tui, theme, _keybindings, done) => {
|
|
16
16
|
const checked = new Set<number>();
|
|
17
17
|
let selectedIndex = 0;
|
|
@@ -49,8 +49,13 @@ async function pickMultiple(ctx: ExtensionCommandContext, title: string, options
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Picks one (single) or several (multi) of the given options, or undefined if the user cancels.
|
|
54
|
+
* Takes the base ExtensionContext (just .ui) rather than the wider ExtensionCommandContext, since
|
|
55
|
+
* a tool's execute() only ever receives the former -- the discuss tool's own live mode reuses
|
|
56
|
+
* this same picker, not just the /discuss TUI panel.
|
|
57
|
+
*/
|
|
58
|
+
export async function pickDiscussionOptions(ctx: ExtensionContext, mode: DiscussionOptionsMode, options: string[]): Promise<string[] | undefined> {
|
|
54
59
|
if (mode === "single") {
|
|
55
60
|
const pick = await ctx.ui.select("Pick one:", options);
|
|
56
61
|
return pick ? [pick] : undefined;
|
package/extension/src/docs.ts
CHANGED
|
@@ -24,12 +24,21 @@ export async function showDocs(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
24
24
|
statusOrder: ["draft", "active", "archived"],
|
|
25
25
|
presentation: DOC_STATUS_PRESENTATION,
|
|
26
26
|
rowMeta: documentRowMeta,
|
|
27
|
-
actions: (document) => ["Show details", "Link artifact", ...(DOC_ACTIONS[document.status] ?? [])],
|
|
27
|
+
actions: (document) => ["Show details", "Edit", "Link artifact", ...(DOC_ACTIONS[document.status] ?? [])],
|
|
28
28
|
handleAction: async (choice, document, commandCtx) => {
|
|
29
29
|
if (choice === "Show details") {
|
|
30
30
|
await showArtifactDetails(commandCtx, document.id, "docs.show");
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
+
if (choice === "Edit") {
|
|
34
|
+
const title = await commandCtx.ui.input("Title:", document.title);
|
|
35
|
+
if (title === undefined) return; // canceled
|
|
36
|
+
const body = await commandCtx.ui.input("Body:", document.body);
|
|
37
|
+
if (body === undefined) return; // canceled
|
|
38
|
+
const updated = await callService<Record<string, unknown>, Artifact>("docs.update", { id: document.id, title, body });
|
|
39
|
+
commandCtx.ui.notify(`Updated "${updated.title}"`, "info");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
33
42
|
if (choice === "Link artifact") {
|
|
34
43
|
const targetId = await commandCtx.ui.input("Target artifact id:", "");
|
|
35
44
|
if (!targetId) return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
4
4
|
import { PROOF_TYPES } from "../../src/domain/checklist.ts";
|
|
@@ -8,7 +8,8 @@ import type { TaskHistoryPage } from "../../src/domain/task-event.ts";
|
|
|
8
8
|
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
|
-
import type
|
|
11
|
+
import { readDiscussionExtra, type DiscussionRound } from "../../src/domain/discussion.ts";
|
|
12
|
+
import { pickDiscussionOptions } from "./discussion-picker.ts";
|
|
12
13
|
import type { OperationName } from "../../src/service.ts";
|
|
13
14
|
import { emitTaskFocusEvent } from "./task-focus-events.ts";
|
|
14
15
|
import { sessionSecretField } from "./session-identity.ts";
|
|
@@ -30,6 +31,24 @@ function text(message: string, details: unknown = {}) {
|
|
|
30
31
|
return { content: [{ type: "text" as const, text: modelContent.text }], details };
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* live:true's synchronous half: renders the same picker /discuss's own "Reply" action uses when
|
|
36
|
+
* the just-created round posed a structured choice, or a plain freeform prompt otherwise -- so
|
|
37
|
+
* "ask" covers both a completely open question and a choice tied to this specific Discussion.
|
|
38
|
+
* Returns undefined on cancel or when no interactive UI is available, never throws -- an
|
|
39
|
+
* unanswered live prompt still leaves the round it already recorded intact.
|
|
40
|
+
*/
|
|
41
|
+
async function liveAnswer(ctx: ExtensionContext, discussion: Artifact): Promise<{ content: string; selected?: string[] } | undefined> {
|
|
42
|
+
if (!ctx.hasUI) return undefined;
|
|
43
|
+
const pending = (() => { try { return readDiscussionExtra(discussion.extra); } catch { return undefined; } })();
|
|
44
|
+
if (pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode) {
|
|
45
|
+
const selected = await pickDiscussionOptions(ctx, pending.pendingOptionsMode, pending.pendingOptions);
|
|
46
|
+
return selected ? { content: selected.join(", "), selected } : undefined;
|
|
47
|
+
}
|
|
48
|
+
const content = await ctx.ui.input(`Reply to "${discussion.title}":`, "");
|
|
49
|
+
return content ? { content } : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
/**
|
|
34
53
|
* Every domain tool's primary interfacing point is an artifact's NAME, not its id -- id is a
|
|
35
54
|
* backend implementation detail (a stable key other operations need, and titles aren't
|
|
@@ -397,7 +416,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
397
416
|
pi.registerTool({
|
|
398
417
|
name: "docs",
|
|
399
418
|
label: "Documents",
|
|
400
|
-
description: "Document domain tool. ACTIONS: create, list, show, activate, archive, reopen, link, assign_project, remove, restore. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. remove moves a Doc to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. PREFER `name` (the doc's exact title) over `id`, and `target_name` over `target_id` for link -- both are backend implementation details, resolved from name automatically (target_name searches across every kind, since a link target can be a doc, task, rule, or skill). Prefer this over low-level papyrus_* tools for document work.",
|
|
419
|
+
description: "Document domain tool. ACTIONS: create, list, show, activate, archive, reopen, link, assign_project, update, remove, restore. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. update changes title/body/labels (at least one required) and is refused for a read-only external projection (e.g. web-spider-ingested Docs) -- capture a correction as a new linked Doc instead. remove moves a Doc to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. PREFER `name` (the doc's exact title) over `id`, and `target_name` over `target_id` for link -- both are backend implementation details, resolved from name automatically (target_name searches across every kind, since a link target can be a doc, task, rule, or skill). Prefer this over low-level papyrus_* tools for document work.",
|
|
401
420
|
parameters: Type.Object({
|
|
402
421
|
action: Type.String(),
|
|
403
422
|
id: Type.Optional(Type.String()),
|
|
@@ -443,7 +462,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
443
462
|
}
|
|
444
463
|
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
445
464
|
if (trashResult) return trashResult;
|
|
446
|
-
const operations = { activate: "docs.activate", archive: "docs.archive", reopen: "docs.reopen", link: "docs.link", assign_project: "docs.assign_project" } as const;
|
|
465
|
+
const operations = { activate: "docs.activate", archive: "docs.archive", reopen: "docs.reopen", link: "docs.link", assign_project: "docs.assign_project", update: "docs.update" } as const;
|
|
447
466
|
const operation = operations[action as keyof typeof operations];
|
|
448
467
|
if (!operation) throw new Error(`unknown docs action: ${action}`);
|
|
449
468
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
|
@@ -457,7 +476,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
457
476
|
pi.registerTool({
|
|
458
477
|
name: "rules",
|
|
459
478
|
label: "Rules",
|
|
460
|
-
description: "Rule domain tool. ACTIONS: create, list, show, preview, enable, disable, gate, assign_project, remove, restore. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. Active rules inject into the agent system prompt. remove moves a Rule to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. PREFER `name` (the rule's exact title) over `id`, and `task_name` over `task_id` for gate -- both are backend implementation details, resolved from name automatically.",
|
|
479
|
+
description: "Rule domain tool. ACTIONS: create, list, show, preview, enable, disable, gate, assign_project, update, remove, restore. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. Active rules inject into the agent system prompt. update changes title/body/labels (at least one required); body updates still enforce the same combined condition+action+body context-tax bound as creation, and are refused for a read-only external projection. remove moves a Rule to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. PREFER `name` (the rule's exact title) over `id`, and `task_name` over `task_id` for gate -- both are backend implementation details, resolved from name automatically.",
|
|
461
480
|
parameters: Type.Object({
|
|
462
481
|
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
463
482
|
body: Type.Optional(Type.String()), condition: Type.Optional(Type.String()), rule_action: Type.Optional(Type.String()),
|
|
@@ -491,7 +510,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
491
510
|
}
|
|
492
511
|
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
493
512
|
if (trashResult) return trashResult;
|
|
494
|
-
const operations = { show: "rules.show", enable: "rules.enable", disable: "rules.disable", gate: "rules.gate", assign_project: "rules.assign_project" } as const;
|
|
513
|
+
const operations = { show: "rules.show", enable: "rules.enable", disable: "rules.disable", gate: "rules.gate", assign_project: "rules.assign_project", update: "rules.update" } as const;
|
|
495
514
|
const operation = operations[action as keyof typeof operations];
|
|
496
515
|
if (!operation) throw new Error(`unknown rules action: ${action}`);
|
|
497
516
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
|
@@ -505,7 +524,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
505
524
|
pi.registerTool({
|
|
506
525
|
name: "skills",
|
|
507
526
|
label: "Skills",
|
|
508
|
-
description: "Papyrus Skill workflow and compatibility-template domain tool. Papyrus Skills are parameterized Task/Rule/Doc bundles, distinct from prompt-only skills. ACTIONS: create, create_template, list, show, invoke, run, enable, disable, instantiate, assign_project, remove, restore. run validates arguments and atomically creates one scoped workflow run. project_root is optional at creation (omitted = unscoped) for create/create_template; assign_project reassigns it later, or unscopes when project_root is omitted. remove moves a Skill to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. PREFER `name` (the skill's exact title) over `id`, and `template_name` over `template_id` for instantiate -- both are backend implementation details, resolved from name automatically.",
|
|
527
|
+
description: "Papyrus Skill workflow and compatibility-template domain tool. Papyrus Skills are parameterized Task/Rule/Doc bundles, distinct from prompt-only skills. ACTIONS: create, create_template, list, show, invoke, run, enable, disable, instantiate, assign_project, update, remove, restore. run validates arguments and atomically creates one scoped workflow run. project_root is optional at creation (omitted = unscoped) for create/create_template; assign_project reassigns it later, or unscopes when project_root is omitted. update changes title/body/labels (at least one required) and is refused for a read-only external projection. remove moves a Skill to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. PREFER `name` (the skill's exact title) over `id`, and `template_name` over `template_id` for instantiate -- both are backend implementation details, resolved from name automatically.",
|
|
509
528
|
parameters: Type.Object({
|
|
510
529
|
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
511
530
|
body: Type.Optional(Type.String()), trigger: Type.Optional(Type.String()), steps: Type.Optional(Type.Array(Type.String())),
|
|
@@ -570,7 +589,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
570
589
|
}
|
|
571
590
|
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
572
591
|
if (trashResult) return trashResult;
|
|
573
|
-
const operations = { show: "skills.show", enable: "skills.enable", disable: "skills.disable", instantiate: "skills.instantiate", assign_project: "skills.assign_project" } as const;
|
|
592
|
+
const operations = { show: "skills.show", enable: "skills.enable", disable: "skills.disable", instantiate: "skills.instantiate", assign_project: "skills.assign_project", update: "skills.update" } as const;
|
|
574
593
|
const operation = operations[action as keyof typeof operations];
|
|
575
594
|
if (!operation) throw new Error(`unknown skills action: ${action}`);
|
|
576
595
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, action === "instantiate" ? request : params);
|
|
@@ -584,7 +603,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
584
603
|
pi.registerTool({
|
|
585
604
|
name: "discuss",
|
|
586
605
|
label: "Discuss",
|
|
587
|
-
description: "Native Papyrus deliberation with a real lifecycle -- distinct from a one-shot ask: a Discussion persists, takes multiple rounds, and can genuinely block a Task's completion until settled or deferred. ACTIONS: open, reply, defer, resume, settle, block, unblock, show, rounds, list. open starts round 1 and optionally blocks_task_ids immediately. reply is refused once deferred or settled -- resume first. defer is explicitly non-blocking (paused, resumable); settle is terminal and archives the discussion. block/unblock manage the blocking relationship to a task independently of open. A task's completion is refused while any active Discussion blocks it. open/reply can pose a structured choice via options (2-10 entries) + options_mode ('single' mutually exclusive, 'multi' allows several); reply answers a currently pending choice via selected, validated against it. PREFER `name` (the discussion's exact title) over `id`, `task_name`/`blocks_task_names` over `task_id`/`blocks_task_ids` -- all are backend implementation details, resolved from name automatically.",
|
|
606
|
+
description: "Native Papyrus deliberation with a real lifecycle -- distinct from a one-shot ask: a Discussion persists, takes multiple rounds, and can genuinely block a Task's completion until settled or deferred. ACTIONS: open, reply, defer, resume, settle, block, unblock, show, rounds, list. open starts round 1 and optionally blocks_task_ids immediately. reply is refused once deferred or settled -- resume first. defer is explicitly non-blocking (paused, resumable); settle is terminal and archives the discussion. block/unblock manage the blocking relationship to a task independently of open. A task's completion is refused while any active Discussion blocks it. open/reply can pose a structured choice via options (2-10 entries) + options_mode ('single' mutually exclusive, 'multi' allows several); reply answers a currently pending choice via selected, validated against it. Pass live:true on open or reply to get the human's answer synchronously in this same call, via an interactive prompt (the pending choice's picker if one was posed, otherwise a freeform question) -- covers a completely open question with no artifact (open with no prior discussion) and a question tied to a specific existing artifact (reply, addressed by name) alike. Only takes effect with an interactive UI available; otherwise degrades silently to the normal async round. PREFER `name` (the discussion's exact title) over `id`, `task_name`/`blocks_task_names` over `task_id`/`blocks_task_ids` -- all are backend implementation details, resolved from name automatically.",
|
|
588
607
|
parameters: Type.Object({
|
|
589
608
|
action: Type.String(),
|
|
590
609
|
id: Type.Optional(Type.String()),
|
|
@@ -606,6 +625,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
606
625
|
options: Type.Optional(Type.Array(Type.String())),
|
|
607
626
|
options_mode: Type.Optional(Type.String()),
|
|
608
627
|
selected: Type.Optional(Type.Array(Type.String())),
|
|
628
|
+
live: Type.Optional(Type.Boolean()),
|
|
609
629
|
}),
|
|
610
630
|
renderCall(args, theme) { return renderPapyrusToolCall("Discuss", args, theme); },
|
|
611
631
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
@@ -619,13 +639,19 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
619
639
|
{ nameKey: "task_name", idKey: "task_id", listOperation: "tasks.list", baseRequest: taskScope },
|
|
620
640
|
]);
|
|
621
641
|
await resolveNameArrayField(params, "blocks_task_names", "blocks_task_ids", "tasks.list", taskScope);
|
|
622
|
-
if (action === "open") {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
642
|
+
if (action === "open" || action === "reply") {
|
|
643
|
+
const operation = action === "open" ? "discuss.open" : "discuss.reply";
|
|
644
|
+
const result = await callService<Record<string, unknown>, DiscussionAndRounds>(operation, params);
|
|
645
|
+
const fallback = action === "open"
|
|
646
|
+
? text(`Opened discussion ${artifactLine(result.discussion)}`, createArtifactDetails("discuss.open", result.discussion))
|
|
647
|
+
: text(`Round ${result.rounds[0]?.roundNumber} added to "${result.discussion.title}"`, createArtifactDetails("discuss.reply", result.discussion));
|
|
648
|
+
if (params.live !== true) return fallback;
|
|
649
|
+
const answer = await liveAnswer(ctx, result.discussion);
|
|
650
|
+
if (!answer) return fallback;
|
|
651
|
+
const answered = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.reply", {
|
|
652
|
+
id: result.discussion.id, actor: "human", content: answer.content, ...(answer.selected ? { selected: answer.selected } : {}), source: "discuss-live",
|
|
653
|
+
});
|
|
654
|
+
return text(`"${answered.discussion.title}": ${answer.content}`, createArtifactDetails("discuss.reply", answered.discussion));
|
|
629
655
|
}
|
|
630
656
|
if (action === "block" || action === "unblock") {
|
|
631
657
|
const operation = action === "block" ? "discuss.block" : "discuss.unblock";
|
package/extension/src/rules.ts
CHANGED
|
@@ -25,10 +25,17 @@ export async function showRules(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
25
25
|
statusOrder: ["active", "deprecated"],
|
|
26
26
|
presentation: RULE_STATUS_PRESENTATION,
|
|
27
27
|
rowMeta: ruleRowMeta,
|
|
28
|
-
actions: (rule) => ["Show details", "Preview injection", "Link gated task", rule.status === "active" ? "Disable" : "Enable"],
|
|
28
|
+
actions: (rule) => ["Show details", "Edit", "Preview injection", "Link gated task", rule.status === "active" ? "Disable" : "Enable"],
|
|
29
29
|
handleAction: async (choice, rule, commandCtx) => {
|
|
30
30
|
if (choice === "Show details") await showArtifactDetails(commandCtx, rule.id, "rules.show");
|
|
31
|
-
else if (choice === "
|
|
31
|
+
else if (choice === "Edit") {
|
|
32
|
+
const title = await commandCtx.ui.input("Title:", rule.title);
|
|
33
|
+
if (title === undefined) return; // canceled
|
|
34
|
+
const body = await commandCtx.ui.input("Body:", rule.body);
|
|
35
|
+
if (body === undefined) return; // canceled
|
|
36
|
+
const updated = await callService<Record<string, unknown>, Artifact>("rules.update", { id: rule.id, title, body });
|
|
37
|
+
commandCtx.ui.notify(`Updated "${updated.title}"`, "info");
|
|
38
|
+
} else if (choice === "Preview injection") {
|
|
32
39
|
const preview = await callService<Record<string, unknown>, string>("rules.preview", { id: rule.id });
|
|
33
40
|
commandCtx.ui.notify(preview, "info");
|
|
34
41
|
} else if (choice === "Link gated task") {
|
package/extension/src/skills.ts
CHANGED
|
@@ -76,12 +76,20 @@ export async function showSkills(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
76
76
|
rowMeta: skillRowMeta,
|
|
77
77
|
actions: (skill) => [
|
|
78
78
|
"Show details",
|
|
79
|
+
"Edit",
|
|
79
80
|
skill.subtype === "artifact-template" ? "Use template" : skill.subtype === "workflow" ? "Run workflow" : "Invoke skill",
|
|
80
81
|
skill.status === "active" ? "Disable" : "Enable",
|
|
81
82
|
],
|
|
82
83
|
handleAction: async (choice, skill, commandCtx) => {
|
|
83
84
|
if (choice === "Show details") await showArtifactDetails(commandCtx, skill.id, "skills.show");
|
|
84
|
-
else if (choice === "
|
|
85
|
+
else if (choice === "Edit") {
|
|
86
|
+
const title = await commandCtx.ui.input("Title:", skill.title);
|
|
87
|
+
if (title === undefined) return; // canceled
|
|
88
|
+
const body = await commandCtx.ui.input("Body:", skill.body);
|
|
89
|
+
if (body === undefined) return; // canceled
|
|
90
|
+
const updated = await callService<Record<string, unknown>, Artifact>("skills.update", { id: skill.id, title, body });
|
|
91
|
+
commandCtx.ui.notify(`Updated "${updated.title}"`, "info");
|
|
92
|
+
} else if (choice === "Run workflow") {
|
|
85
93
|
const source = await commandCtx.ui.input("Workflow arguments JSON:", "{}");
|
|
86
94
|
if (source === undefined) return;
|
|
87
95
|
try {
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import type { ArtifactEventContext } from "./domain/artifact-event.ts";
|
|
|
22
22
|
import type { Artifact, ArtifactLink } from "./domain/artifact.ts";
|
|
23
23
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
24
24
|
|
|
25
|
-
export type ArtifactAction = "create" | "link" | "status";
|
|
25
|
+
export type ArtifactAction = "create" | "link" | "status" | "update";
|
|
26
26
|
|
|
27
27
|
export interface AuthorityClaim {
|
|
28
28
|
/** Module id that owns this kind/subtype/relation, e.g. "notes", "tasks". */
|
package/src/cli.ts
CHANGED
|
@@ -92,6 +92,7 @@ const USAGE = `Usage:
|
|
|
92
92
|
papyrus docs activate|archive|reopen <id> [--json]
|
|
93
93
|
papyrus docs link <id> <relation> <target-id> [--json]
|
|
94
94
|
papyrus docs assign-project <id> [project-root] [--json]
|
|
95
|
+
papyrus docs update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
|
|
95
96
|
papyrus rules create --title <title> [--body <body>] [--condition <text>] [--rule-action <text>] [--severity block|warn|info] [--labels-json <json>] [--extra-json <json>] [--project-root <path>] [--json]
|
|
96
97
|
papyrus rules list [--status <status>] [--text <query>] [--limit <count>] [--project-root <path>] [--json]
|
|
97
98
|
papyrus rules show <id> [--json]
|
|
@@ -100,6 +101,7 @@ const USAGE = `Usage:
|
|
|
100
101
|
papyrus rules gate <rule-id> <task-id> [--json]
|
|
101
102
|
papyrus rules injectable [--json]
|
|
102
103
|
papyrus rules assign-project <id> [project-root] [--json]
|
|
104
|
+
papyrus rules update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
|
|
103
105
|
papyrus skills run <id> [--arguments-json <json>] [--run-id <id>] [--json]
|
|
104
106
|
papyrus skills create --title <title> [--body <body>] [--trigger <text>] [--steps-json <json>] [--tools-json <json>] [--definition-json <json>] [--labels-json <json>] [--extra-json <json>] [--project-root <path>] [--json]
|
|
105
107
|
papyrus skills create-template --title <title> --target-kind <kind> [--defaults-json <json>] [--required-json <json>] [--body <body>] [--labels-json <json>] [--project-root <path>] [--json]
|
|
@@ -109,6 +111,7 @@ const USAGE = `Usage:
|
|
|
109
111
|
papyrus skills enable|disable <id> [--json]
|
|
110
112
|
papyrus skills instantiate <template-id> [--title <title>] [--body <body>] [--status <status>] [--labels-json <json>] [--extra-json <json>] [--json]
|
|
111
113
|
papyrus skills assign-project <id> [project-root] [--json]
|
|
114
|
+
papyrus skills update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
|
|
112
115
|
papyrus notes capture <request> [--title <title>] [--json]
|
|
113
116
|
papyrus notes list [--status <draft|active|archived>] [--text <query>] [--limit <count>] [--json]
|
|
114
117
|
papyrus notes show <id> [--json]
|
|
@@ -466,8 +469,16 @@ export async function runSkillCli(args: string[], client: TaskCliClient, project
|
|
|
466
469
|
human = `Created: ${artifactLabel(artifact)}`;
|
|
467
470
|
break;
|
|
468
471
|
}
|
|
472
|
+
case "update": {
|
|
473
|
+
if (!id || second) throw new Error("skills update requires exactly one skill id");
|
|
474
|
+
if (title === undefined && body === undefined && labels === undefined) throw new Error("skills update requires --title, --body, or --labels-json");
|
|
475
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.update", { id, title, body, labels });
|
|
476
|
+
result = artifact;
|
|
477
|
+
human = `${artifactLabel(artifact)}`;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
469
480
|
default:
|
|
470
|
-
throw new Error("skills action must be run, create, create-template, list, show, invoke, enable, disable, instantiate,
|
|
481
|
+
throw new Error("skills action must be run, create, create-template, list, show, invoke, enable, disable, instantiate, assign-project, or update");
|
|
471
482
|
}
|
|
472
483
|
return json ? JSON.stringify(result) : human;
|
|
473
484
|
}
|
|
@@ -632,8 +643,16 @@ export async function runDocsCli(args: string[], client: TaskCliClient): Promise
|
|
|
632
643
|
human = `Linked ${id} --${second}--> ${third}`;
|
|
633
644
|
break;
|
|
634
645
|
}
|
|
646
|
+
case "update": {
|
|
647
|
+
if (!id || second) throw new Error("docs update requires exactly one document id");
|
|
648
|
+
if (title === undefined && body === undefined && labels === undefined) throw new Error("docs update requires --title, --body, or --labels-json");
|
|
649
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.update", { id, title, body, labels });
|
|
650
|
+
result = artifact;
|
|
651
|
+
human = `${artifactLabel(artifact)}`;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
635
654
|
default:
|
|
636
|
-
throw new Error("docs action must be create, list, show, activate, archive, reopen, link,
|
|
655
|
+
throw new Error("docs action must be create, list, show, activate, archive, reopen, link, assign-project, or update");
|
|
637
656
|
}
|
|
638
657
|
return json ? JSON.stringify(result) : human;
|
|
639
658
|
}
|
|
@@ -736,8 +755,16 @@ export async function runRulesCli(args: string[], client: TaskCliClient, project
|
|
|
736
755
|
human = rows.length === 0 ? "No injectable rules." : rows.map((row) => row.title).join("\n");
|
|
737
756
|
break;
|
|
738
757
|
}
|
|
758
|
+
case "update": {
|
|
759
|
+
if (!id || second) throw new Error("rules update requires exactly one rule id");
|
|
760
|
+
if (title === undefined && body === undefined && labels === undefined) throw new Error("rules update requires --title, --body, or --labels-json");
|
|
761
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.update", { id, title, body, labels });
|
|
762
|
+
result = artifact;
|
|
763
|
+
human = `${artifactLabel(artifact)}`;
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
739
766
|
default:
|
|
740
|
-
throw new Error("rules action must be create, list, show, preview, enable, disable, gate, injectable,
|
|
767
|
+
throw new Error("rules action must be create, list, show, preview, enable, disable, gate, injectable, assign-project, or update");
|
|
741
768
|
}
|
|
742
769
|
return json ? JSON.stringify(result) : human;
|
|
743
770
|
}
|
package/src/constants.ts
CHANGED
|
@@ -119,6 +119,11 @@ export const TASK_TITLE_MAX_LENGTH = 500;
|
|
|
119
119
|
export const TASK_BODY_MAX_LENGTH = 100_000;
|
|
120
120
|
export const TASK_LABEL_MAX_COUNT = 64;
|
|
121
121
|
export const TASK_LABEL_MAX_LENGTH = 128;
|
|
122
|
+
/** Mutable Doc/Rule/Skill content bounds -- same numbers as Task's, since these are the same kind of freeform content at the same scale. Rules also enforce their own stricter RULE_TEXT_HARD_LIMIT_CHARACTERS on top of this. */
|
|
123
|
+
export const ARTIFACT_TITLE_MAX_LENGTH = 500;
|
|
124
|
+
export const ARTIFACT_BODY_MAX_LENGTH = 100_000;
|
|
125
|
+
export const ARTIFACT_LABEL_MAX_COUNT = 64;
|
|
126
|
+
export const ARTIFACT_LABEL_MAX_LENGTH = 128;
|
|
122
127
|
/** Append-only Task chronology query and evidence bounds. */
|
|
123
128
|
export const TASK_HISTORY_DEFAULT_LIMIT = 25;
|
|
124
129
|
export const TASK_HISTORY_MAX_LIMIT = 100;
|
package/src/domain/artifact.ts
CHANGED
|
@@ -67,3 +67,27 @@ export interface RelationshipQuery {
|
|
|
67
67
|
artifactIds?: string[];
|
|
68
68
|
limit?: number;
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A label of the form "source:<system>" marks an artifact as ingested/projected from an
|
|
73
|
+
* external, non-Papyrus system (e.g. web-spider's own "source:web-spider" convention on the
|
|
74
|
+
* Docs it creates) -- content Papyrus does not own and cannot safely rewrite without silently
|
|
75
|
+
* diverging from the true source. Editing one directly would look like a correction but really
|
|
76
|
+
* just be a local fork nobody re-syncs.
|
|
77
|
+
*/
|
|
78
|
+
export const EXTERNAL_SOURCE_LABEL_PREFIX = "source:";
|
|
79
|
+
|
|
80
|
+
/** The external system name from a "source:<system>" label, or undefined if this artifact has no such label (i.e. it's Papyrus-native content). */
|
|
81
|
+
export function externalSourceOf(artifact: Pick<Artifact, "labels">): string | undefined {
|
|
82
|
+
const label = artifact.labels.find((entry) => entry.startsWith(EXTERNAL_SOURCE_LABEL_PREFIX));
|
|
83
|
+
return label === undefined ? undefined : label.slice(EXTERNAL_SOURCE_LABEL_PREFIX.length) || undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Throws if the artifact is a read-only external projection; a caller must never silently rewrite content it doesn't own the source of. */
|
|
87
|
+
export function requireLocallyOwnedContent(artifact: Artifact): Artifact {
|
|
88
|
+
const system = externalSourceOf(artifact);
|
|
89
|
+
if (system !== undefined) {
|
|
90
|
+
throw new Error(`"${artifact.title}" is a read-only projection from ${system}; edit it there, or capture a correction as a new linked Doc, until a write-back capability is integrated`);
|
|
91
|
+
}
|
|
92
|
+
return artifact;
|
|
93
|
+
}
|
package/src/domain-services.ts
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ARTIFACT_BODY_MAX_LENGTH,
|
|
3
|
+
ARTIFACT_LABEL_MAX_COUNT,
|
|
4
|
+
ARTIFACT_LABEL_MAX_LENGTH,
|
|
2
5
|
ARTIFACT_SCOPE_MAX_ARTIFACTS,
|
|
6
|
+
ARTIFACT_TITLE_MAX_LENGTH,
|
|
3
7
|
RULE_TEXT_HARD_LIMIT_CHARACTERS,
|
|
4
8
|
SKILL_INVOCATION_MAX_CALL_DEPTH,
|
|
5
9
|
SKILL_INVOCATION_MAX_LINKED_ARTIFACTS,
|
|
6
10
|
} from "./constants.ts";
|
|
7
|
-
import type
|
|
11
|
+
import { requireLocallyOwnedContent, type Artifact, type CreateArtifactInput } from "./domain/artifact.ts";
|
|
8
12
|
import type { ArtifactEventContext } from "./domain/artifact-event.ts";
|
|
9
13
|
import { normalizeProjectRoot } from "./domain/task-scope.ts";
|
|
10
14
|
import { validateSkillDefinition } from "./domain/skill-definition.ts";
|
|
11
15
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
12
16
|
import type { ArtifactScopeStore } from "./ports/artifact-scope-store.ts";
|
|
13
17
|
import { NOTE_SUBTYPE } from "./note-service.ts";
|
|
14
|
-
import type
|
|
18
|
+
import { type ArtifactAction, type AuthorityRegistry } from "./authority-registry.ts";
|
|
19
|
+
|
|
20
|
+
export interface UpdateContentInput {
|
|
21
|
+
title?: string;
|
|
22
|
+
body?: string;
|
|
23
|
+
labels?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireContentUpdateFields(input: UpdateContentInput): void {
|
|
27
|
+
if (input.title === undefined && input.body === undefined && input.labels === undefined) {
|
|
28
|
+
throw new Error("update requires title, body, or labels");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertTitleBounds(title: string | undefined): void {
|
|
33
|
+
if (title !== undefined && (title.trim().length === 0 || title.length > ARTIFACT_TITLE_MAX_LENGTH)) {
|
|
34
|
+
throw new Error(`title must be between 1 and ${ARTIFACT_TITLE_MAX_LENGTH} characters`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertBodyBounds(body: string | undefined): void {
|
|
39
|
+
if (body !== undefined && body.length > ARTIFACT_BODY_MAX_LENGTH) throw new Error(`body cannot exceed ${ARTIFACT_BODY_MAX_LENGTH} characters`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertLabelsBounds(labels: string[] | undefined): void {
|
|
43
|
+
if (labels === undefined) return;
|
|
44
|
+
if (labels.length > ARTIFACT_LABEL_MAX_COUNT) throw new Error(`labels cannot exceed ${ARTIFACT_LABEL_MAX_COUNT} entries`);
|
|
45
|
+
if (labels.some((label) => label.length === 0 || label.length > ARTIFACT_LABEL_MAX_LENGTH)) {
|
|
46
|
+
throw new Error(`each label must be between 1 and ${ARTIFACT_LABEL_MAX_LENGTH} characters`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
15
49
|
|
|
16
50
|
export interface ListFilter {
|
|
17
51
|
status?: string;
|
|
@@ -83,8 +117,8 @@ function templateSubtype(artifacts: ArtifactStore, templateId: string | undefine
|
|
|
83
117
|
return typeof subtype === "string" ? subtype : undefined;
|
|
84
118
|
}
|
|
85
119
|
|
|
86
|
-
function requireMutableDocument(document: Artifact, authority: AuthorityRegistry): Artifact {
|
|
87
|
-
authority.requireArtifactAllowed(document.kind, document.subtype,
|
|
120
|
+
function requireMutableDocument(document: Artifact, authority: AuthorityRegistry, action: ArtifactAction = "status"): Artifact {
|
|
121
|
+
authority.requireArtifactAllowed(document.kind, document.subtype, action, "docs");
|
|
88
122
|
return document;
|
|
89
123
|
}
|
|
90
124
|
|
|
@@ -99,6 +133,8 @@ export interface CreateDocumentInput {
|
|
|
99
133
|
projectRoot?: string;
|
|
100
134
|
}
|
|
101
135
|
|
|
136
|
+
export type UpdateDocumentInput = UpdateContentInput;
|
|
137
|
+
|
|
102
138
|
export type DocumentTransition = "activate" | "archive" | "reopen";
|
|
103
139
|
export type DocumentRelation = "references" | "documents" | "supersedes" | "relates_to" | "contains" | "part_of";
|
|
104
140
|
|
|
@@ -157,6 +193,23 @@ export function transitionDocument(artifacts: ArtifactStore, id: string, action:
|
|
|
157
193
|
return artifacts.setStatus(id, transition.to, context)!;
|
|
158
194
|
}
|
|
159
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Docs are immutable-by-convention only in the sense that no path existed to change them --
|
|
198
|
+
* this is that path. A read-only external projection (see requireLocallyOwnedContent) still
|
|
199
|
+
* refuses, on purpose: rewriting it here would silently fork from whatever system actually
|
|
200
|
+
* owns it (e.g. web-spider's ingested pages), with nothing to ever reconcile the two again.
|
|
201
|
+
*/
|
|
202
|
+
export function updateDocument(artifacts: ArtifactStore, id: string, input: UpdateDocumentInput, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
|
|
203
|
+
requireContentUpdateFields(input);
|
|
204
|
+
assertTitleBounds(input.title);
|
|
205
|
+
assertBodyBounds(input.body);
|
|
206
|
+
assertLabelsBounds(input.labels);
|
|
207
|
+
const document = requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "update"));
|
|
208
|
+
const updated = artifacts.updateContent(id, input, context);
|
|
209
|
+
if (!updated) throw new Error(`document "${id}" not found`);
|
|
210
|
+
return updated;
|
|
211
|
+
}
|
|
212
|
+
|
|
160
213
|
export function linkDocument(artifacts: ArtifactStore, id: string, relation: DocumentRelation, targetId: string, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
|
|
161
214
|
requireMutableDocument(requireDocument(artifacts, id), authority);
|
|
162
215
|
const target = artifacts.get(targetId);
|
|
@@ -258,6 +311,24 @@ export function transitionRule(artifacts: ArtifactStore, id: string, action: Rul
|
|
|
258
311
|
return artifacts.setStatus(id, target, context)!;
|
|
259
312
|
}
|
|
260
313
|
|
|
314
|
+
export type UpdateRuleInput = UpdateContentInput;
|
|
315
|
+
|
|
316
|
+
/** A Rule's body update stays under the same combined condition+action+body ceiling as creation -- a permanent per-turn injection cost doesn't get looser just because it's an edit, not a create. */
|
|
317
|
+
export function updateRule(artifacts: ArtifactStore, id: string, input: UpdateRuleInput, context?: ArtifactEventContext): Artifact {
|
|
318
|
+
requireContentUpdateFields(input);
|
|
319
|
+
assertTitleBounds(input.title);
|
|
320
|
+
assertLabelsBounds(input.labels);
|
|
321
|
+
const rule = requireLocallyOwnedContent(requireKind(artifacts, id, "rule"));
|
|
322
|
+
if (input.body !== undefined) {
|
|
323
|
+
const condition = typeof rule.extra["condition"] === "string" ? rule.extra["condition"] : undefined;
|
|
324
|
+
const action = typeof rule.extra["action"] === "string" ? rule.extra["action"] : undefined;
|
|
325
|
+
assertRuleTextWithinBounds(condition, action, input.body);
|
|
326
|
+
}
|
|
327
|
+
const updated = artifacts.updateContent(id, input, context);
|
|
328
|
+
if (!updated) throw new Error(`rule "${id}" not found`);
|
|
329
|
+
return updated;
|
|
330
|
+
}
|
|
331
|
+
|
|
261
332
|
export function gateTaskWithRule(artifacts: ArtifactStore, ruleId: string, taskId: string, context?: ArtifactEventContext): Artifact {
|
|
262
333
|
requireKind(artifacts, ruleId, "rule");
|
|
263
334
|
requireKind(artifacts, taskId, "task");
|
|
@@ -353,6 +424,19 @@ export function showSkill(artifacts: ArtifactStore, id: string): Artifact {
|
|
|
353
424
|
return artifacts.get(id, { tree: true })!;
|
|
354
425
|
}
|
|
355
426
|
|
|
427
|
+
export type UpdateSkillInput = UpdateContentInput;
|
|
428
|
+
|
|
429
|
+
export function updateSkill(artifacts: ArtifactStore, id: string, input: UpdateSkillInput, context?: ArtifactEventContext): Artifact {
|
|
430
|
+
requireContentUpdateFields(input);
|
|
431
|
+
assertTitleBounds(input.title);
|
|
432
|
+
assertBodyBounds(input.body);
|
|
433
|
+
assertLabelsBounds(input.labels);
|
|
434
|
+
const skill = requireLocallyOwnedContent(requireKind(artifacts, id, "skill"));
|
|
435
|
+
const updated = artifacts.updateContent(skill.id, input, context);
|
|
436
|
+
if (!updated) throw new Error(`skill "${id}" not found`);
|
|
437
|
+
return updated;
|
|
438
|
+
}
|
|
439
|
+
|
|
356
440
|
function skillInvocationBody(skill: Artifact): string {
|
|
357
441
|
if (skill.subtype === "artifact-template") {
|
|
358
442
|
return `Create an artifact using Papyrus template "${skill.title}".\ntemplate_id: ${skill.id}\nAsk for or infer all required template fields, then call the skills domain tool instantiate action.`;
|
package/src/modules/docs.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* ArtifactStore-based with no other module's concrete class dependency.
|
|
8
8
|
*/
|
|
9
9
|
import type { AuthorityRegistry } from "../authority-registry.ts";
|
|
10
|
-
import { assignDocumentProject, createDocument, linkDocument, listDocuments, showDocument, transitionDocument, type DocumentRelation } from "../domain-services.ts";
|
|
10
|
+
import { assignDocumentProject, createDocument, linkDocument, listDocuments, showDocument, transitionDocument, updateDocument, type DocumentRelation } from "../domain-services.ts";
|
|
11
11
|
import type { OperationDefinition } from "../module-registry.ts";
|
|
12
12
|
import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
|
|
13
13
|
import type { ArtifactStore } from "../ports/artifact-store.ts";
|
|
@@ -52,7 +52,7 @@ const artifactFilter = (input: OperationInput) => ({
|
|
|
52
52
|
/** Registers every docs.* operation against the shared ArtifactStore port. Behavior is unchanged from the prior inline handlers in src/service.ts. */
|
|
53
53
|
/** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. */
|
|
54
54
|
export const DOCS_OPERATION_NAMES = [
|
|
55
|
-
"docs.create", "docs.list", "docs.show", "docs.activate", "docs.archive", "docs.reopen", "docs.link", "docs.assign_project",
|
|
55
|
+
"docs.create", "docs.list", "docs.show", "docs.activate", "docs.archive", "docs.reopen", "docs.link", "docs.assign_project", "docs.update",
|
|
56
56
|
] as const;
|
|
57
57
|
|
|
58
58
|
export function docsOperations(artifacts: ArtifactStore, scopes: ArtifactScopeStore, authority: AuthorityRegistry): OperationDefinition[] {
|
|
@@ -73,5 +73,8 @@ export function docsOperations(artifacts: ArtifactStore, scopes: ArtifactScopeSt
|
|
|
73
73
|
define("docs.reopen", (input: OperationInput) => transitionDocument(artifacts, string(input, "id"), "reopen", authority, eventContext(input))),
|
|
74
74
|
define("docs.link", (input: OperationInput) => linkDocument(artifacts, string(input, "id"), string(input, "relation") as DocumentRelation, string(input, "target_id"), authority, eventContext(input))),
|
|
75
75
|
define("docs.assign_project", (input: OperationInput) => assignDocumentProject(artifacts, scopes, string(input, "id"), optionalString(input, "project_root"))),
|
|
76
|
+
define("docs.update", (input: OperationInput) => updateDocument(artifacts, string(input, "id"), {
|
|
77
|
+
title: optionalString(input, "title"), body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
|
|
78
|
+
}, authority, eventContext(input))),
|
|
76
79
|
];
|
|
77
80
|
}
|
package/src/modules/rules.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* into this module or introducing a premature "modules call each other through the
|
|
11
11
|
* registry" convention.
|
|
12
12
|
*/
|
|
13
|
-
import { assignRuleProject, createRule, gateTaskWithRule, listRules, previewRule, showRule, transitionRule } from "../domain-services.ts";
|
|
13
|
+
import { assignRuleProject, createRule, gateTaskWithRule, listRules, previewRule, showRule, transitionRule, updateRule } from "../domain-services.ts";
|
|
14
14
|
import type { OperationDefinition } from "../module-registry.ts";
|
|
15
15
|
import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
|
|
16
16
|
import type { ArtifactStore } from "../ports/artifact-store.ts";
|
|
@@ -55,7 +55,7 @@ const artifactFilter = (input: OperationInput) => ({
|
|
|
55
55
|
/** Registers every rules.* operation except rules.injectable (see module comment). Behavior is unchanged from the prior inline handlers in src/service.ts. */
|
|
56
56
|
/** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. rules.injectable is deliberately absent -- see the module comment above. */
|
|
57
57
|
export const RULES_OPERATION_NAMES = [
|
|
58
|
-
"rules.create", "rules.list", "rules.show", "rules.preview", "rules.enable", "rules.disable", "rules.gate", "rules.assign_project",
|
|
58
|
+
"rules.create", "rules.list", "rules.show", "rules.preview", "rules.enable", "rules.disable", "rules.gate", "rules.assign_project", "rules.update",
|
|
59
59
|
] as const;
|
|
60
60
|
|
|
61
61
|
export function rulesOperations(artifacts: ArtifactStore, scopes: ArtifactScopeStore): OperationDefinition[] {
|
|
@@ -77,5 +77,8 @@ export function rulesOperations(artifacts: ArtifactStore, scopes: ArtifactScopeS
|
|
|
77
77
|
define("rules.disable", (input: OperationInput) => transitionRule(artifacts, string(input, "id"), "disable", eventContext(input))),
|
|
78
78
|
define("rules.gate", (input: OperationInput) => gateTaskWithRule(artifacts, string(input, "id"), string(input, "task_id"), eventContext(input))),
|
|
79
79
|
define("rules.assign_project", (input: OperationInput) => assignRuleProject(artifacts, scopes, string(input, "id"), optionalString(input, "project_root"))),
|
|
80
|
+
define("rules.update", (input: OperationInput) => updateRule(artifacts, string(input, "id"), {
|
|
81
|
+
title: optionalString(input, "title"), body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
|
|
82
|
+
}, eventContext(input))),
|
|
80
83
|
];
|
|
81
84
|
}
|
package/src/modules/skills.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* extraction.
|
|
18
18
|
*/
|
|
19
19
|
import type { AuthorityRegistry } from "../authority-registry.ts";
|
|
20
|
-
import { assignSkillProject, createArtifactTemplate, createSkill, listSkills, showSkill, skillInvocation, transitionSkill } from "../domain-services.ts";
|
|
20
|
+
import { assignSkillProject, createArtifactTemplate, createSkill, listSkills, showSkill, skillInvocation, transitionSkill, updateSkill } from "../domain-services.ts";
|
|
21
21
|
import type { OperationDefinition } from "../module-registry.ts";
|
|
22
22
|
import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
|
|
23
23
|
import type { ArtifactStore } from "../ports/artifact-store.ts";
|
|
@@ -79,7 +79,7 @@ export interface SkillsModuleDeps {
|
|
|
79
79
|
/** Registers every skills.* operation except skills.instantiate (see module comment). Behavior is unchanged from the prior inline handlers in src/service.ts. */
|
|
80
80
|
/** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. skills.instantiate is deliberately absent -- see the module comment above. */
|
|
81
81
|
export const SKILLS_OPERATION_NAMES = [
|
|
82
|
-
"skills.create", "skills.create_template", "skills.list", "skills.show", "skills.invoke", "skills.run", "skills.enable", "skills.disable", "skills.assign_project",
|
|
82
|
+
"skills.create", "skills.create_template", "skills.list", "skills.show", "skills.invoke", "skills.run", "skills.enable", "skills.disable", "skills.assign_project", "skills.update",
|
|
83
83
|
] as const;
|
|
84
84
|
|
|
85
85
|
export function skillsOperations({ artifacts, events, scopes, artifactScopes, authority }: SkillsModuleDeps): OperationDefinition[] {
|
|
@@ -109,5 +109,8 @@ export function skillsOperations({ artifacts, events, scopes, artifactScopes, au
|
|
|
109
109
|
define("skills.enable", (input: OperationInput) => transitionSkill(artifacts, string(input, "id"), "enable", eventContext(input))),
|
|
110
110
|
define("skills.disable", (input: OperationInput) => transitionSkill(artifacts, string(input, "id"), "disable", eventContext(input))),
|
|
111
111
|
define("skills.assign_project", (input: OperationInput) => assignSkillProject(artifacts, artifactScopes, string(input, "id"), optionalString(input, "project_root"))),
|
|
112
|
+
define("skills.update", (input: OperationInput) => updateSkill(artifacts, string(input, "id"), {
|
|
113
|
+
title: optionalString(input, "title"), body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
|
|
114
|
+
}, eventContext(input))),
|
|
112
115
|
];
|
|
113
116
|
}
|
package/src/service.ts
CHANGED
|
@@ -144,6 +144,7 @@ const notesAuthorityClaim: AuthorityClaim = {
|
|
|
144
144
|
denyMessage: (action) => {
|
|
145
145
|
if (action === "link") return "note relationships require a notes.* operation so disposition provenance is preserved";
|
|
146
146
|
if (action === "status") return "note lifecycle changes require a notes.* operation so disposition provenance is preserved";
|
|
147
|
+
if (action === "update") return "note content changes require a notes.* operation so disposition provenance is preserved";
|
|
147
148
|
return "note creation requires notes.capture";
|
|
148
149
|
},
|
|
149
150
|
};
|
|
@@ -341,6 +342,7 @@ function handlers(
|
|
|
341
342
|
"docs.reopen": forwardToModule("docs.reopen"),
|
|
342
343
|
"docs.link": forwardToModule("docs.link"),
|
|
343
344
|
"docs.assign_project": forwardToModule("docs.assign_project"),
|
|
345
|
+
"docs.update": forwardToModule("docs.update"),
|
|
344
346
|
"notes.capture": forwardToModule("notes.capture"),
|
|
345
347
|
"notes.list": forwardToModule("notes.list"),
|
|
346
348
|
"notes.show": forwardToModule("notes.show"),
|
|
@@ -355,6 +357,7 @@ function handlers(
|
|
|
355
357
|
"rules.disable": forwardToModule("rules.disable"),
|
|
356
358
|
"rules.gate": forwardToModule("rules.gate"),
|
|
357
359
|
"rules.assign_project": forwardToModule("rules.assign_project"),
|
|
360
|
+
"rules.update": forwardToModule("rules.update"),
|
|
358
361
|
"skills.create": forwardToModule("skills.create"),
|
|
359
362
|
"skills.create_template": forwardToModule("skills.create_template"),
|
|
360
363
|
"skills.list": forwardToModule("skills.list"),
|
|
@@ -364,6 +367,7 @@ function handlers(
|
|
|
364
367
|
"skills.enable": forwardToModule("skills.enable"),
|
|
365
368
|
"skills.disable": forwardToModule("skills.disable"),
|
|
366
369
|
"skills.assign_project": forwardToModule("skills.assign_project"),
|
|
370
|
+
"skills.update": forwardToModule("skills.update"),
|
|
367
371
|
"skills.instantiate": (input) => {
|
|
368
372
|
const templateId = string(input, "template_id");
|
|
369
373
|
const template = artifacts.get(templateId);
|