@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.
Files changed (78) hide show
  1. package/README.md +8 -11
  2. package/package.json +2 -2
  3. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  4. package/src/adapters/sqlite-artifact-store.ts +7 -5
  5. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  6. package/src/adapters/sqlite-gate-runner.ts +1 -1
  7. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  8. package/src/adapters/sqlite-log-store.ts +36 -17
  9. package/src/adapters/sqlite-note-event-store.ts +20 -16
  10. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  11. package/src/adapters/sqlite-task-event-store.ts +29 -21
  12. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  13. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  14. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  15. package/src/artifact-relationship-view.ts +4 -4
  16. package/src/artifact-subtree.ts +4 -2
  17. package/src/authority-registry.ts +2 -1
  18. package/src/cli.ts +794 -354
  19. package/src/client.ts +6 -3
  20. package/src/constants.ts +34 -56
  21. package/src/daemon-state.ts +4 -12
  22. package/src/daemon.ts +31 -9
  23. package/src/db.ts +153 -105
  24. package/src/discussion-service.ts +109 -44
  25. package/src/domain/artifact-event.ts +18 -5
  26. package/src/domain/artifact.ts +3 -1
  27. package/src/domain/blueprint-definition.ts +268 -0
  28. package/src/domain/checklist.ts +20 -17
  29. package/src/domain/discussion.ts +37 -18
  30. package/src/domain/gate.ts +7 -7
  31. package/src/domain/log-entry.ts +1 -1
  32. package/src/domain/note-event.ts +20 -7
  33. package/src/domain/task-event.ts +17 -7
  34. package/src/domain-services.ts +362 -288
  35. package/src/graph-projection-service.ts +34 -8
  36. package/src/id-migration.ts +17 -4
  37. package/src/index.ts +16 -11
  38. package/src/log-service.ts +6 -5
  39. package/src/log.ts +19 -0
  40. package/src/modules/discuss.ts +63 -28
  41. package/src/modules/docs.ts +74 -17
  42. package/src/modules/graph-projection.ts +20 -9
  43. package/src/modules/logs.ts +34 -22
  44. package/src/modules/notes.ts +66 -28
  45. package/src/modules/playbooks.ts +93 -32
  46. package/src/modules/rules.ts +57 -15
  47. package/src/modules/session-identity.ts +6 -2
  48. package/src/modules/tasks.ts +142 -67
  49. package/src/note-service.ts +11 -7
  50. package/src/ops.ts +134 -69
  51. package/src/playbook-definition.ts +124 -39
  52. package/src/playbook-execution.ts +18 -25
  53. package/src/ports/artifact-scope-store.ts +1 -1
  54. package/src/ports/note-event-store.ts +6 -4
  55. package/src/ports/task-event-store.ts +9 -6
  56. package/src/ports/task-focus-store.ts +17 -4
  57. package/src/ports/task-lease-store.ts +9 -4
  58. package/src/ports/task-scope-store.ts +3 -1
  59. package/src/service.ts +148 -110
  60. package/src/session-identity-service.ts +10 -2
  61. package/src/task-context.ts +28 -16
  62. package/src/task-execution.ts +4 -12
  63. package/src/task-graph-view.ts +12 -12
  64. package/src/task-relationship-view.ts +1 -3
  65. package/src/task-service.ts +168 -73
  66. package/src/vehicle/artifact-trash-vehicle.ts +26 -14
  67. package/src/vehicle/artifact-vehicle-shared.ts +32 -13
  68. package/src/vehicle/docs-vehicle.ts +50 -18
  69. package/src/vehicle/notes-vehicle.ts +26 -8
  70. package/src/vehicle/papyrus-vehicle.ts +16 -8
  71. package/src/vehicle/playbooks-vehicle.ts +88 -19
  72. package/src/vehicle/rules-vehicle.ts +58 -21
  73. package/src/vehicle/tasks-vehicle.ts +366 -54
  74. package/src/version.ts +1 -1
  75. package/src/workflow-execution.ts +198 -109
  76. package/src/domain/skill-definition.ts +0 -270
  77. package/src/modules/skills.ts +0 -158
  78. package/src/vehicle/skills-vehicle.ts +0 -194
@@ -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(options: string[], mode: string, optionDescriptions?: string[]): { options: string[]; mode: DiscussionOptionsMode; optionDescriptions?: string[] } {
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 (descriptionsRequired && (optionDescriptions === undefined || optionDescriptions.some((description) => description.trim().length === 0))) {
127
- throw new Error(`option_descriptions is required, with a non-empty entry for every option, once ${DISCUSSION_OPTION_DESCRIPTION_REQUIRED_FROM_COUNT} or more options are posed`);
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) throw new Error("option_descriptions must have exactly one entry per option (use an empty string for none)");
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) throw new Error(`option description must be at most ${DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH} characters`);
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 { options: [...options], mode: mode as DiscussionOptionsMode, ...(optionDescriptions !== undefined ? { optionDescriptions: [...optionDescriptions] } : {}) };
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(selected: string[], pendingOptions: string[] | undefined, pendingMode: DiscussionOptionsMode | undefined): string[] {
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["discussion"];
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["state"];
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["roundCount"];
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["pendingOptions"];
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["pendingOptionsMode"];
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["pendingOptionDescriptions"];
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["deferredReason"] === "string" ? { deferredReason: record["deferredReason"] } : {}),
190
- ...(typeof record["settlement"] === "string" ? { settlement: record["settlement"] } : {}),
191
- ...(typeof record["settledAt"] === "string" ? { settledAt: record["settledAt"] } : {}),
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[] } : {}),
@@ -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["type"] as GateType)) {
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["target"] !== "string" || entry["target"].trim().length === 0) {
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["expect"] !== undefined && typeof entry["expect"] !== "string") {
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["type"] as GateType,
34
- target: entry["target"],
35
- ...(typeof entry["expect"] === "string" ? { expect: entry["expect"] } : {}),
33
+ type: entry.type as GateType,
34
+ target: entry.target,
35
+ ...(typeof entry.expect === "string" ? { expect: entry.expect } : {}),
36
36
  };
37
37
  });
38
38
  }
@@ -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;
@@ -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 { NOTE_HISTORY_DEFAULT_LIMIT, NOTE_HISTORY_MAX_LIMIT, NOTE_PROVENANCE_MAX_LENGTH, NOTE_REASON_MAX_CHARACTERS } from "../constants.ts";
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(query: NoteHistoryQuery = {}): Required<Pick<NoteHistoryQuery, "limit" | "direction">> & Pick<NoteHistoryQuery, "cursor"> {
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 [["actor", event.actor], ["source", event.source]] as const) {
66
- if (!value || value.length > NOTE_PROVENANCE_MAX_LENGTH) throw new Error(`${field} must be between 1 and ${NOTE_PROVENANCE_MAX_LENGTH} characters`);
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) throw new Error(`sessionId cannot exceed ${NOTE_PROVENANCE_MAX_LENGTH} characters`);
69
- if (event.reason !== undefined && event.reason.length > NOTE_REASON_MAX_CHARACTERS) throw new Error(`reason cannot exceed ${NOTE_REASON_MAX_CHARACTERS} characters`);
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
  }
@@ -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(query: TaskEventFeedQuery = {}): Required<Pick<TaskEventFeedQuery, "limit">> & Pick<TaskEventFeedQuery, "cursor" | "eventTypes"> {
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(query: TaskHistoryQuery = {}): Required<Pick<TaskHistoryQuery, "limit" | "direction">> & Pick<TaskHistoryQuery, "cursor"> {
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 [["actor", event.actor], ["source", event.source]] as const) {
141
- if (!value || value.length > TASK_EVENT_ACTOR_MAX_LENGTH) throw new Error(`${field} must be between 1 and ${TASK_EVENT_ACTOR_MAX_LENGTH} characters`);
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) throw new Error(`sessionId cannot exceed ${TASK_EVENT_ACTOR_MAX_LENGTH} characters`);
144
- if (event.reason !== undefined && event.reason.length > TASK_EVENT_REASON_MAX_LENGTH) throw new Error(`reason cannot exceed ${TASK_EVENT_REASON_MAX_LENGTH} characters`);
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
  }