@danypops/papyrus 0.41.0 → 0.42.1
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 +8 -11
- package/package.json +2 -2
- package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
- package/src/adapters/sqlite-artifact-store.ts +7 -5
- package/src/adapters/sqlite-discussion-round-store.ts +26 -17
- package/src/adapters/sqlite-gate-runner.ts +1 -1
- package/src/adapters/sqlite-graph-projection-store.ts +14 -10
- package/src/adapters/sqlite-log-store.ts +36 -17
- package/src/adapters/sqlite-note-event-store.ts +20 -16
- package/src/adapters/sqlite-session-identity-store.ts +13 -7
- package/src/adapters/sqlite-task-event-store.ts +29 -21
- package/src/adapters/sqlite-task-focus-store.ts +36 -10
- package/src/adapters/sqlite-task-lease-store.ts +12 -6
- package/src/adapters/sqlite-task-scope-store.ts +25 -14
- package/src/artifact-relationship-view.ts +4 -4
- package/src/artifact-subtree.ts +4 -2
- package/src/authority-registry.ts +2 -1
- package/src/cli.ts +794 -354
- package/src/client.ts +6 -3
- package/src/constants.ts +34 -56
- package/src/daemon-state.ts +4 -12
- package/src/daemon.ts +31 -9
- package/src/db.ts +153 -105
- package/src/discussion-service.ts +109 -44
- package/src/domain/artifact-event.ts +18 -5
- package/src/domain/artifact.ts +3 -1
- package/src/domain/blueprint-definition.ts +268 -0
- package/src/domain/checklist.ts +20 -17
- package/src/domain/discussion.ts +37 -18
- package/src/domain/gate.ts +7 -7
- package/src/domain/log-entry.ts +1 -1
- package/src/domain/note-event.ts +20 -7
- package/src/domain/task-event.ts +17 -7
- package/src/domain-services.ts +362 -288
- package/src/graph-projection-service.ts +34 -8
- package/src/id-migration.ts +17 -4
- package/src/index.ts +16 -11
- package/src/log-service.ts +6 -5
- package/src/log.ts +19 -0
- package/src/modules/discuss.ts +63 -28
- package/src/modules/docs.ts +74 -17
- package/src/modules/graph-projection.ts +20 -9
- package/src/modules/logs.ts +34 -22
- package/src/modules/notes.ts +66 -28
- package/src/modules/playbooks.ts +93 -32
- package/src/modules/rules.ts +57 -15
- package/src/modules/session-identity.ts +6 -2
- package/src/modules/tasks.ts +142 -67
- package/src/note-service.ts +11 -7
- package/src/ops.ts +134 -69
- package/src/playbook-definition.ts +124 -39
- package/src/playbook-execution.ts +18 -25
- package/src/ports/artifact-scope-store.ts +1 -1
- package/src/ports/note-event-store.ts +6 -4
- package/src/ports/task-event-store.ts +9 -6
- package/src/ports/task-focus-store.ts +17 -4
- package/src/ports/task-lease-store.ts +9 -4
- package/src/ports/task-scope-store.ts +3 -1
- package/src/service.ts +148 -110
- package/src/session-identity-service.ts +10 -2
- package/src/task-context.ts +28 -16
- package/src/task-execution.ts +4 -12
- package/src/task-graph-view.ts +12 -12
- package/src/task-relationship-view.ts +1 -3
- package/src/task-service.ts +168 -73
- package/src/vehicle/artifact-trash-vehicle.ts +26 -14
- package/src/vehicle/artifact-vehicle-shared.ts +32 -13
- package/src/vehicle/docs-vehicle.ts +50 -18
- package/src/vehicle/notes-vehicle.ts +26 -8
- package/src/vehicle/papyrus-vehicle.ts +16 -8
- package/src/vehicle/playbooks-vehicle.ts +88 -19
- package/src/vehicle/rules-vehicle.ts +58 -21
- package/src/vehicle/tasks-vehicle.ts +366 -54
- package/src/version.ts +1 -1
- package/src/workflow-execution.ts +198 -109
- package/src/domain/skill-definition.ts +0 -270
- package/src/modules/skills.ts +0 -158
- package/src/vehicle/skills-vehicle.ts +0 -194
package/src/domain/discussion.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
export const DISCUSSION_SUBTYPE = "discussion";
|
|
33
33
|
|
|
34
34
|
export const DISCUSSION_STATES = ["active", "deferred", "settled"] as const;
|
|
35
|
-
export type DiscussionState = typeof DISCUSSION_STATES[number];
|
|
35
|
+
export type DiscussionState = (typeof DISCUSSION_STATES)[number];
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* A round can pose a choice (options + optionsMode) the way opencode's QuestionV2 poses
|
|
@@ -40,7 +40,7 @@ export type DiscussionState = typeof DISCUSSION_STATES[number];
|
|
|
40
40
|
* (exactly one pick); "multi" allows several -- see constants.ts.
|
|
41
41
|
*/
|
|
42
42
|
export const DISCUSSION_OPTIONS_MODES = ["single", "multi"] as const;
|
|
43
|
-
export type DiscussionOptionsMode = typeof DISCUSSION_OPTIONS_MODES[number];
|
|
43
|
+
export type DiscussionOptionsMode = (typeof DISCUSSION_OPTIONS_MODES)[number];
|
|
44
44
|
|
|
45
45
|
/** Persisted in a discussion Doc's `extra.discussion`. pendingOptions/-Mode is the current-state cache of "is there an unanswered posed choice right now" -- cleared once answered, set again whenever a round poses a new one. */
|
|
46
46
|
export interface DiscussionExtra {
|
|
@@ -113,7 +113,11 @@ export function validateSettlement(settlement: string): string {
|
|
|
113
113
|
/** Validates a freshly-posed choice: 2..DISCUSSION_OPTIONS_MAX_COUNT unique, bounded-length
|
|
114
114
|
* options, a real mode, and a non-empty description for every option once
|
|
115
115
|
* DISCUSSION_OPTION_DESCRIPTION_REQUIRED_FROM_COUNT or more are posed. */
|
|
116
|
-
export function validateDiscussionOptions(
|
|
116
|
+
export function validateDiscussionOptions(
|
|
117
|
+
options: string[],
|
|
118
|
+
mode: string,
|
|
119
|
+
optionDescriptions?: string[],
|
|
120
|
+
): { options: string[]; mode: DiscussionOptionsMode; optionDescriptions?: string[] } {
|
|
117
121
|
if (!(DISCUSSION_OPTIONS_MODES as readonly string[]).includes(mode)) {
|
|
118
122
|
throw new Error(`options_mode must be one of ${DISCUSSION_OPTIONS_MODES.join(", ")}`);
|
|
119
123
|
}
|
|
@@ -123,20 +127,35 @@ export function validateDiscussionOptions(options: string[], mode: string, optio
|
|
|
123
127
|
for (const option of options) boundedString(option, "option", DISCUSSION_OPTION_MAX_LENGTH);
|
|
124
128
|
if (new Set(options).size !== options.length) throw new Error("options must not repeat an entry");
|
|
125
129
|
const descriptionsRequired = options.length >= DISCUSSION_OPTION_DESCRIPTION_REQUIRED_FROM_COUNT;
|
|
126
|
-
if (
|
|
127
|
-
|
|
130
|
+
if (
|
|
131
|
+
descriptionsRequired &&
|
|
132
|
+
(optionDescriptions === undefined || optionDescriptions.some((description) => description.trim().length === 0))
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`option_descriptions is required, with a non-empty entry for every option, once ${DISCUSSION_OPTION_DESCRIPTION_REQUIRED_FROM_COUNT} or more options are posed`,
|
|
136
|
+
);
|
|
128
137
|
}
|
|
129
138
|
if (optionDescriptions !== undefined) {
|
|
130
|
-
if (optionDescriptions.length !== options.length)
|
|
139
|
+
if (optionDescriptions.length !== options.length)
|
|
140
|
+
throw new Error("option_descriptions must have exactly one entry per option (use an empty string for none)");
|
|
131
141
|
for (const description of optionDescriptions) {
|
|
132
|
-
if (description.length > DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH)
|
|
142
|
+
if (description.length > DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH)
|
|
143
|
+
throw new Error(`option description must be at most ${DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH} characters`);
|
|
133
144
|
}
|
|
134
145
|
}
|
|
135
|
-
return {
|
|
146
|
+
return {
|
|
147
|
+
options: [...options],
|
|
148
|
+
mode: mode as DiscussionOptionsMode,
|
|
149
|
+
...(optionDescriptions !== undefined ? { optionDescriptions: [...optionDescriptions] } : {}),
|
|
150
|
+
};
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
/** Validates an answer against the Discussion's currently pending posed choice, if any. */
|
|
139
|
-
export function validateSelectedOptions(
|
|
154
|
+
export function validateSelectedOptions(
|
|
155
|
+
selected: string[],
|
|
156
|
+
pendingOptions: string[] | undefined,
|
|
157
|
+
pendingMode: DiscussionOptionsMode | undefined,
|
|
158
|
+
): string[] {
|
|
140
159
|
if (!pendingOptions || pendingOptions.length === 0 || !pendingMode) {
|
|
141
160
|
throw new Error("this Discussion has no pending options to select from");
|
|
142
161
|
}
|
|
@@ -155,26 +174,26 @@ export function isDiscussionArtifact(artifact: { kind: string; subtype: string }
|
|
|
155
174
|
|
|
156
175
|
/** Reads and defensively validates the extra.discussion shape; throws on a corrupt/foreign shape rather than silently treating it as some default state. */
|
|
157
176
|
export function readDiscussionExtra(extra: Record<string, unknown>): DiscussionExtra {
|
|
158
|
-
const raw = extra
|
|
177
|
+
const raw = extra.discussion;
|
|
159
178
|
if (typeof raw !== "object" || raw === null) throw new Error("artifact is not a Discussion (missing extra.discussion)");
|
|
160
179
|
const record = raw as Record<string, unknown>;
|
|
161
|
-
const state = record
|
|
180
|
+
const state = record.state;
|
|
162
181
|
if (typeof state !== "string" || !(DISCUSSION_STATES as readonly string[]).includes(state)) {
|
|
163
182
|
throw new Error(`invalid Discussion state "${String(state)}"`);
|
|
164
183
|
}
|
|
165
|
-
const roundCount = record
|
|
184
|
+
const roundCount = record.roundCount;
|
|
166
185
|
if (typeof roundCount !== "number" || !Number.isInteger(roundCount) || roundCount < 0) {
|
|
167
186
|
throw new Error("invalid Discussion roundCount");
|
|
168
187
|
}
|
|
169
|
-
const pendingOptions = record
|
|
188
|
+
const pendingOptions = record.pendingOptions;
|
|
170
189
|
if (pendingOptions !== undefined && (!Array.isArray(pendingOptions) || pendingOptions.some((entry) => typeof entry !== "string"))) {
|
|
171
190
|
throw new Error("invalid Discussion pendingOptions");
|
|
172
191
|
}
|
|
173
|
-
const pendingOptionsMode = record
|
|
192
|
+
const pendingOptionsMode = record.pendingOptionsMode;
|
|
174
193
|
if (pendingOptionsMode !== undefined && !(DISCUSSION_OPTIONS_MODES as readonly unknown[]).includes(pendingOptionsMode)) {
|
|
175
194
|
throw new Error("invalid Discussion pendingOptionsMode");
|
|
176
195
|
}
|
|
177
|
-
const pendingOptionDescriptions = record
|
|
196
|
+
const pendingOptionDescriptions = record.pendingOptionDescriptions;
|
|
178
197
|
if (pendingOptionDescriptions !== undefined) {
|
|
179
198
|
if (!Array.isArray(pendingOptionDescriptions) || pendingOptionDescriptions.some((entry) => typeof entry !== "string")) {
|
|
180
199
|
throw new Error("invalid Discussion pendingOptionDescriptions");
|
|
@@ -186,9 +205,9 @@ export function readDiscussionExtra(extra: Record<string, unknown>): DiscussionE
|
|
|
186
205
|
return {
|
|
187
206
|
state: state as DiscussionState,
|
|
188
207
|
roundCount,
|
|
189
|
-
...(typeof record
|
|
190
|
-
...(typeof record
|
|
191
|
-
...(typeof record
|
|
208
|
+
...(typeof record.deferredReason === "string" ? { deferredReason: record.deferredReason } : {}),
|
|
209
|
+
...(typeof record.settlement === "string" ? { settlement: record.settlement } : {}),
|
|
210
|
+
...(typeof record.settledAt === "string" ? { settledAt: record.settledAt } : {}),
|
|
192
211
|
...(pendingOptions !== undefined ? { pendingOptions: pendingOptions as string[] } : {}),
|
|
193
212
|
...(pendingOptionsMode !== undefined ? { pendingOptionsMode: pendingOptionsMode as DiscussionOptionsMode } : {}),
|
|
194
213
|
...(pendingOptionDescriptions !== undefined ? { pendingOptionDescriptions: pendingOptionDescriptions as string[] } : {}),
|
package/src/domain/gate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const GATE_TYPES = ["file-exists", "command", "contains", "test"] as const;
|
|
2
|
-
export type GateType = typeof GATE_TYPES[number];
|
|
2
|
+
export type GateType = (typeof GATE_TYPES)[number];
|
|
3
3
|
|
|
4
4
|
export interface Gate {
|
|
5
5
|
type: GateType;
|
|
@@ -20,19 +20,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
20
20
|
export function validateGates(value: unknown): Gate[] {
|
|
21
21
|
if (!Array.isArray(value)) throw new Error("gates must be an array");
|
|
22
22
|
return value.map((entry, index) => {
|
|
23
|
-
if (!isRecord(entry) || !GATE_TYPES.includes(entry
|
|
23
|
+
if (!isRecord(entry) || !GATE_TYPES.includes(entry.type as GateType)) {
|
|
24
24
|
throw new Error(`gate at index ${index} requires a valid type (${GATE_TYPES.join(", ")})`);
|
|
25
25
|
}
|
|
26
|
-
if (typeof entry
|
|
26
|
+
if (typeof entry.target !== "string" || entry.target.trim().length === 0) {
|
|
27
27
|
throw new Error(`gate at index ${index} requires a non-empty target`);
|
|
28
28
|
}
|
|
29
|
-
if (entry
|
|
29
|
+
if (entry.expect !== undefined && typeof entry.expect !== "string") {
|
|
30
30
|
throw new Error(`gate at index ${index} expect must be a string`);
|
|
31
31
|
}
|
|
32
32
|
return {
|
|
33
|
-
type: entry
|
|
34
|
-
target: entry
|
|
35
|
-
...(typeof entry
|
|
33
|
+
type: entry.type as GateType,
|
|
34
|
+
target: entry.target,
|
|
35
|
+
...(typeof entry.expect === "string" ? { expect: entry.expect } : {}),
|
|
36
36
|
};
|
|
37
37
|
});
|
|
38
38
|
}
|
package/src/domain/log-entry.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
export const LOG_LEVELS = ["debug", "info", "warning", "error"] as const;
|
|
21
|
-
export type LogLevel = typeof LOG_LEVELS[number];
|
|
21
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
22
22
|
|
|
23
23
|
export const LOG_SOURCE_ID_MAX_LENGTH = 256;
|
|
24
24
|
export const LOG_MESSAGE_MAX_CHARACTERS = 4000;
|
package/src/domain/note-event.ts
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
* previous extra.noteHistory: a bounded array inside a mutable JSON field, which a later
|
|
4
4
|
* setExtra from a concurrent operation could overwrite -- a real record can't do that.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
NOTE_HISTORY_DEFAULT_LIMIT,
|
|
8
|
+
NOTE_HISTORY_MAX_LIMIT,
|
|
9
|
+
NOTE_PROVENANCE_MAX_LENGTH,
|
|
10
|
+
NOTE_REASON_MAX_CHARACTERS,
|
|
11
|
+
} from "../constants.ts";
|
|
7
12
|
|
|
8
13
|
export const NOTE_EVENT_TYPES = ["captured", "consumed", "promoted", "archived"] as const;
|
|
9
|
-
export type NoteEventType = typeof NOTE_EVENT_TYPES[number];
|
|
14
|
+
export type NoteEventType = (typeof NOTE_EVENT_TYPES)[number];
|
|
10
15
|
export type NoteEventDirection = "asc" | "desc";
|
|
11
16
|
|
|
12
17
|
export interface NoteEvent {
|
|
@@ -47,7 +52,9 @@ export interface NoteHistoryPage {
|
|
|
47
52
|
nextCursor?: number;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
export function normalizeNoteHistoryQuery(
|
|
55
|
+
export function normalizeNoteHistoryQuery(
|
|
56
|
+
query: NoteHistoryQuery = {},
|
|
57
|
+
): Required<Pick<NoteHistoryQuery, "limit" | "direction">> & Pick<NoteHistoryQuery, "cursor"> {
|
|
51
58
|
const limit = query.limit ?? NOTE_HISTORY_DEFAULT_LIMIT;
|
|
52
59
|
if (!Number.isInteger(limit) || limit < 1 || limit > NOTE_HISTORY_MAX_LIMIT) {
|
|
53
60
|
throw new Error(`note history limit must be between 1 and ${NOTE_HISTORY_MAX_LIMIT}`);
|
|
@@ -62,10 +69,16 @@ export function normalizeNoteHistoryQuery(query: NoteHistoryQuery = {}): Require
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
export function validateNoteEvent(event: AppendNoteEvent): AppendNoteEvent {
|
|
65
|
-
for (const [field, value] of [
|
|
66
|
-
|
|
72
|
+
for (const [field, value] of [
|
|
73
|
+
["actor", event.actor],
|
|
74
|
+
["source", event.source],
|
|
75
|
+
] as const) {
|
|
76
|
+
if (!value || value.length > NOTE_PROVENANCE_MAX_LENGTH)
|
|
77
|
+
throw new Error(`${field} must be between 1 and ${NOTE_PROVENANCE_MAX_LENGTH} characters`);
|
|
67
78
|
}
|
|
68
|
-
if (event.sessionId !== undefined && event.sessionId.length > NOTE_PROVENANCE_MAX_LENGTH)
|
|
69
|
-
|
|
79
|
+
if (event.sessionId !== undefined && event.sessionId.length > NOTE_PROVENANCE_MAX_LENGTH)
|
|
80
|
+
throw new Error(`sessionId cannot exceed ${NOTE_PROVENANCE_MAX_LENGTH} characters`);
|
|
81
|
+
if (event.reason !== undefined && event.reason.length > NOTE_REASON_MAX_CHARACTERS)
|
|
82
|
+
throw new Error(`reason cannot exceed ${NOTE_REASON_MAX_CHARACTERS} characters`);
|
|
70
83
|
return event;
|
|
71
84
|
}
|
package/src/domain/task-event.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const TASK_EVENT_TYPES = [
|
|
|
33
33
|
"became_ready",
|
|
34
34
|
] as const;
|
|
35
35
|
|
|
36
|
-
export type TaskEventType = typeof TASK_EVENT_TYPES[number];
|
|
36
|
+
export type TaskEventType = (typeof TASK_EVENT_TYPES)[number];
|
|
37
37
|
export type TaskEventDirection = "asc" | "desc";
|
|
38
38
|
|
|
39
39
|
export interface TaskEventContext {
|
|
@@ -105,7 +105,9 @@ export interface TaskEventFeedPage {
|
|
|
105
105
|
nextCursor?: number;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function normalizeTaskEventFeedQuery(
|
|
108
|
+
export function normalizeTaskEventFeedQuery(
|
|
109
|
+
query: TaskEventFeedQuery = {},
|
|
110
|
+
): Required<Pick<TaskEventFeedQuery, "limit">> & Pick<TaskEventFeedQuery, "cursor" | "eventTypes"> {
|
|
109
111
|
const limit = query.limit ?? TASK_EVENT_FEED_DEFAULT_LIMIT;
|
|
110
112
|
if (!Number.isInteger(limit) || limit < 1 || limit > TASK_EVENT_FEED_MAX_LIMIT) {
|
|
111
113
|
throw new Error(`task event feed limit must be between 1 and ${TASK_EVENT_FEED_MAX_LIMIT}`);
|
|
@@ -122,7 +124,9 @@ export function normalizeTaskEventFeedQuery(query: TaskEventFeedQuery = {}): Req
|
|
|
122
124
|
return { limit, cursor: query.cursor, eventTypes: query.eventTypes };
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
export function normalizeTaskHistoryQuery(
|
|
127
|
+
export function normalizeTaskHistoryQuery(
|
|
128
|
+
query: TaskHistoryQuery = {},
|
|
129
|
+
): Required<Pick<TaskHistoryQuery, "limit" | "direction">> & Pick<TaskHistoryQuery, "cursor"> {
|
|
126
130
|
const limit = query.limit ?? TASK_HISTORY_DEFAULT_LIMIT;
|
|
127
131
|
if (!Number.isInteger(limit) || limit < 1 || limit > TASK_HISTORY_MAX_LIMIT) {
|
|
128
132
|
throw new Error(`task history limit must be between 1 and ${TASK_HISTORY_MAX_LIMIT}`);
|
|
@@ -137,11 +141,17 @@ export function normalizeTaskHistoryQuery(query: TaskHistoryQuery = {}): Require
|
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
export function validateTaskEvent(event: AppendTaskEvent): AppendTaskEvent {
|
|
140
|
-
for (const [field, value] of [
|
|
141
|
-
|
|
144
|
+
for (const [field, value] of [
|
|
145
|
+
["actor", event.actor],
|
|
146
|
+
["source", event.source],
|
|
147
|
+
] as const) {
|
|
148
|
+
if (!value || value.length > TASK_EVENT_ACTOR_MAX_LENGTH)
|
|
149
|
+
throw new Error(`${field} must be between 1 and ${TASK_EVENT_ACTOR_MAX_LENGTH} characters`);
|
|
142
150
|
}
|
|
143
|
-
if (event.sessionId !== undefined && event.sessionId.length > TASK_EVENT_ACTOR_MAX_LENGTH)
|
|
144
|
-
|
|
151
|
+
if (event.sessionId !== undefined && event.sessionId.length > TASK_EVENT_ACTOR_MAX_LENGTH)
|
|
152
|
+
throw new Error(`sessionId cannot exceed ${TASK_EVENT_ACTOR_MAX_LENGTH} characters`);
|
|
153
|
+
if (event.reason !== undefined && event.reason.length > TASK_EVENT_REASON_MAX_LENGTH)
|
|
154
|
+
throw new Error(`reason cannot exceed ${TASK_EVENT_REASON_MAX_LENGTH} characters`);
|
|
145
155
|
if (event.evidence !== undefined && new TextEncoder().encode(JSON.stringify(event.evidence)).byteLength > TASK_EVENT_MAX_EVIDENCE_BYTES) {
|
|
146
156
|
throw new Error(`task event evidence cannot exceed ${TASK_EVENT_MAX_EVIDENCE_BYTES} bytes`);
|
|
147
157
|
}
|