pi-feats 0.1.4 → 0.1.7

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 (60) hide show
  1. package/README.md +90 -9
  2. package/extensions/api-server/README.md +21 -1
  3. package/extensions/api-server/application-log-store.ts +4 -2
  4. package/extensions/api-server/application-runtime.ts +79 -8
  5. package/extensions/api-server/application-store.ts +23 -10
  6. package/extensions/api-server/index.ts +6 -2
  7. package/extensions/api-server/profile-store.ts +109 -39
  8. package/extensions/api-server/server.ts +220 -44
  9. package/extensions/cli-resources.ts +144 -113
  10. package/extensions/cli-tui-mode.ts +7 -0
  11. package/extensions/context-memory/index.ts +6 -1
  12. package/extensions/conversation-search/index.ts +42 -0
  13. package/extensions/guardrails/index.ts +2 -1
  14. package/extensions/lib/application-context.ts +2 -0
  15. package/extensions/lib/builtin-tools.ts +24 -0
  16. package/extensions/lib/cli-tui-mode.ts +72 -0
  17. package/extensions/lib/context-memory.ts +6 -1
  18. package/extensions/lib/conversation-search.ts +33 -0
  19. package/extensions/lib/package-resources.ts +58 -0
  20. package/extensions/lib/pi-command.ts +11 -0
  21. package/extensions/lib/profile-sandbox.ts +14 -3
  22. package/extensions/lib/remote-hosts.ts +7 -5
  23. package/extensions/lib/workflow-evaluator.ts +113 -0
  24. package/extensions/lib/workflow-harness.ts +409 -43
  25. package/extensions/pi-console-webui/app/[section]/page.tsx +1 -1
  26. package/extensions/pi-console-webui/app/api/admin/models/route.ts +22 -0
  27. package/extensions/pi-console-webui/app/globals.css +3 -0
  28. package/extensions/pi-console-webui/components/application-handler-editor.tsx +1 -1
  29. package/extensions/pi-console-webui/components/application-logs.tsx +4 -4
  30. package/extensions/pi-console-webui/components/application-mappings.tsx +10 -12
  31. package/extensions/pi-console-webui/components/application-sessions.tsx +2 -2
  32. package/extensions/pi-console-webui/components/application-settings.tsx +12 -6
  33. package/extensions/pi-console-webui/components/chat-workspace.tsx +17 -7
  34. package/extensions/pi-console-webui/components/console-page.tsx +4 -0
  35. package/extensions/pi-console-webui/components/console-state.tsx +1 -1
  36. package/extensions/pi-console-webui/components/console.tsx +32 -11
  37. package/extensions/pi-console-webui/components/guardrails-panel.tsx +2 -2
  38. package/extensions/pi-console-webui/components/models-config-form.tsx +31 -0
  39. package/extensions/pi-console-webui/components/profile-model-form.tsx +54 -0
  40. package/extensions/pi-console-webui/components/pulse-resources.tsx +7 -6
  41. package/extensions/pi-console-webui/components/sequential-workflow-config.tsx +32 -0
  42. package/extensions/pi-console-webui/components/skill-resources.tsx +4 -3
  43. package/extensions/pi-console-webui/components/ui/input.tsx +1 -1
  44. package/extensions/pi-console-webui/index.ts +2 -1
  45. package/extensions/pi-console-webui/lib/admin-models.ts +38 -0
  46. package/extensions/pi-console-webui/lib/page-metadata.ts +4 -1
  47. package/extensions/pi-console-webui/next.config.js +1 -0
  48. package/extensions/profile-ssh.ts +274 -0
  49. package/extensions/profiles.ts +69 -25
  50. package/extensions/pulse/index.ts +148 -16
  51. package/extensions/pulse/store.ts +132 -18
  52. package/extensions/sequential-workflow.ts +121 -79
  53. package/extensions/skill-sources/store.ts +29 -10
  54. package/package.json +21 -9
  55. package/scripts/build-console-webui.mjs +7 -0
  56. package/scripts/console-webui-cache.mjs +79 -0
  57. package/scripts/ensure-console-build.mjs +43 -0
  58. package/skills/profile-ssh/SKILL.md +65 -0
  59. package/skills/sequential-workflow/SKILL.md +4 -3
  60. package/themes/legacy-dark.json +85 -0
@@ -9,7 +9,7 @@ import { isAbsolute, join, resolve } from "node:path";
9
9
  import { createHash } from "node:crypto";
10
10
  type TaskType = "action" | "collect" | "evaluate" | "workflow";
11
11
  type TaskStatus = "pending" | "running" | "awaiting_user" | "waiting_subworkflow" | "evaluating" | "accepted" | "rejected" | "failed";
12
- type WorkflowStatus = "running" | "awaiting_user" | "evaluating" | "completed" | "cancelled" | "failed";
12
+ type WorkflowStatus = "pending_definition" | "running" | "awaiting_user" | "evaluating" | "completed" | "cancelled" | "failed";
13
13
  type Task = {
14
14
  id: number;
15
15
  workflow_id: number;
@@ -48,43 +48,45 @@ const templateMaxTasks = 1000;
48
48
  const templateMaxTitleLength = 1000;
49
49
  const templateMaxSourceLength = 2000;
50
50
  const templateMaxTextLength = 10000;
51
+ const maxActionAttempts = 5;
51
52
  const taskType = StringEnum(["action", "collect", "evaluate", "workflow"] as const);
52
53
  const phaseType = StringEnum(["action", "collect"] as const);
54
+ const evaluationOutcome = StringEnum(["accept", "retry", "fail"] as const);
53
55
  const workflowRoot = () => process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
54
56
  const workflowDatabase = () => join(workflowRoot(), "sequential-workflow.db");
55
57
  const templateDirectory = () => join(workflowRoot(), "sequential_workflow_templates");
56
58
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
57
59
  const requiredText = (value: unknown, field: string, maxLength: number) => {
58
60
  if (typeof value !== "string" || value.trim().length === 0)
59
- throw new Error(`Template inválido: ${field} é obrigatório.`);
61
+ throw new Error(`Invalid template: ${field} is required.`);
60
62
  if (value.length > maxLength)
61
- throw new Error(`Template inválido: ${field} excede ${maxLength} caracteres.`);
63
+ throw new Error(`Invalid template: ${field} exceeds ${maxLength} characters.`);
62
64
  return value;
63
65
  };
64
66
  const onlyProperties = (value: Record<string, unknown>, allowed: string[], field: string) => {
65
67
  const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
66
68
  if (unexpected)
67
- throw new Error(`Template inválido: ${field} contém a propriedade não permitida ${unexpected}.`);
69
+ throw new Error(`Invalid template: ${field} contains unsupported property ${unexpected}.`);
68
70
  };
69
71
  const validateTemplate = (value: unknown): WorkflowTemplate => {
70
72
  if (!isRecord(value))
71
- throw new Error("Template inválido: a raiz deve ser um objeto JSON.");
73
+ throw new Error("Invalid template: root must be a JSON object.");
72
74
  onlyProperties(value, ["version", "title", "source", "tasks"], "raiz");
73
75
  if (value.version !== 1)
74
- throw new Error("Template inválido: version deve ser 1.");
76
+ throw new Error("Invalid template: version must be 1.");
75
77
  if (!Array.isArray(value.tasks) || value.tasks.length === 0)
76
- throw new Error("Template inválido: tasks deve conter ao menos uma task.");
78
+ throw new Error("Invalid template: tasks must contain at least one task.");
77
79
  if (value.tasks.length > templateMaxTasks)
78
- throw new Error(`Template inválido: tasks não pode exceder ${templateMaxTasks} itens.`);
80
+ throw new Error(`Invalid template: tasks cannot exceed ${templateMaxTasks} items.`);
79
81
  const tasks = value.tasks.map((item, index) => {
80
82
  if (!isRecord(item))
81
- throw new Error(`Template inválido: tasks[${index}] deve ser um objeto.`);
83
+ throw new Error(`Invalid template: tasks[${index}] must be an object.`);
82
84
  onlyProperties(item, ["type", "instruction", "criteria"], `tasks[${index}]`);
83
85
  if (item.type !== "action" && item.type !== "collect" && item.type !== "evaluate" && item.type !== "workflow")
84
- throw new Error(`Template inválido: tasks[${index}].type deve ser action, collect, evaluate ou workflow.`);
86
+ throw new Error(`Invalid template: tasks[${index}].type must be action, collect, evaluate, or workflow.`);
85
87
  const criteria = item.criteria === undefined ? undefined : requiredText(item.criteria, `tasks[${index}].criteria`, templateMaxTextLength);
86
88
  if (item.type === "collect" && !criteria)
87
- throw new Error(`Template inválido: tasks[${index}] é Collect e exige criteria.`);
89
+ throw new Error(`Invalid template: tasks[${index}] is Collect and requires criteria.`);
88
90
  return { type: item.type as TaskType, instruction: requiredText(item.instruction, `tasks[${index}].instruction`, templateMaxTextLength), criteria };
89
91
  });
90
92
  return { version: 1, title: requiredText(value.title, "title", templateMaxTitleLength), source: requiredText(value.source, "source", templateMaxSourceLength), tasks };
@@ -99,20 +101,20 @@ const loadTemplate = async (inputPath: string) => {
99
101
  try {
100
102
  const info = await stat(path);
101
103
  if (!info.isFile())
102
- throw new Error("não é um arquivo regular");
104
+ throw new Error("not a regular file");
103
105
  if (info.size > templateMaxBytes)
104
106
  throw new Error(`excede o limite de ${templateMaxBytes} bytes`);
105
107
  content = await readFile(path, "utf8");
106
108
  }
107
109
  catch (error) {
108
- throw new Error(`Não foi possível ler o template ${path}: ${error instanceof Error ? error.message : String(error)}`);
110
+ throw new Error(`Could not read template ${path}: ${error instanceof Error ? error.message : String(error)}`);
109
111
  }
110
112
  let document: unknown;
111
113
  try {
112
114
  document = JSON.parse(content);
113
115
  }
114
116
  catch (error) {
115
- throw new Error(`Template inválido: JSON malformado em ${path}: ${error instanceof Error ? error.message : String(error)}`);
117
+ throw new Error(`Invalid template: malformed JSON in ${path}: ${error instanceof Error ? error.message : String(error)}`);
116
118
  }
117
119
  return { path, hash: createHash("sha256").update(content).digest("hex"), template: validateTemplate(document) };
118
120
  };
@@ -135,13 +137,14 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
135
137
  const one = <T>(sql: string, ...params: SQLInputValue[]) => db.prepare(sql).get(...params) as T | undefined;
136
138
  const many = <T>(sql: string, ...params: SQLInputValue[]) => db.prepare(sql).all(...params) as T[];
137
139
  const event = (workflowId: number, taskId: number | null, phase: string, payload: unknown) => db.prepare("INSERT INTO workflow_events (workflow_id, task_id, phase, payload) VALUES (?, ?, ?, ?)").run(workflowId, taskId, phase, JSON.stringify(payload));
140
+ const evaluationRecord = (task: Task, outcome: "accept" | "retry" | "fail", reasoning: string, decision?: { evaluator?: string; model?: string; confidence?: number; probabilities?: Record<string, number> }) => JSON.stringify({ version: 1, outcome, reasoning, contract: { instruction: task.instruction, criteria: task.criteria }, evidence: { result: task.result }, decision, evaluatedAt: new Date().toISOString() });
138
141
  const workflow = (id: number) => one<Workflow>("SELECT id, title, source, status, parent_workflow_id FROM workflows WHERE id = ?", id);
139
- const isActive = (item: Workflow) => ["running", "awaiting_user", "evaluating"].includes(item.status);
142
+ const isActive = (item: Workflow) => ["pending_definition", "running", "awaiting_user", "evaluating"].includes(item.status);
140
143
  const currentTask = (workflowId: number) => one<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE workflow_id = ? AND status NOT IN ('accepted', 'failed') ORDER BY position LIMIT 1", workflowId);
141
144
  const taskSummary = (task: Task) => ({ id: task.id, position: task.position, type: task.type, instruction: task.instruction, criteria: task.criteria, status: task.status, attempts: task.attempts, childWorkflowId: task.child_workflow_id });
142
145
  const requireActiveWorkflow = (id: number) => { const item = workflow(id); if (!item)
143
- throw new Error(`Workflow #${id} não existe.`); if (!isActive(item))
144
- throw new Error(`Workflow #${id} está ${item.status} e não pode ser alterado.`); return item; };
146
+ throw new Error(`Workflow #${id} does not exist.`); if (!isActive(item))
147
+ throw new Error(`Workflow #${id} is ${item.status} and cannot be changed.`); return item; };
145
148
  const resolveParentAfterChild = (child: Workflow) => {
146
149
  const parentTask = one<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE child_workflow_id = ? AND status = 'waiting_subworkflow'", child.id);
147
150
  if (!parentTask)
@@ -149,7 +152,7 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
149
152
  const parent = workflow(parentTask.workflow_id);
150
153
  if (!parent || !isActive(parent))
151
154
  return;
152
- const result = `Subworkflow #${child.id} (${child.title}) terminou com status ${child.status}.`;
155
+ const result = `Subworkflow #${child.id} (${child.title}) finished with status ${child.status}.`;
153
156
  if (child.status === "completed" && !parentTask.criteria) {
154
157
  db.prepare("UPDATE workflow_tasks SET result = ?, status = 'accepted', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(result, parentTask.id);
155
158
  event(parent.id, parentTask.id, "subworkflow_completed", { childWorkflowId: child.id, accepted: true });
@@ -168,7 +171,7 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
168
171
  if (!next) {
169
172
  db.prepare("UPDATE workflows SET status = 'completed', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(item.id);
170
173
  const completed = workflow(item.id)!;
171
- event(item.id, null, "completed", { message: "Todas as tasks foram aceitas." });
174
+ event(item.id, null, "completed", { message: "All tasks were accepted." });
172
175
  resolveParentAfterChild(completed);
173
176
  return { completed: true };
174
177
  }
@@ -185,68 +188,87 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
185
188
  db.prepare("UPDATE workflows SET status='cancelled', updated_at=CURRENT_TIMESTAMP WHERE id=?").run(item.id);
186
189
  event(item.id, null, "cancelled", { by: "user", sessionId: harness.session() });
187
190
  resolveParentAfterChild(workflow(item.id)!);
188
- return { content: [{ type: "text" as const, text: `Workflow #${item.id} cancelado.` }], details: { workflowId: item.id, status: "cancelled" } };
191
+ return { content: [{ type: "text" as const, text: `Workflow #${item.id} cancelled.` }], details: { workflowId: item.id, status: "cancelled" } };
189
192
  };
190
- const createWorkflow = (definition: WorkflowDefinition, parentWorkflowId?: number, parentTaskId?: number) => {
193
+ const validateDefinition = (definition: WorkflowDefinition) => {
191
194
  for (const [index, task] of definition.tasks.entries())
192
195
  if (task.type === "collect" && !task.criteria)
193
- throw new Error(`Task ${index + 1} é Collect e exige criteria.`);
194
- let parentTask: Task | undefined;
195
- if (parentTaskId !== undefined) {
196
- parentTask = one<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE id = ?", parentTaskId);
197
- if (!parentTask)
198
- throw new Error(`Task pai #${parentTaskId} não existe.`);
199
- if (parentTask.type !== "workflow")
200
- throw new Error(`Task pai #${parentTaskId} deve ser do tipo workflow.`);
201
- if (parentTask.status !== "running")
202
- throw new Error(`Task pai #${parentTaskId} não está pronta para iniciar um subworkflow.`);
203
- if (parentTask.child_workflow_id !== null)
204
- throw new Error(`Task pai #${parentTaskId} já possui um subworkflow vinculado.`);
205
- if (parentWorkflowId !== undefined && parentWorkflowId !== parentTask.workflow_id)
206
- throw new Error("parentWorkflowId não corresponde ao workflow da parentTaskId.");
207
- parentWorkflowId = parentTask.workflow_id;
208
- requireActiveWorkflow(parentWorkflowId);
209
- }
210
- else if (parentWorkflowId !== undefined && !workflow(parentWorkflowId))
211
- throw new Error(`Workflow pai #${parentWorkflowId} não existe.`);
212
- {
213
- const inserted = db.prepare("INSERT INTO workflows (title, source, status, parent_workflow_id, session_id) VALUES (?, ?, 'running', ?, ?)").run(definition.title, definition.source, parentWorkflowId ?? null, harness.session());
214
- const workflowId = Number(inserted.lastInsertRowid);
215
- const insertTask = db.prepare("INSERT INTO workflow_tasks (workflow_id, position, type, instruction, criteria, status) VALUES (?, ?, ?, ?, ?, 'pending')");
216
- definition.tasks.forEach((task, index) => insertTask.run(workflowId, index + 1, task.type, task.instruction, task.criteria ?? null));
217
- event(workflowId, null, "created", { title: definition.title, taskCount: definition.tasks.length, parentWorkflowId: parentWorkflowId ?? null });
218
- if (parentTask) {
219
- db.prepare("UPDATE workflow_tasks SET child_workflow_id = ?, status = 'waiting_subworkflow', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(workflowId, parentTask.id);
220
- event(parentTask.workflow_id, parentTask.id, "child_linked", { childWorkflowId: workflowId });
221
- }
222
- const next = activateNext(workflow(workflowId)!);
223
- const suffix = parentTask ? ` vinculado à task pai #${parentTask.id}.` : ".";
224
- return { content: [{ type: "text" as const, text: next.completed ? `Workflow #${workflowId} criado e concluído${suffix}` : `Workflow #${workflowId} criado${suffix} Execute somente a task #${next.task!.position}: ${next.task!.instruction}` }], details: { workflowId, parentWorkflowId: parentWorkflowId ?? null, parentTaskId: parentTask?.id ?? null, next } };
196
+ throw new Error(`Task ${index + 1} is Collect and requires criteria.`);
197
+ };
198
+ const insertTasks = (workflowId: number, definition: WorkflowDefinition) => {
199
+ const insertTask = db.prepare("INSERT INTO workflow_tasks (workflow_id, position, type, instruction, criteria, status) VALUES (?, ?, ?, ?, ?, 'pending')");
200
+ definition.tasks.forEach((task, index) => insertTask.run(workflowId, index + 1, task.type, task.instruction, task.criteria ?? null));
201
+ };
202
+ const workflowResult = (workflowId: number, parentTask?: Task) => {
203
+ const next = activateNext(workflow(workflowId)!);
204
+ const suffix = parentTask ? ` linked to parent task #${parentTask.id}.` : ".";
205
+ return { content: [{ type: "text" as const, text: next.completed ? `Workflow #${workflowId} created and completed${suffix}` : `Workflow #${workflowId} created${suffix} Execute only task #${next.task!.position}: ${next.task!.instruction}` }], details: { workflowId, parentWorkflowId: parentTask?.workflow_id ?? null, parentTaskId: parentTask?.id ?? null, next } };
206
+ };
207
+ const createRootWorkflow = (definition: WorkflowDefinition) => {
208
+ validateDefinition(definition);
209
+ const pendingWorkflowId = harness.pendingWorkflow();
210
+ const workflowId = pendingWorkflowId ?? Number(db.prepare("INSERT INTO workflows (title, source, status, session_id) VALUES (?, ?, 'running', ?)").run(definition.title, definition.source, harness.session()).lastInsertRowid);
211
+ if (pendingWorkflowId) {
212
+ db.prepare("UPDATE workflows SET title=?, source=?, status='running', updated_at=CURRENT_TIMESTAMP WHERE id=?").run(definition.title, definition.source, workflowId);
213
+ event(workflowId, null, "definition_supplied", { title: definition.title, taskCount: definition.tasks.length });
225
214
  }
215
+ else
216
+ event(workflowId, null, "created", { title: definition.title, taskCount: definition.tasks.length, parentWorkflowId: null });
217
+ insertTasks(workflowId, definition);
218
+ return workflowResult(workflowId);
219
+ };
220
+ const createSubworkflow = (definition: WorkflowDefinition, parentWorkflowId: number, parentTaskId: number) => {
221
+ validateDefinition(definition);
222
+ const parentTask = one<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE id = ?", parentTaskId);
223
+ if (!parentTask)
224
+ throw new Error(`Parent task #${parentTaskId} does not exist.`);
225
+ if (parentTask.workflow_id !== parentWorkflowId)
226
+ throw new Error("parentWorkflowId does not match parentTaskId's workflow.");
227
+ if (parentTask.type !== "workflow")
228
+ throw new Error(`Task pai #${parentTaskId} deve ser do tipo workflow.`);
229
+ if (parentTask.status !== "running")
230
+ throw new Error(`Parent task #${parentTaskId} is not ready to start a subworkflow.`);
231
+ if (parentTask.child_workflow_id !== null)
232
+ throw new Error(`Parent task #${parentTaskId} already has a linked subworkflow.`);
233
+ requireActiveWorkflow(parentWorkflowId);
234
+ const workflowId = Number(db.prepare("INSERT INTO workflows (title, source, status, parent_workflow_id, session_id) VALUES (?, ?, 'running', ?, ?)").run(definition.title, definition.source, parentWorkflowId, harness.session()).lastInsertRowid);
235
+ event(workflowId, null, "created", { title: definition.title, taskCount: definition.tasks.length, parentWorkflowId });
236
+ insertTasks(workflowId, definition);
237
+ db.prepare("UPDATE workflow_tasks SET child_workflow_id = ?, status = 'waiting_subworkflow', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(workflowId, parentTask.id);
238
+ event(parentTask.workflow_id, parentTask.id, "child_linked", { childWorkflowId: workflowId });
239
+ return workflowResult(workflowId, parentTask);
226
240
  };
227
- pi.registerTool({ name: "sequential_workflow_create", label: "Create Sequential Workflow", description: "Persiste um workflow e ativa sua primeira task, sem bloquear outros workflows.", promptSnippet: "Create a persisted sequential workflow", promptGuidelines: ["Use sequential_workflow_create only after the user explicitly names Sequential Workflow, sequential-workflow, workflow sequencial, or fluxo de trabalho sequencial and asks to create or execute it.", "A Collect task passed to sequential_workflow_create must include acceptance criteria."], parameters: Type.Object({ title: Type.String({ minLength: 1 }), source: Type.String({ minLength: 1 }), parentWorkflowId: Type.Optional(Type.Integer({ minimum: 1 })), parentTaskId: Type.Optional(Type.Integer({ minimum: 1 })), tasks: Type.Array(Type.Object({ type: taskType, instruction: Type.String({ minLength: 1 }), criteria: Type.Optional(Type.String({ minLength: 1 })) }), { minItems: 1 }) }), async execute(_id, params): Promise<{
241
+ const definitionParameters = { title: Type.String({ minLength: 1 }), source: Type.String({ minLength: 1 }), tasks: Type.Array(Type.Object({ type: taskType, instruction: Type.String({ minLength: 1 }), criteria: Type.Optional(Type.String({ minLength: 1 })) }), { minItems: 1 }) };
242
+ pi.registerTool({ name: "sequential_workflow_create", label: "Create Sequential Workflow", description: "Creates a root workflow or defines the focused pending workflow.", promptSnippet: "Create or define a root Sequential Workflow", promptGuidelines: ["Use sequential_workflow_create only after an explicit Sequential Workflow requirement was identified in the current user input or a Skill. It creates a root workflow or defines the focused pending workflow; it never creates a subworkflow.", "Do not use a template unless the user explicitly requested a named template; use sequential_workflow_create_from_template only for that case.", "Generate every workflow artifact in English: title, source, instructions, criteria, and recorded result summaries.", "A Collect task passed to sequential_workflow_create must include strict acceptance criteria. Criteria are a durable acceptance contract: preserve every material collection requirement in concrete, verifiable terms. Do not weaken a compound requirement to non-empty input; for example, collecting a full name must require both given name and surname.", "Action criteria must be outcome-oriented and proportionate: verify completion and recorded evidence of the requested action, without requiring an evaluator to independently reconstruct incidental calculations unless the request explicitly makes that verification critical.", "An Action task with criteria fails its workflow after five rejected attempts."], parameters: Type.Object(definitionParameters), async execute(_id, params): Promise<{
228
243
  content: {
229
244
  type: "text";
230
245
  text: string;
231
246
  }[];
232
247
  details: Record<string, unknown>;
233
- }> { return createWorkflow(params, params.parentWorkflowId, params.parentTaskId); } });
234
- pi.registerTool({ name: "sequential_workflow_prepare_template_directory", label: "Prepare Sequential Workflow Template Directory", description: "Cria, quando necessário, e informa o diretório padrão de templates JSON de Sequential Workflow do profile ativo.", promptSnippet: "Prepare the active profile's default Sequential Workflow template directory", promptGuidelines: ["Use sequential_workflow_prepare_template_directory only when the user explicitly names Sequential Workflow and asks to create or edit its JSON template without specifying an output directory."], parameters: Type.Object({}), async execute() { const path = templateDirectory(); await mkdir(path, { recursive: true }); return { content: [{ type: "text", text: `Diretório de templates pronto: ${path}` }], details: { path } }; } });
235
- pi.registerTool({ name: "sequential_workflow_validate_template", label: "Validate Sequential Workflow Template", description: "Lê e valida um arquivo JSON de template de Sequential Workflow sem criar ou executar um workflow.", promptSnippet: "Validate a Sequential Workflow JSON template before it is used", promptGuidelines: ["Use sequential_workflow_validate_template only when the user explicitly names Sequential Workflow and asks to create, edit, or validate its JSON template."], parameters: Type.Object({ path: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
248
+ }> { return createRootWorkflow(params); } });
249
+ pi.registerTool({ name: "sequential_workflow_create_subworkflow", label: "Create Sequential Subworkflow", description: "Creates a child workflow for the focused running workflow task.", promptSnippet: "Create a Sequential Workflow child", promptGuidelines: ["Use only when the focused current task has type workflow and requires a child workflow.", "parentWorkflowId and parentTaskId are required and must identify that focused running task."], parameters: Type.Object({ parentWorkflowId: Type.Integer({ minimum: 1 }), parentTaskId: Type.Integer({ minimum: 1 }), ...definitionParameters }), async execute(_id, params): Promise<{
236
250
  content: {
237
251
  type: "text";
238
252
  text: string;
239
253
  }[];
240
254
  details: Record<string, unknown>;
241
- }> { const loaded = await loadTemplate(params.path); return { content: [{ type: "text", text: `Template válido: ${loaded.path} (${loaded.template.tasks.length} tasks, SHA-256: ${loaded.hash}).` }], details: loaded }; } });
242
- pi.registerTool({ name: "sequential_workflow_create_from_template", label: "Create Sequential Workflow from Template", description: "Lê um template JSON validado e cria um workflow.", promptSnippet: "Create and start a persisted Sequential Workflow from a validated JSON template", promptGuidelines: ["Use sequential_workflow_create_from_template only when the user explicitly names Sequential Workflow and asks to execute a JSON template."], parameters: Type.Object({ path: Type.String({ minLength: 1 }), parentWorkflowId: Type.Optional(Type.Integer({ minimum: 1 })), parentTaskId: Type.Optional(Type.Integer({ minimum: 1 })) }), async execute(_id, params): Promise<{
255
+ }> { return createSubworkflow(params, params.parentWorkflowId, params.parentTaskId); } });
256
+ pi.registerTool({ name: "sequential_workflow_prepare_template_directory", label: "Prepare Sequential Workflow Template Directory", description: "Creates when needed and reports the active profile's default Sequential Workflow JSON template directory.", promptSnippet: "Prepare the active profile's default Sequential Workflow template directory", promptGuidelines: ["Use sequential_workflow_prepare_template_directory only when the user explicitly names Sequential Workflow and asks to create or edit its JSON template without specifying an output directory."], parameters: Type.Object({}), async execute() { const path = templateDirectory(); await mkdir(path, { recursive: true }); return { content: [{ type: "text", text: `Template directory is ready: ${path}` }], details: { path } }; } });
257
+ pi.registerTool({ name: "sequential_workflow_validate_template", label: "Validate Sequential Workflow Template", description: "Reads and validates a Sequential Workflow JSON template without creating or executing a workflow.", promptSnippet: "Validate a Sequential Workflow JSON template before it is used", promptGuidelines: ["Use sequential_workflow_validate_template only when the user explicitly names Sequential Workflow and asks to create, edit, or validate its JSON template."], parameters: Type.Object({ path: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
243
258
  content: {
244
259
  type: "text";
245
260
  text: string;
246
261
  }[];
247
262
  details: Record<string, unknown>;
248
- }> { const loaded = await loadTemplate(params.path); return createWorkflow({ title: loaded.template.title, source: `Template ${loaded.path} (SHA-256: ${loaded.hash}): ${loaded.template.source}`, tasks: loaded.template.tasks }, params.parentWorkflowId, params.parentTaskId); } });
249
- pi.registerTool({ name: "sequential_workflow_record_result", label: "Record Workflow Result", description: "Registra o resultado da Action ou Collect atual de um workflow identificado.", promptSnippet: "Record the result of the active Action or Collect task", promptGuidelines: ["Use sequential_workflow_record_result immediately after completing the active Action or receiving the active Collect response."], parameters: Type.Object({ workflowId: Type.Integer({ minimum: 1 }), taskId: Type.Integer({ minimum: 1 }), phase: phaseType, result: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
263
+ }> { const loaded = await loadTemplate(params.path); return { content: [{ type: "text", text: `Valid template: ${loaded.path} (${loaded.template.tasks.length} tasks, SHA-256: ${loaded.hash}).` }], details: loaded }; } });
264
+ pi.registerTool({ name: "sequential_workflow_create_from_template", label: "Create Sequential Workflow from Template", description: "Loads an explicitly named reusable template to create a root workflow or hydrate a template-mode pending workflow.", promptSnippet: "Create a root Sequential Workflow from an explicitly named template", promptGuidelines: ["Use sequential_workflow_create_from_template only when the user explicitly requested activation from a named reusable Sequential Workflow JSON template, or when template-mode definition context supplies that exact path.", "The path must be explicitly supplied by the user or the controlled template-mode context. Never guess, search for, infer, or invent a template name or use this tool as a fallback for an ordinary workflow request.", "This tool never creates a subworkflow."], parameters: Type.Object({ path: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
265
+ content: {
266
+ type: "text";
267
+ text: string;
268
+ }[];
269
+ details: Record<string, unknown>;
270
+ }> { const loaded = await loadTemplate(params.path); return createRootWorkflow({ title: loaded.template.title, source: `Template ${loaded.path} (SHA-256: ${loaded.hash}): ${loaded.template.source}`, tasks: loaded.template.tasks }); } });
271
+ pi.registerTool({ name: "sequential_workflow_record_result", label: "Record Workflow Result", description: "Records the result of the current Action or Collect task in an identified workflow.", promptSnippet: "Record the result of the active Action or Collect task", promptGuidelines: ["Use sequential_workflow_record_result immediately after completing the active Action or receiving the active Collect response."], parameters: Type.Object({ workflowId: Type.Integer({ minimum: 1 }), taskId: Type.Integer({ minimum: 1 }), phase: phaseType, result: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
250
272
  content: {
251
273
  type: "text";
252
274
  text: string;
@@ -256,23 +278,23 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
256
278
  const item = requireActiveWorkflow(params.workflowId);
257
279
  const task = currentTask(item.id);
258
280
  if (!task)
259
- throw new Error("Não existe task pendente.");
281
+ throw new Error("There is no pending task.");
260
282
  if (task.type !== params.phase)
261
- throw new Error(`A task atual é ${task.type}, não ${params.phase}.`);
283
+ throw new Error(`The current task is ${task.type}, not ${params.phase}.`);
262
284
  if (task.status === "evaluating")
263
- throw new Error("O resultado já foi registrado; avalie a task atual.");
285
+ throw new Error("The result is already recorded; evaluate the current task.");
264
286
  if (task.criteria) {
265
287
  db.prepare("UPDATE workflow_tasks SET result = ?, status = 'evaluating', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(params.result, task.id);
266
288
  db.prepare("UPDATE workflows SET status = 'evaluating', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(item.id);
267
289
  event(item.id, task.id, "result_recorded", { phase: params.phase, result: params.result });
268
- return { content: [{ type: "text", text: `Resultado registrado. Agora avalie a task #${task.position} contra o critério.` }], details: { taskId: task.id, needsEvaluation: true } };
290
+ return { content: [{ type: "text", text: `Result recorded. Now evaluate task #${task.position} against its criteria.` }], details: { taskId: task.id, needsEvaluation: true } };
269
291
  }
270
292
  db.prepare("UPDATE workflow_tasks SET result = ?, status = 'accepted', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(params.result, task.id);
271
293
  event(item.id, task.id, "accepted_without_criteria", { result: params.result });
272
294
  const next = activateNext(item);
273
- return { content: [{ type: "text", text: next.completed ? "Task aceita; workflow concluído." : `Task aceita. Execute a task #${next.task!.position}: ${next.task!.instruction}` }], details: { next } };
295
+ return { content: [{ type: "text", text: next.completed ? "Task accepted; workflow completed." : `Task accepted. Execute task #${next.task!.position}: ${next.task!.instruction}` }], details: { next } };
274
296
  } });
275
- pi.registerTool({ name: "sequential_workflow_evaluate", label: "Evaluate Workflow Task", description: "Registra a aceitação ou reprovação da task atual de um workflow identificado.", promptSnippet: "Accept or reject the active workflow task after evaluation", promptGuidelines: ["Use sequential_workflow_evaluate after every task with criteria and for every Evaluate task; never advance a rejected task."], parameters: Type.Object({ workflowId: Type.Integer({ minimum: 1 }), taskId: Type.Integer({ minimum: 1 }), accepted: Type.Boolean(), reasoning: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
297
+ pi.registerTool({ name: "sequential_workflow_evaluate", label: "Evaluate Workflow Task", description: "Records acceptance, retry, or terminal failure of the current task in an identified workflow.", promptSnippet: "Evaluate the active workflow task", promptGuidelines: ["Use sequential_workflow_evaluate after every task with criteria and for every Evaluate task.", "Use outcome 'retry' only when repeating the task could produce a different result. Use outcome 'fail' after recording evidence when the task cannot safely progress; it ends the workflow without advancing or retrying.", "The legacy accepted boolean remains supported: true means accept and false means retry."], parameters: Type.Object({ workflowId: Type.Integer({ minimum: 1 }), taskId: Type.Integer({ minimum: 1 }), outcome: Type.Optional(evaluationOutcome), accepted: Type.Optional(Type.Boolean()), reasoning: Type.String({ minLength: 1 }), decision: Type.Optional(Type.Object({ evaluator: Type.Optional(Type.String()), model: Type.Optional(Type.String()), confidence: Type.Optional(Type.Number()), probabilities: Type.Optional(Type.Record(Type.String(), Type.Number())) })) }), async execute(_id, params): Promise<{
276
298
  content: {
277
299
  type: "text";
278
300
  text: string;
@@ -282,14 +304,34 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
282
304
  const item = requireActiveWorkflow(params.workflowId);
283
305
  const task = currentTask(item.id);
284
306
  if (!task)
285
- throw new Error("Não existe task pendente.");
307
+ throw new Error("There is no pending task.");
286
308
  if (task.type !== "evaluate" && task.status !== "evaluating")
287
- throw new Error("Registre o resultado da task antes de avaliá-la.");
288
- if (params.accepted) {
289
- db.prepare("UPDATE workflow_tasks SET status = 'accepted', evaluation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(params.reasoning, task.id);
309
+ throw new Error("Record the task result before evaluating it.");
310
+ if (!params.outcome && typeof params.accepted !== "boolean")
311
+ throw new Error("Provide an evaluation outcome.");
312
+ if (params.outcome && typeof params.accepted === "boolean")
313
+ throw new Error("Provide outcome or accepted, not both.");
314
+ const outcome = params.outcome ?? (params.accepted ? "accept" : "retry");
315
+ const evaluation = evaluationRecord(task, outcome, params.reasoning, params.decision);
316
+ if (outcome === "fail") {
317
+ db.prepare("UPDATE workflow_tasks SET status = 'failed', evaluation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(evaluation, task.id);
318
+ db.prepare("UPDATE workflows SET status = 'failed', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(item.id);
319
+ event(item.id, task.id, "failed_evaluation", { reasoning: params.reasoning, attempts: task.attempts });
320
+ resolveParentAfterChild(workflow(item.id)!);
321
+ return { content: [{ type: "text", text: `Task #${task.position} failed by terminal evaluation; workflow failed.` }], details: { accepted: false, failed: true, terminal: true, task: taskSummary(currentTask(item.id) ?? task) } };
322
+ }
323
+ if (outcome === "accept") {
324
+ db.prepare("UPDATE workflow_tasks SET status = 'accepted', evaluation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(evaluation, task.id);
290
325
  event(item.id, task.id, "accepted", { reasoning: params.reasoning });
291
326
  const next = activateNext(item);
292
- return { content: [{ type: "text", text: next.completed ? "Avaliação aceita; workflow concluído." : `Avaliação aceita. Execute somente a task #${next.task!.position}: ${next.task!.instruction}` }], details: { accepted: true, next } };
327
+ return { content: [{ type: "text", text: next.completed ? "Evaluation accepted; workflow completed." : `Evaluation accepted. Execute only task #${next.task!.position}: ${next.task!.instruction}` }], details: { accepted: true, next } };
328
+ }
329
+ if (task.type === "action" && task.attempts + 1 >= maxActionAttempts) {
330
+ db.prepare("UPDATE workflow_tasks SET status = 'failed', attempts = attempts + 1, evaluation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(evaluation, task.id);
331
+ db.prepare("UPDATE workflows SET status = 'failed', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(item.id);
332
+ event(item.id, task.id, "failed_attempt_limit", { reasoning: params.reasoning, attempts: task.attempts + 1, maxAttempts: maxActionAttempts });
333
+ resolveParentAfterChild(workflow(item.id)!);
334
+ return { content: [{ type: "text", text: `Task #${task.position} failed after ${maxActionAttempts} rejected attempts; workflow failed.` }], details: { accepted: false, failed: true, attempts: task.attempts + 1, maxAttempts: maxActionAttempts } };
293
335
  }
294
336
  if (task.type === "workflow") {
295
337
  event(item.id, task.id, "child_detached", { childWorkflowId: task.child_workflow_id });
@@ -298,10 +340,10 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
298
340
  db.prepare("DELETE FROM workflow_collect_checkpoint WHERE task_id=?").run(task.id);
299
341
  const retryStatus: TaskStatus = task.type === "collect" ? "awaiting_user" : "running";
300
342
  const workflowStatus: WorkflowStatus = task.type === "collect" ? "awaiting_user" : "running";
301
- db.prepare("UPDATE workflow_tasks SET status = ?, attempts = attempts + 1, evaluation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(retryStatus, params.reasoning, task.id);
343
+ db.prepare("UPDATE workflow_tasks SET status = ?, attempts = attempts + 1, evaluation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(retryStatus, evaluation, task.id);
302
344
  db.prepare("UPDATE workflows SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(workflowStatus, item.id);
303
345
  event(item.id, task.id, "rejected", { reasoning: params.reasoning });
304
- return { content: [{ type: "text", text: task.type === "collect" ? `Critério reprovado. Permaneça na task #${task.position}.` : `Critério reprovado. Permaneça na task #${task.position} e repita a ação.` }], details: { accepted: false, task: taskSummary(currentTask(item.id)!) } };
346
+ return { content: [{ type: "text", text: task.type === "collect" ? `Criteria rejected. Remain on task #${task.position}.` : `Criteria rejected. Remain on task #${task.position} and repeat the action.` }], details: { accepted: false, task: taskSummary(currentTask(item.id)!) } };
305
347
  } });
306
348
  pi.registerTool({ name: "sequential_workflow_cancel", label: "Cancel Sequential Workflow", description: "Cancela um workflow ativo deste profile, sem cancelar seus descendentes.", promptSnippet: "Cancel a Sequential Workflow", promptGuidelines: ["Use sequential_workflow_cancel only after the user explicitly asks to cancel a named Sequential Workflow.", "sequential_workflow_cancel can cancel a workflow created in another session of the same profile; do not require adoption first.", "Cancelling a child workflow lets its parent handle the terminal child state; do not cancel descendants automatically."], parameters: Type.Object({ workflowId: Type.Integer({ minimum: 1 }) }), async execute(_id, params): Promise<{
307
349
  content: {
@@ -310,7 +352,7 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
310
352
  }[];
311
353
  details: Record<string, unknown>;
312
354
  }> { return cancelWorkflow(params.workflowId); } });
313
- pi.registerTool({ name: "sequential_workflow_status", label: "Sequential Workflow Status", description: "Lista workflows ativos ou consulta o estado persistido de um workflow.", promptGuidelines: ["Use sequential_workflow_status only when the user explicitly asks to inspect, list, or continue a persisted Sequential Workflow. Never select or resume an existing workflow merely because it is active."], parameters: Type.Object({ workflowId: Type.Optional(Type.Integer({ minimum: 1 })), includeChildren: Type.Optional(Type.Boolean()) }), async execute(_id, params): Promise<{
355
+ pi.registerTool({ name: "sequential_workflow_status", label: "Sequential Workflow Status", description: "Lists active workflows or inspects the persisted state of an identified workflow.", promptGuidelines: ["Use sequential_workflow_status only when the user explicitly asks to inspect, list, or continue a persisted Sequential Workflow. Never select or resume an existing workflow merely because it is active."], parameters: Type.Object({ workflowId: Type.Optional(Type.Integer({ minimum: 1 })), includeChildren: Type.Optional(Type.Boolean()) }), async execute(_id, params): Promise<{
314
356
  content: {
315
357
  type: "text";
316
358
  text: string;
@@ -318,15 +360,15 @@ CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT
318
360
  details: Record<string, unknown>;
319
361
  }> {
320
362
  if (params.workflowId === undefined) {
321
- const items = many<Workflow>("SELECT id, title, source, status, parent_workflow_id FROM workflows WHERE session_id = ? AND status IN ('running', 'awaiting_user', 'evaluating') ORDER BY id DESC", harness.session());
322
- return { content: [{ type: "text", text: items.length ? items.map((item) => `#${item.id} ${item.title} (${item.status})${item.parent_workflow_id ? `, pai #${item.parent_workflow_id}` : ""}; task #${currentTask(item.id)?.position ?? "nenhuma"}.`).join("\n") : "Não há workflows ativos." }], details: { workflows: items.map((item) => ({ ...item, currentTask: currentTask(item.id) })) } };
363
+ const items = many<Workflow>("SELECT id, title, source, status, parent_workflow_id FROM workflows WHERE session_id = ? AND status IN ('pending_definition', 'running', 'awaiting_user', 'evaluating') ORDER BY id DESC", harness.session());
364
+ return { content: [{ type: "text", text: items.length ? items.map((item) => `#${item.id} ${item.title} (${item.status})${item.parent_workflow_id ? `, parent #${item.parent_workflow_id}` : ""}; current task #${currentTask(item.id)?.position ?? "none"}.`).join("\n") : "There are no active workflows." }], details: { workflows: items.map((item) => ({ ...item, currentTask: currentTask(item.id) })) } };
323
365
  }
324
366
  const item = workflow(params.workflowId);
325
367
  if (!item)
326
- throw new Error(`Workflow #${params.workflowId} não existe.`);
368
+ throw new Error(`Workflow #${params.workflowId} does not exist.`);
327
369
  const tasks = many<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE workflow_id = ? ORDER BY position", item.id);
328
370
  const children = params.includeChildren ? many<Workflow>("SELECT id, title, source, status, parent_workflow_id FROM workflows WHERE parent_workflow_id = ? ORDER BY id", item.id) : [];
329
- return { content: [{ type: "text", text: `Workflow #${item.id} (${item.status}). Task atual: ${currentTask(item.id)?.position ?? "nenhuma"}.` }], details: { workflow: item, tasks, children } };
371
+ return { content: [{ type: "text", text: `Workflow #${item.id} (${item.status}). Current task: ${currentTask(item.id)?.position ?? "none"}.` }], details: { workflow: item, tasks, children } };
330
372
  } });
331
373
  pi.registerCommand("workflow-status", { description: "Show this session's workflows", handler: async (args, ctx) => {
332
374
  try {
@@ -50,7 +50,22 @@ export class SkillSourceStore {
50
50
  return this.db.prepare("SELECT source_identifier,source_relative_path,target_path FROM skill_source_installations WHERE profile=? AND skill_name=?").get(profile, skillName) as Row | undefined;
51
51
  }
52
52
 
53
- async publish(identifier: string, profile: string, skillName: string, skillPath: string) {
53
+ private async replacePublishedSkill(source: string, target: string): Promise<void> {
54
+ const temporary = `${target}.publish-${randomUUID()}`, backup = `${target}.backup-${randomUUID()}`;
55
+ try {
56
+ await rm(temporary, { recursive: true, force: true });
57
+ await cp(source, temporary, { recursive: true, filter: (path) => !path.split("/").includes("node_modules") && !path.split("/").includes(".git") });
58
+ if (existsSync(target)) await rename(target, backup);
59
+ await rename(temporary, target);
60
+ await rm(backup, { recursive: true, force: true });
61
+ } catch (error) {
62
+ await rm(temporary, { recursive: true, force: true });
63
+ if (!existsSync(target) && existsSync(backup)) await rename(backup, target);
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ async publish(identifier: string, profile: string, skillName: string, skillPath: string, confirmOverwrite = false) {
54
69
  const previous = this.publishing.get(identifier) ?? Promise.resolve();
55
70
  let release!: () => void;
56
71
  const queued = new Promise<void>((resolveRun) => { release = resolveRun; });
@@ -66,17 +81,21 @@ export class SkillSourceStore {
66
81
  await this.validateSkillDirectory(local);
67
82
  const installation = this.installation(profile, skillName);
68
83
  if (installation && String(installation.source_identifier) !== identifier) throw Object.assign(new Error("This skill was imported from a different Skill Source. Publish it there or create a new local skill."), { status: 409 });
69
- const targetRelative = installation ? String(installation.source_relative_path) : join(source.basePath, skillName).replaceAll("\\", "/");
70
- if (targetRelative.split("/").includes("..") || targetRelative.startsWith("/")) throw Object.assign(new Error("Invalid skill destination."), { status: 400 });
71
- const sameName = this.skills(identifier).filter((item) => item.name === skillName && item.relativePath !== targetRelative && item.status === "synced");
72
- if (sameName.length) throw Object.assign(new Error(`A different skill named '${skillName}' already exists in this Skill Source. Publish by its original path or rename the local skill.`), { status: 409 });
73
84
  const refreshed = await this.sync(identifier);
74
85
  if (refreshed.status !== "succeeded") throw Object.assign(new Error(refreshed.error ?? "Unable to synchronize the Skill Source before publishing."), { status: 502 });
86
+ const available = this.skills(identifier).filter((item) => item.status === "synced");
87
+ const matching = available.filter((item) => item.name === skillName);
88
+ if (!installation && matching.length > 1) throw Object.assign(new Error(`Multiple Skills named '${skillName}' exist in this Skill Source. Rename the local Skill before publishing.`), { status: 409 });
89
+ const sourceRelative = installation ? String(installation.source_relative_path) : matching[0]?.relativePath ?? skillName;
90
+ if (sourceRelative.split("/").includes("..") || sourceRelative.startsWith("/")) throw Object.assign(new Error("Invalid skill destination."), { status: 400 });
91
+ const existing = available.find((item) => item.relativePath === sourceRelative);
92
+ if (installation && !existing) throw Object.assign(new Error("The Skill no longer exists in its Skill Source. Import it again or publish it as a new local Skill."), { status: 409 });
93
+ const targetRelative = join(source.basePath, sourceRelative).replaceAll("\\", "/");
94
+ if (existing && !confirmOverwrite) return { action: "confirmation_required" as const, source: identifier, relativePath: targetRelative, description: existing.description, commitHash: existing.commitHash };
75
95
  const repository = resolve(this.root, ".git-cache", identifier), target = resolve(repository, targetRelative);
76
96
  if (!target.startsWith(`${repository}/`)) throw Object.assign(new Error("Invalid skill destination."), { status: 400 });
77
- await rm(target, { recursive: true, force: true });
78
97
  await mkdir(dirname(target), { recursive: true });
79
- await cp(local, target, { recursive: true, filter: (path) => !path.split("/").includes("node_modules") && !path.split("/").includes(".git") });
98
+ await this.replacePublishedSkill(local, target);
80
99
  const token = String((this.db.prepare("SELECT credential_token FROM skill_sources WHERE identifier=?").get(identifier) as Row).credential_token);
81
100
  const askpass = join(this.root, `.git-askpass-publish-${randomUUID()}`), env: NodeJS.ProcessEnv = {
82
101
  ...process.env,
@@ -101,9 +120,9 @@ export class SkillSourceStore {
101
120
  } finally { await rm(askpass, { force: true }); }
102
121
  const synced = await this.sync(identifier);
103
122
  if (synced.status !== "succeeded") throw Object.assign(new Error(synced.error ?? "The Skill Source was pushed but could not be synchronized."), { status: 502 });
104
- const published = this.skills(identifier).find((item) => item.relativePath === targetRelative);
105
- if (published) this.db.prepare("INSERT INTO skill_source_installations VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(profile,skill_name) DO UPDATE SET source_identifier=excluded.source_identifier,source_relative_path=excluded.source_relative_path,source_commit_hash=excluded.source_commit_hash,source_content_hash=excluded.source_content_hash,target_path=excluded.target_path,installed_at=excluded.installed_at").run(profile,skillName,identifier,targetRelative,published.commitHash,published.contentHash,local,now());
106
- return { action: installation ? "updated" as const : "created" as const, source: identifier, relativePath: targetRelative, commitHash: synced.commitHash };
123
+ const published = this.skills(identifier).find((item) => item.relativePath === sourceRelative);
124
+ if (published) this.db.prepare("INSERT INTO skill_source_installations VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(profile,skill_name) DO UPDATE SET source_identifier=excluded.source_identifier,source_relative_path=excluded.source_relative_path,source_commit_hash=excluded.source_commit_hash,source_content_hash=excluded.source_content_hash,target_path=excluded.target_path,installed_at=excluded.installed_at").run(profile,skillName,identifier,sourceRelative,published.commitHash,published.contentHash,local,now());
125
+ return { action: existing ? "updated" as const : "created" as const, source: identifier, relativePath: targetRelative, commitHash: synced.commitHash };
107
126
  } finally { release(); if (this.publishing.get(identifier) === chain) this.publishing.delete(identifier); }
108
127
  }
109
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-feats",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "A modular Pi package for profiles, sandboxing, remote access, applications, guardrails, skills, session management, observability, and web-based operations.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -29,6 +29,7 @@
29
29
  "scripts",
30
30
  "schemas",
31
31
  "skills",
32
+ "themes",
32
33
  "README.md",
33
34
  "LICENSE",
34
35
  "THIRD_PARTY_NOTICES.md",
@@ -40,18 +41,18 @@
40
41
  "node": ">=22"
41
42
  },
42
43
  "scripts": {
43
- "build:web": "cd extensions/pi-console-webui && npx --no-install next build",
44
+ "build:web": "node scripts/build-console-webui.mjs",
45
+ "build:web:next": "cd extensions/pi-console-webui && npx --no-install next build",
44
46
  "ensure:nono": "sh scripts/install-nono.sh",
45
- "postinstall": "npm run ensure:nono && npm run build:web",
46
- "test": "npm run test:composio && npm run build:web",
47
- "test:composio": "node --experimental-strip-types --test tests/*.test.ts"
47
+ "postinstall": "npm run ensure:nono && node scripts/ensure-console-build.mjs",
48
+ "test": "npm run test:extensions && npm run build:web",
49
+ "test:extensions": "node --experimental-strip-types --test tests/*.test.ts"
48
50
  },
49
51
  "dependencies": {
50
52
  "@codemirror/lang-javascript": "^6.2.5",
51
53
  "@codemirror/lang-json": "^6.0.2",
52
54
  "@codemirror/lang-markdown": "^6.5.2",
53
55
  "@composio/core": "^0.18.1",
54
- "@earendil-works/pi-ai": "^0.85.1",
55
56
  "@fastify/websocket": "^11.3.0",
56
57
  "@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
57
58
  "@radix-ui/react-slot": "^1.2.4",
@@ -79,22 +80,27 @@
79
80
  "remark-gfm": "^4.0.1",
80
81
  "tailwind-merge": "^3.0.2",
81
82
  "tailwindcss": "^3.4.17",
82
- "typebox": "^1.3.30",
83
83
  "typescript": "^5.7.2"
84
84
  },
85
85
  "peerDependencies": {
86
- "@earendil-works/pi-coding-agent": "*"
86
+ "@earendil-works/pi-ai": "*",
87
+ "@earendil-works/pi-coding-agent": "*",
88
+ "@earendil-works/pi-tui": "*",
89
+ "typebox": "*"
87
90
  },
88
91
  "pi": {
89
92
  "extensions": [
93
+ "./extensions/cli-tui-mode.ts",
90
94
  "./extensions/api-server",
91
95
  "./extensions/browser-harness",
92
96
  "./extensions/cli-resources.ts",
93
97
  "./extensions/composio",
94
98
  "./extensions/context-memory",
99
+ "./extensions/conversation-search",
95
100
  "./extensions/guardrails",
96
101
  "./extensions/pi-console-webui",
97
102
  "./extensions/profiles.ts",
103
+ "./extensions/profile-ssh.ts",
98
104
  "./extensions/pulse",
99
105
  "./extensions/sequential-workflow.ts",
100
106
  "./extensions/skill-sources"
@@ -102,9 +108,15 @@
102
108
  "skills": [
103
109
  "./skills"
104
110
  ],
111
+ "themes": [
112
+ "./themes/*.json"
113
+ ],
105
114
  "image": "https://raw.githubusercontent.com/rjaskonis/pi-feats/v0.1.4/assets/pi-feats-preview.png"
106
115
  },
107
116
  "devDependencies": {
108
- "@types/ws": "^8.18.1"
117
+ "@earendil-works/pi-ai": "^0.85.1",
118
+ "@earendil-works/pi-tui": "^0.85.1",
119
+ "@types/ws": "^8.18.1",
120
+ "typebox": "^1.3.30"
109
121
  }
110
122
  }
@@ -0,0 +1,7 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { packageRoot, saveBuildCache } from "./console-webui-cache.mjs";
3
+
4
+ const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build:web:next"], { cwd: packageRoot, stdio: "inherit" });
5
+ if (result.status !== 0) process.exit(result.status ?? 1);
6
+ await saveBuildCache();
7
+ console.log("Cached Pi Console WebUI build for future updates.");