@danypops/papyrus 0.7.0 → 0.9.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/src/cli.ts CHANGED
@@ -10,7 +10,6 @@ import { serveMain } from "./daemon.ts";
10
10
  import type { GateResult } from "./domain/gate.ts";
11
11
  import type { TaskExecutionPlan } from "./task-execution.ts";
12
12
  import type { TaskBlockage, TaskCompletion } from "./task-service.ts";
13
- import type { TaskAutomationResult, TaskAutomationSettings } from "./task-automation.ts";
14
13
 
15
14
  export interface SystemdUnitOptions {
16
15
  bunBin: string;
@@ -57,23 +56,26 @@ function installService(): void {
57
56
  const USAGE = `Usage:
58
57
  papyrus serve
59
58
  papyrus service <install|start|stop|restart|status>
60
- papyrus migrate task-scope [--json]
61
- papyrus automation <status|run> [--json]
59
+ papyrus migrate task-focus [--json]
62
60
  papyrus skills run <id> [--arguments-json <json>] [--run-id <id>] [--json]
63
61
  papyrus tasks plan [--json]
64
62
  papyrus tasks graph [--json]
65
63
  papyrus tasks active [--json]
64
+ papyrus tasks focused [--json]
65
+ papyrus tasks pause [--json]
66
+ papyrus tasks unpause [--json]
67
+ papyrus tasks clear-focus [--json]
66
68
  papyrus tasks history <id> [--json]
67
69
  papyrus tasks scope [project|all|graph <root-id>] [--json]
68
70
  papyrus tasks assign-project <id> [project-root] [--json]
69
71
  papyrus tasks focus <id> [--json]
72
+ papyrus tasks update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
70
73
  papyrus tasks complete <id> [--json]
71
74
  papyrus tasks start <id> [--json]
72
75
  papyrus tasks submit <id> [--json]
73
76
  papyrus tasks reject <id> [--json]
74
77
  papyrus tasks retry <id> [--json]
75
78
  papyrus tasks cancel <id> [--json]
76
- papyrus tasks automate <id> <on|off> [--json]
77
79
  papyrus tasks depend <id> <prerequisite-id> [--json]`;
78
80
 
79
81
  function usage(): never {
@@ -112,8 +114,8 @@ function planText(plan: TaskExecutionPlan): string {
112
114
  export async function runMigrationCli(args: string[], client: TaskCliClient): Promise<string> {
113
115
  const json = args.includes("--json");
114
116
  const positional = args.filter((arg) => arg !== "--json");
115
- if (positional.length !== 1 || positional[0] !== "task-scope") {
116
- throw new Error("migrate requires exactly `task-scope`");
117
+ if (positional.length !== 1 || positional[0] !== "task-focus") {
118
+ throw new Error("migrate requires exactly `task-focus`");
117
119
  }
118
120
  const result = await client.call<Record<string, never>, MigrationResult>("system.migrate", {});
119
121
  if (json) return JSON.stringify(result);
@@ -121,20 +123,6 @@ export async function runMigrationCli(args: string[], client: TaskCliClient): Pr
121
123
  return `Migrated schema ${result.from} → ${result.to}: ${result.applied.join(", ")}`;
122
124
  }
123
125
 
124
- export async function runAutomationCli(args: string[], client: TaskCliClient): Promise<string> {
125
- const json = args.includes("--json");
126
- const positional = args.filter((argument) => argument !== "--json");
127
- if (positional.length !== 1 || (positional[0] !== "status" && positional[0] !== "run")) {
128
- throw new Error("automation requires exactly `status` or `run`");
129
- }
130
- if (positional[0] === "status") {
131
- const status = await client.call<Record<string, never>, TaskAutomationSettings & { inFlight: boolean }>("automation.status", {});
132
- return json ? JSON.stringify(status) : `Automation: ${status.enabled ? "enabled" : "disabled"} · interval ${status.intervalMs}ms · max ${status.maxTasksPerSweep} tasks · concurrency ${status.gateConcurrency}`;
133
- }
134
- const result = await client.call<Record<string, never>, TaskAutomationResult>("automation.reconcile", {});
135
- return json ? JSON.stringify(result) : `Automation sweep: ${result.examined} examined · ${result.completed} completed · ${result.rejected} rejected · ${result.started} started · ${result.errors.length} errors${result.skipped ? ` · skipped ${result.skipped}` : ""}`;
136
- }
137
-
138
126
  export async function runSkillCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
139
127
  const json = args.includes("--json");
140
128
  const positional: string[] = [];
@@ -182,7 +170,25 @@ export async function runSkillCli(args: string[], client: TaskCliClient, project
182
170
 
183
171
  export async function runTaskCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
184
172
  const json = args.includes("--json");
185
- const positional = args.filter((arg) => arg !== "--json");
173
+ const positional: string[] = [];
174
+ const updateInput: { title?: string; body?: string; labels?: string[] } = {};
175
+ for (let index = 0; index < args.length; index++) {
176
+ const argument = args[index]!;
177
+ if (argument === "--json") continue;
178
+ if (argument === "--title" || argument === "--body" || argument === "--labels-json") {
179
+ const value = args[++index];
180
+ if (value === undefined) throw new Error(`${argument} requires a value`);
181
+ if (argument === "--title") updateInput.title = value;
182
+ else if (argument === "--body") updateInput.body = value;
183
+ else {
184
+ const parsed = JSON.parse(value) as unknown;
185
+ if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("--labels-json requires a JSON string array");
186
+ updateInput.labels = parsed as string[];
187
+ }
188
+ continue;
189
+ }
190
+ positional.push(argument);
191
+ }
186
192
  const [action, id, dependencyId] = positional;
187
193
  let result: unknown;
188
194
  let human: string;
@@ -194,6 +200,37 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
194
200
  human = active ? `Active: ${artifactLabel(active)}` : "No active task.";
195
201
  break;
196
202
  }
203
+ case "focused": {
204
+ if (id) throw new Error("tasks focused accepts no positional arguments");
205
+ const focus = await client.call<Record<string, string>, { artifact: CliArtifact; status: "active" | "paused"; updatedAt: string } | null>("tasks.focused", { project_root: projectRoot });
206
+ result = focus;
207
+ human = focus ? `Focused (${focus.status}): ${artifactLabel(focus.artifact)}` : "No focused task.";
208
+ break;
209
+ }
210
+ case "pause":
211
+ case "unpause": {
212
+ if (id) throw new Error(`tasks ${action} accepts no positional arguments`);
213
+ const operation = action === "pause" ? "tasks.pause" : "tasks.unpause";
214
+ const focus = await client.call<Record<string, string>, { artifact: CliArtifact; status: string }>(operation, { actor: "user", source: "cli" });
215
+ result = focus;
216
+ human = `Focused (${focus.status}): ${artifactLabel(focus.artifact)}`;
217
+ break;
218
+ }
219
+ case "clear-focus": {
220
+ if (id) throw new Error("tasks clear-focus accepts no positional arguments");
221
+ const cleared = await client.call<Record<string, string>, { cleared: boolean }>("tasks.clear_focus", { actor: "user", source: "cli" });
222
+ result = cleared;
223
+ human = cleared.cleared ? "Task focus cleared." : "No focused task.";
224
+ break;
225
+ }
226
+ case "update": {
227
+ if (!id || dependencyId) throw new Error("tasks update requires exactly one task id");
228
+ if (Object.keys(updateInput).length === 0) throw new Error("tasks update requires --title, --body, or --labels-json");
229
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.update", { id, ...updateInput, actor: "user", source: "cli" });
230
+ result = artifact;
231
+ human = `Updated: ${artifactLabel(artifact)}`;
232
+ break;
233
+ }
197
234
  case "history": {
198
235
  if (!id || dependencyId) throw new Error("tasks history requires exactly one task id");
199
236
  const page = await client.call<{ id: string; direction: "desc" }, import("./domain/task-event.ts").TaskHistoryPage>("tasks.history", { id, direction: "desc" });
@@ -236,7 +273,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
236
273
  }
237
274
  case "focus": {
238
275
  if (!id || dependencyId) throw new Error("tasks focus requires exactly one task id");
239
- const active = await client.call<{ id: string }, CliArtifact>("tasks.focus", { id });
276
+ const active = await client.call<Record<string, string>, CliArtifact>("tasks.focus", { id, actor: "user", source: "cli" });
240
277
  result = active;
241
278
  human = `Active: ${artifactLabel(active)}`;
242
279
  break;
@@ -291,18 +328,6 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
291
328
  human = `${action[0]!.toUpperCase()}${action.slice(1)}: ${artifactLabel(artifact)}`;
292
329
  break;
293
330
  }
294
- case "automate": {
295
- if (!id || (dependencyId !== "on" && dependencyId !== "off") || positional.length !== 3) throw new Error("tasks automate requires a task id and on or off");
296
- const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.set_automation", {
297
- id,
298
- enabled: dependencyId === "on",
299
- actor: "user",
300
- source: "cli",
301
- });
302
- result = artifact;
303
- human = `Automation ${dependencyId}: ${artifactLabel(artifact)}`;
304
- break;
305
- }
306
331
  case "depend": {
307
332
  if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks depend requires a task id and prerequisite id");
308
333
  const artifact = await client.call<{ id: string; dependency_id: string }, CliArtifact>("tasks.depend", {
@@ -314,7 +339,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
314
339
  break;
315
340
  }
316
341
  default:
317
- throw new Error("tasks action must be active, focus, graph, plan, history, scope, assign-project, complete, start, submit, reject, retry, cancel, automate, or depend");
342
+ throw new Error("tasks action must be active, focused, focus, pause, unpause, clear-focus, update, graph, plan, history, scope, assign-project, complete, start, submit, reject, retry, cancel, or depend");
318
343
  }
319
344
  return json ? JSON.stringify(result) : human;
320
345
  }
@@ -327,11 +352,6 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
327
352
  console.log(await runTaskCli(args.slice(1), client));
328
353
  return;
329
354
  }
330
- if (command === "automation") {
331
- const client = await connectPapyrusClient();
332
- console.log(await runAutomationCli(args.slice(1), client));
333
- return;
334
- }
335
355
  if (command === "skills") {
336
356
  const client = await connectPapyrusClient();
337
357
  console.log(await runSkillCli(args.slice(1), client));
package/src/constants.ts CHANGED
@@ -7,7 +7,7 @@ export const DAEMON_PROBE_TIMEOUT_MS = 800;
7
7
  export const DAEMON_UNIT_NAME = "papyrus.service";
8
8
  export const DAEMON_DIR_ENV = "PAPYRUS_DAEMON_DIR";
9
9
  export const SQLITE_BUSY_TIMEOUT_MS = 5_000;
10
- export const SQLITE_SCHEMA_VERSION = 4;
10
+ export const SQLITE_SCHEMA_VERSION = 5;
11
11
  export const SERVICE_MAX_BODY_BYTES = 1_048_576;
12
12
  export const WAL_CHECKPOINT_INTERVAL_MS = 60_000;
13
13
  export const DB_OPTIMIZE_INTERVAL_MS = 24 * 60 * 60_000;
@@ -17,6 +17,10 @@ export const GATE_OUTPUT_LIMIT = 200;
17
17
  export const GATE_MAX_BUFFER_BYTES = 1_048_576;
18
18
  export const GATE_FILE_MAX_BYTES = 1_048_576;
19
19
 
20
+ export const PAPYRUS_CONTEXT_INJECTION_CHANNEL = "papyrus.context-injection.v1";
21
+ export const PAPYRUS_CONTEXT_INJECTION_SCHEMA = "papyrus.context-injection/v1";
22
+ export const CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN = 4;
23
+
20
24
  /** Compact task-context limits keep recurring prompt injection bounded. */
21
25
  export const TASK_CONTEXT_CURRENT_LIMIT = 3;
22
26
  export const TASK_CONTEXT_REJECTED_LIMIT = 3;
@@ -43,25 +47,17 @@ export const SKILL_RUN_ID_MAX_LENGTH = 64;
43
47
  /** Bounded automatic Pi continuations while a focused Papyrus Task remains. */
44
48
  export const TASK_DRIVER_MAX_TURNS = 20;
45
49
  export const TASK_DRIVER_MAX_UNCHANGED_TURNS = 6;
50
+ /** Mutable Task content bounds. */
51
+ export const TASK_TITLE_MAX_LENGTH = 500;
52
+ export const TASK_BODY_MAX_LENGTH = 100_000;
53
+ export const TASK_LABEL_MAX_COUNT = 64;
54
+ export const TASK_LABEL_MAX_LENGTH = 128;
46
55
  /** Append-only Task chronology query and evidence bounds. */
47
56
  export const TASK_HISTORY_DEFAULT_LIMIT = 25;
48
57
  export const TASK_HISTORY_MAX_LIMIT = 100;
49
58
  export const TASK_EVENT_MAX_EVIDENCE_BYTES = 65_536;
50
59
  export const TASK_EVENT_ACTOR_MAX_LENGTH = 128;
51
60
  export const TASK_EVENT_REASON_MAX_LENGTH = 2_000;
52
- /** Explicitly opt-in supervised Task graph reconciliation bounds. */
53
- export const TASK_AUTOMATION_INTERVAL_MS = 60_000;
54
- export const TASK_AUTOMATION_MIN_INTERVAL_MS = 10_000;
55
- export const TASK_AUTOMATION_MAX_INTERVAL_MS = 3_600_000;
56
- export const TASK_AUTOMATION_MAX_TASKS_PER_SWEEP = 10;
57
- export const TASK_AUTOMATION_HARD_MAX_TASKS_PER_SWEEP = 100;
58
- export const TASK_AUTOMATION_GATE_CONCURRENCY = 1;
59
- export const TASK_AUTOMATION_MAX_GATE_CONCURRENCY = 4;
60
- export const TASK_AUTOMATION_MAX_RUNTIME_MS = 120_000;
61
- export const TASK_AUTOMATION_HARD_MAX_RUNTIME_MS = 600_000;
62
- export const TASK_AUTOMATION_MAX_CANDIDATE_SCAN = 1_000;
63
- export const TASK_AUTOMATION_ERROR_ID_MAX_LENGTH = 128;
64
- export const TASK_AUTOMATION_ERROR_MESSAGE_MAX_LENGTH = 500;
65
61
  /** Persisted project and focused-graph Task view bounds. */
66
62
  export const TASK_SCOPE_MAX_TASKS = 1_000;
67
63
  export const TASK_PROJECT_ROOT_MAX_LENGTH = 4_096;
package/src/daemon.ts CHANGED
@@ -1,15 +1,13 @@
1
1
  import { DAEMON_HOST, DB_OPTIMIZE_INTERVAL_MS, WAL_CHECKPOINT_INTERVAL_MS, dbPath } from "./constants.ts";
2
2
  import { clearDaemonPort, daemonStateDir, loadOrCreateToken, writeDaemonPort } from "./daemon-state.ts";
3
3
  import { createApp, createPapyrusService } from "./service.ts";
4
- import { scheduleTaskAutomation, taskAutomationSettings, type TaskAutomationResult } from "./task-automation.ts";
5
4
  import { logEvent } from "./log.ts";
6
5
 
7
6
  /** Start the supervised, long-running Papyrus service. */
8
7
  export function serveMain(): void {
9
8
  const stateDir = daemonStateDir();
10
9
  const token = loadOrCreateToken(stateDir);
11
- const automation = taskAutomationSettings(process.env);
12
- const service = createPapyrusService(dbPath(), { automation });
10
+ const service = createPapyrusService(dbPath());
13
11
  const app = createApp({ service, token });
14
12
  const server = Bun.serve({
15
13
  hostname: DAEMON_HOST,
@@ -27,31 +25,17 @@ export function serveMain(): void {
27
25
  const optimizeTimer = setInterval(() => {
28
26
  try { service.optimize(); } catch (error) { logEvent("error", "optimize_failed", { message: error instanceof Error ? error.message : String(error) }); }
29
27
  }, DB_OPTIMIZE_INTERVAL_MS);
30
- const stopAutomation = scheduleTaskAutomation(automation, async () => {
31
- const result = await service.execute("automation.reconcile", {}) as TaskAutomationResult;
32
- logEvent(result.errors.length > 0 ? "warn" : "info", "automation_sweep", {
33
- examined: result.examined,
34
- completed: result.completed,
35
- rejected: result.rejected,
36
- started: result.started,
37
- errors: result.errors.length,
38
- timedOut: result.timedOut,
39
- skipped: result.skipped,
40
- });
41
- }, (error) => logEvent("error", "automation_sweep_failed", { message: error instanceof Error ? error.message : String(error) }));
42
-
43
28
  let stopping = false;
44
29
  const shutdown = () => {
45
30
  if (stopping) return;
46
31
  stopping = true;
47
32
  clearInterval(checkpointTimer);
48
33
  clearInterval(optimizeTimer);
49
- stopAutomation();
50
34
  clearDaemonPort(stateDir);
51
35
  service.close();
52
36
  void server.stop(true).finally(() => process.exit(0));
53
37
  };
54
38
  process.on("SIGINT", shutdown);
55
39
  process.on("SIGTERM", shutdown);
56
- logEvent("info", "listening", { host: DAEMON_HOST, port: server.port, automationEnabled: automation.enabled });
40
+ logEvent("info", "listening", { host: DAEMON_HOST, port: server.port });
57
41
  }
package/src/db.ts CHANGED
@@ -100,6 +100,8 @@ CREATE TABLE IF NOT EXISTS relation_names (
100
100
  CREATE TABLE IF NOT EXISTS task_focus (
101
101
  scope TEXT PRIMARY KEY CHECK (scope = 'global'),
102
102
  task_id TEXT NOT NULL UNIQUE REFERENCES artifacts(id),
103
+ status TEXT NOT NULL CHECK (status IN ('active', 'paused')),
104
+ pause_reason TEXT,
103
105
  updated_at TEXT NOT NULL
104
106
  );
105
107
  CREATE TABLE IF NOT EXISTS task_events (
@@ -140,7 +142,7 @@ CREATE TABLE IF NOT EXISTS task_views (
140
142
 
141
143
  const SEED_SQL = `
142
144
  INSERT OR IGNORE INTO kinds VALUES ('doc','Knowledge — what we know (specs, decisions, research, designs)');
143
- INSERT OR IGNORE INTO kinds VALUES ('task','Work — what we are doing (goals, steps, checklists)');
145
+ INSERT OR IGNORE INTO kinds VALUES ('task','Work — what we are doing (objectives, steps, checklists)');
144
146
  INSERT OR IGNORE INTO kinds VALUES ('rule','Governance — when doing X, follow Y');
145
147
  INSERT OR IGNORE INTO kinds VALUES ('skill','Parameterized workflow bundle — inputs and templates load tasks, rules, and docs');
146
148
  INSERT OR IGNORE INTO statuses VALUES ('draft','doc');
@@ -199,7 +201,7 @@ export function migrateDb(db: Db): MigrationResult {
199
201
  throw new Error(`database schema ${from} is newer than supported ${SQLITE_SCHEMA_VERSION}`);
200
202
  }
201
203
  if (from === SQLITE_SCHEMA_VERSION) return { from, to: from, applied: [] };
202
- if (from !== 1 && from !== 2 && from !== 3) throw new Error(`no explicit migration path from database schema ${from}`);
204
+ if (from !== 1 && from !== 2 && from !== 3 && from !== 4) throw new Error(`no explicit migration path from database schema ${from}`);
203
205
  const applied: string[] = [];
204
206
 
205
207
  inTransaction(db, () => {
@@ -280,6 +282,14 @@ export function migrateDb(db: Db): MigrationResult {
280
282
  `);
281
283
  applied.push("task-project-scope");
282
284
  }
285
+ if (schemaVersion(db) === 4) {
286
+ db.exec(`
287
+ ALTER TABLE task_focus ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'paused'));
288
+ ALTER TABLE task_focus ADD COLUMN pause_reason TEXT;
289
+ PRAGMA user_version = 5;
290
+ `);
291
+ applied.push("task-focus-continuation");
292
+ }
283
293
  });
284
294
  return { from, to: schemaVersion(db), applied };
285
295
  }
@@ -30,6 +30,12 @@ export interface CreateArtifactInput {
30
30
  templateId?: string;
31
31
  }
32
32
 
33
+ export interface UpdateArtifactInput {
34
+ title?: string;
35
+ body?: string;
36
+ labels?: string[];
37
+ }
38
+
33
39
  export interface ArtifactQuery {
34
40
  kind?: string;
35
41
  status?: string;
@@ -9,12 +9,15 @@ export type TaskLifecycleStatus = "todo" | "in-progress" | "review" | "rejected"
9
9
 
10
10
  export const TASK_EVENT_TYPES = [
11
11
  "created",
12
+ "updated",
12
13
  "started",
13
14
  "submitted",
14
15
  "completion_attempted",
15
16
  "gates_evaluated",
16
- "automation_enabled",
17
- "automation_disabled",
17
+ "focus_set",
18
+ "focus_paused",
19
+ "focus_unpaused",
20
+ "focus_cleared",
18
21
  "project_assigned",
19
22
  "review_rejected",
20
23
  "retried",
package/src/ops.ts CHANGED
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
6
6
  import { exec } from "node:child_process";
7
7
  import type { Db } from "./db.ts";
8
8
  import { inTransaction } from "./db.ts";
9
- import type { Artifact, CreateArtifactInput } from "./domain/artifact.ts";
9
+ import type { Artifact, CreateArtifactInput, UpdateArtifactInput } from "./domain/artifact.ts";
10
10
  import type { Gate, GateResult, GateRunOptions } from "./domain/gate.ts";
11
11
  export type { Artifact } from "./domain/artifact.ts";
12
12
  export type { Gate, GateResult } from "./domain/gate.ts";
@@ -204,6 +204,22 @@ export function linkArtifacts(db: Db, fromId: string, relation: string, toId: st
204
204
  });
205
205
  }
206
206
 
207
+ export function updateArtifactContent(db: Db, id: string, input: UpdateArtifactInput): Artifact | null {
208
+ const artifact = getArtifact(db, id);
209
+ if (!artifact) return null;
210
+ const now = new Date().toISOString();
211
+ inTransaction(db, () => {
212
+ db.prepare("UPDATE artifacts SET title = ?, body = ?, labels = ?, updated_at = ? WHERE id = ?").run(
213
+ input.title ?? artifact.title,
214
+ input.body ?? artifact.body,
215
+ JSON.stringify(input.labels ?? artifact.labels),
216
+ now,
217
+ id,
218
+ );
219
+ });
220
+ return getArtifact(db, id);
221
+ }
222
+
207
223
  export function updateStatus(db: Db, id: string, status: string): Artifact | null {
208
224
  const art = getArtifact(db, id);
209
225
  if (!art) return null;
@@ -6,6 +6,7 @@ import type {
6
6
  ArtifactQuery,
7
7
  CreateArtifactInput,
8
8
  RelationshipQuery,
9
+ UpdateArtifactInput,
9
10
  } from "../domain/artifact.ts";
10
11
 
11
12
  export interface ArtifactStore {
@@ -15,5 +16,6 @@ export interface ArtifactStore {
15
16
  link(link: ArtifactLink): void;
16
17
  setStatus(id: string, status: string): Artifact | null;
17
18
  setExtra(id: string, extra: Record<string, unknown>): Artifact | null;
19
+ updateContent(id: string, input: UpdateArtifactInput): Artifact | null;
18
20
  relationships(filter?: RelationshipQuery): ArtifactEdge[];
19
21
  }
@@ -1,21 +1,43 @@
1
+ export type TaskFocusStatus = "active" | "paused";
2
+
3
+ export interface TaskFocusState {
4
+ taskId: string;
5
+ status: TaskFocusStatus;
6
+ updatedAt: string;
7
+ pauseReason?: string;
8
+ }
9
+
1
10
  export interface TaskFocusStore {
2
- get(): string | undefined;
3
- set(taskId: string): void;
11
+ get(): TaskFocusState | undefined;
12
+ set(taskId: string): TaskFocusState;
13
+ pause(taskId: string, reason?: string): TaskFocusState;
14
+ unpause(taskId: string): TaskFocusState;
4
15
  clear(taskId?: string): void;
5
16
  }
6
17
 
7
18
  export class InMemoryTaskFocusStore implements TaskFocusStore {
8
- private taskId: string | undefined;
19
+ private state: TaskFocusState | undefined;
20
+
21
+ get(): TaskFocusState | undefined { return this.state; }
22
+
23
+ set(taskId: string): TaskFocusState {
24
+ this.state = { taskId, status: "active", updatedAt: new Date().toISOString() };
25
+ return this.state;
26
+ }
9
27
 
10
- get(): string | undefined {
11
- return this.taskId;
28
+ pause(taskId: string, reason?: string): TaskFocusState {
29
+ if (this.state?.taskId !== taskId) throw new Error(`task "${taskId}" is not focused`);
30
+ this.state = { ...this.state, status: "paused", updatedAt: new Date().toISOString(), ...(reason ? { pauseReason: reason } : {}) };
31
+ return this.state;
12
32
  }
13
33
 
14
- set(taskId: string): void {
15
- this.taskId = taskId;
34
+ unpause(taskId: string): TaskFocusState {
35
+ if (this.state?.taskId !== taskId) throw new Error(`task "${taskId}" is not focused`);
36
+ this.state = { taskId, status: "active", updatedAt: new Date().toISOString() };
37
+ return this.state;
16
38
  }
17
39
 
18
40
  clear(taskId?: string): void {
19
- if (taskId === undefined || taskId === this.taskId) this.taskId = undefined;
41
+ if (taskId === undefined || this.state?.taskId === taskId) this.state = undefined;
20
42
  }
21
43
  }
package/src/service.ts CHANGED
@@ -16,7 +16,6 @@ import type { TaskEventStore } from "./ports/task-event-store.ts";
16
16
  import type { TaskScopeStore } from "./ports/task-scope-store.ts";
17
17
  import { projectTaskExecution } from "./task-execution.ts";
18
18
  import { Tasks, type TaskStatus } from "./task-service.ts";
19
- import { TaskAutomationReconciler, taskAutomationSettings, type TaskAutomationSettings } from "./task-automation.ts";
20
19
  import {
21
20
  createArtifactTemplate,
22
21
  createDocument,
@@ -44,8 +43,6 @@ import { instantiateSkillWorkflow } from "./skill-execution.ts";
44
43
 
45
44
  export const EXPECTED_OPERATION_NAMES = [
46
45
  "system.migrate",
47
- "automation.status",
48
- "automation.reconcile",
49
46
  "artifact.create",
50
47
  "artifact.query",
51
48
  "artifact.show",
@@ -55,6 +52,7 @@ export const EXPECTED_OPERATION_NAMES = [
55
52
  "gates.run",
56
53
  "rules.injectable",
57
54
  "tasks.create",
55
+ "tasks.update",
58
56
  "tasks.list",
59
57
  "tasks.graph",
60
58
  "tasks.plan",
@@ -64,13 +62,16 @@ export const EXPECTED_OPERATION_NAMES = [
64
62
  "tasks.set_scope",
65
63
  "tasks.assign_project",
66
64
  "tasks.active",
65
+ "tasks.focused",
67
66
  "tasks.focus",
67
+ "tasks.pause",
68
+ "tasks.unpause",
69
+ "tasks.clear_focus",
68
70
  "tasks.start",
69
71
  "tasks.submit",
70
72
  "tasks.complete",
71
73
  "tasks.run_gates",
72
74
  "tasks.set_checklist",
73
- "tasks.set_automation",
74
75
  "tasks.context",
75
76
  "tasks.reject",
76
77
  "tasks.retry",
@@ -123,6 +124,13 @@ function optionalString(input: OperationInput, key: string): string | undefined
123
124
  return value;
124
125
  }
125
126
 
127
+ function optionalStringArray(input: OperationInput, key: string): string[] | undefined {
128
+ const value = input[key];
129
+ if (value === undefined) return undefined;
130
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${key} must be an array of strings`);
131
+ return value as string[];
132
+ }
133
+
126
134
  function optionalNumber(input: OperationInput, key: string): number | undefined {
127
135
  const value = input[key];
128
136
  if (value === undefined) return undefined;
@@ -154,7 +162,6 @@ function handlers(
154
162
  artifacts: ArtifactStore,
155
163
  gates: GateRunner,
156
164
  tasks: Tasks,
157
- automation: TaskAutomationReconciler,
158
165
  events: TaskEventStore,
159
166
  scopes: TaskScopeStore,
160
167
  migrate: () => unknown,
@@ -169,18 +176,19 @@ function handlers(
169
176
  const context = eventContext(input);
170
177
  return { ...context, source: context.source ?? source };
171
178
  };
172
- const taskFilter = (input: OperationInput) => ({
179
+ const artifactFilter = (input: OperationInput) => ({
173
180
  status: optionalString(input, "status"),
174
181
  text: optionalString(input, "text"),
175
182
  limit: optionalNumber(input, "limit"),
183
+ });
184
+ const taskFilter = (input: OperationInput) => ({
185
+ ...artifactFilter(input),
176
186
  projectRoot: string(input, "project_root"),
177
187
  scope: optionalString(input, "scope") as TaskViewMode | undefined,
178
188
  rootTaskId: optionalString(input, "root_task_id"),
179
189
  });
180
190
  return {
181
191
  "system.migrate": () => migrate(),
182
- "automation.status": () => automation.status(),
183
- "automation.reconcile": () => automation.reconcile(),
184
192
  "artifact.create": (input) => {
185
193
  const normalized = normalizeCreateInput(input);
186
194
  if (normalized.kind !== "task") return artifacts.create(normalized);
@@ -246,6 +254,11 @@ function handlers(
246
254
  projectRoot: string(input, "project_root"),
247
255
  projectSource: "cwd",
248
256
  }, eventContext(input)),
257
+ "tasks.update": (input) => tasks.update(string(input, "id"), {
258
+ ...(input["title"] !== undefined ? { title: optionalString(input, "title")! } : {}),
259
+ ...(input["body"] !== undefined ? { body: optionalString(input, "body")! } : {}),
260
+ ...(input["labels"] !== undefined ? { labels: optionalStringArray(input, "labels")! } : {}),
261
+ }, eventContext(input)),
249
262
  "tasks.list": (input) => tasks.list(taskFilter(input)),
250
263
  "tasks.graph": (input) => tasks.graph(taskFilter(input)),
251
264
  "tasks.plan": (input) => projectTaskExecution(tasks.graph(taskFilter(input))),
@@ -267,16 +280,16 @@ function handlers(
267
280
  eventContext(input),
268
281
  ),
269
282
  "tasks.active": (input) => tasks.active(taskFilter(input)),
270
- "tasks.focus": (input) => tasks.focus(string(input, "id")),
283
+ "tasks.focused": (input) => tasks.focused(taskFilter(input)),
284
+ "tasks.focus": (input) => tasks.focus(string(input, "id"), eventContext(input)),
285
+ "tasks.pause": (input) => tasks.pauseFocus(eventContext(input)),
286
+ "tasks.unpause": (input) => tasks.unpauseFocus(eventContext(input)),
287
+ "tasks.clear_focus": (input) => tasks.clearFocus(eventContext(input)),
271
288
  "tasks.start": (input) => tasks.transition(string(input, "id"), "start", eventContext(input)),
272
289
  "tasks.submit": (input) => tasks.transition(string(input, "id"), "submit", eventContext(input)),
273
290
  "tasks.complete": (input) => tasks.completeAsync(string(input, "id"), eventContext(input)),
274
291
  "tasks.run_gates": (input) => tasks.runGates(string(input, "id"), eventContext(input)),
275
292
  "tasks.set_checklist": (input) => tasks.setChecklist(string(input, "id"), input["checklist"] as Checklist),
276
- "tasks.set_automation": (input) => {
277
- if (typeof input["enabled"] !== "boolean") throw new Error("enabled must be a boolean");
278
- return tasks.setAutomation(string(input, "id"), input["enabled"], eventContext(input));
279
- },
280
293
  "tasks.context": (input) => taskContext(artifacts, tasks.active()?.id, new Set(tasks.list(taskFilter(input)).map((task) => task.id))),
281
294
  "tasks.reject": (input) => tasks.transition(string(input, "id"), "reject", eventContext(input)),
282
295
  "tasks.retry": (input) => tasks.transition(string(input, "id"), "retry", eventContext(input)),
@@ -288,7 +301,7 @@ function handlers(
288
301
  labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
289
302
  templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
290
303
  }),
291
- "docs.list": (input) => listDocuments(artifacts, taskFilter(input)),
304
+ "docs.list": (input) => listDocuments(artifacts, artifactFilter(input)),
292
305
  "docs.show": (input) => showDocument(artifacts, string(input, "id")),
293
306
  "docs.activate": (input) => transitionDocument(artifacts, string(input, "id"), "activate"),
294
307
  "docs.archive": (input) => transitionDocument(artifacts, string(input, "id"), "archive"),
@@ -300,7 +313,7 @@ function handlers(
300
313
  severity: optionalString(input, "severity") as "block" | "warn" | "info" | undefined,
301
314
  labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
302
315
  }),
303
- "rules.list": (input) => listRules(artifacts, taskFilter(input)),
316
+ "rules.list": (input) => listRules(artifacts, artifactFilter(input)),
304
317
  "rules.show": (input) => showRule(artifacts, string(input, "id")),
305
318
  "rules.preview": (input) => previewRule(artifacts, string(input, "id")),
306
319
  "rules.enable": (input) => transitionRule(artifacts, string(input, "id"), "enable"),
@@ -316,7 +329,7 @@ function handlers(
316
329
  title: string(input, "title"), targetKind: string(input, "target_kind"), defaults: input["defaults"] as Record<string, unknown> | undefined,
317
330
  required: input["required"] as string[] | undefined, body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
318
331
  }),
319
- "skills.list": (input) => listSkills(artifacts, taskFilter(input)),
332
+ "skills.list": (input) => listSkills(artifacts, artifactFilter(input)),
320
333
  "skills.show": (input) => showSkill(artifacts, string(input, "id")),
321
334
  "skills.invoke": (input) => skillInvocation(artifacts, string(input, "id")),
322
335
  "skills.run": (input) => instantiateSkillWorkflow(artifacts, string(input, "id"), {
@@ -343,7 +356,7 @@ function handlers(
343
356
  };
344
357
  }
345
358
 
346
- export function createPapyrusService(path: string, options: { automation?: TaskAutomationSettings } = {}): PapyrusService {
359
+ export function createPapyrusService(path: string): PapyrusService {
347
360
  const db = openDb(path);
348
361
  const artifacts = new SQLiteArtifactStore(db);
349
362
  const gates = new SQLiteGateRunner(db);
@@ -351,8 +364,7 @@ export function createPapyrusService(path: string, options: { automation?: TaskA
351
364
  const events = new SQLiteTaskEventStore(db);
352
365
  const scopes = new SQLiteTaskScopeStore(db);
353
366
  const tasks = new Tasks(artifacts, gates, focus, events, scopes);
354
- const automation = new TaskAutomationReconciler(tasks, options.automation ?? taskAutomationSettings({}));
355
- const registry = handlers(artifacts, gates, tasks, automation, events, scopes, () => migrateDb(db));
367
+ const registry = handlers(artifacts, gates, tasks, events, scopes, () => migrateDb(db));
356
368
  const state = (): SchemaState => {
357
369
  const current = schemaVersion(db);
358
370
  return { current, required: SQLITE_SCHEMA_VERSION, migrationRequired: current !== SQLITE_SCHEMA_VERSION };
@@ -363,8 +375,8 @@ export function createPapyrusService(path: string, options: { automation?: TaskA
363
375
  async execute(operation, input = {}) {
364
376
  const handler = registry[operation as OperationName];
365
377
  if (!handler) throw new UnknownOperationError(`unknown operation "${operation}"`);
366
- if (operation !== "system.migrate" && operation !== "automation.status" && state().migrationRequired) {
367
- throw new MigrationRequiredError("database migration required; run `papyrus migrate task-scope`");
378
+ if (operation !== "system.migrate" && state().migrationRequired) {
379
+ throw new MigrationRequiredError("database migration required; run `papyrus migrate task-focus`");
368
380
  }
369
381
  return handler(input);
370
382
  },