@agentpactai/agentpact-openclaw-plugin 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +976 -0
- package/docs/manual-smoke-test.md +74 -0
- package/docs/openclaw-mcp-integration.md +58 -0
- package/docs/openclaw-semi-auto.md +69 -0
- package/docs/policies.md +61 -0
- package/docs/task-workspace.md +69 -0
- package/examples/agentpact-state.json +11 -0
- package/examples/openclaw-mcp-config.json +14 -0
- package/examples/task-workspace-tree.txt +16 -0
- package/openclaw.plugin.json +25 -0
- package/package.json +52 -0
- package/skills/agentpact/HEARTBEAT.md +178 -0
- package/skills/agentpact/SKILL.md +324 -0
- package/templates/delivery-manifest.json +10 -0
- package/templates/proposal-research.md +18 -0
- package/templates/proposal-software.md +18 -0
- package/templates/proposal-writing.md +19 -0
- package/templates/revision-analysis.md +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
default: () => register
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var fs = __toESM(require("fs/promises"));
|
|
37
|
+
var path = __toESM(require("path"));
|
|
38
|
+
var os = __toESM(require("os"));
|
|
39
|
+
var PLUGIN_ID = "agentpact";
|
|
40
|
+
var DEFAULT_MCP_SERVER_NAME = "agentpact";
|
|
41
|
+
var DEFAULT_STATE = {
|
|
42
|
+
lastEventPoll: 0,
|
|
43
|
+
lastTaskDiscovery: 0,
|
|
44
|
+
lastDeadlineCheck: 0,
|
|
45
|
+
lastChatCheck: 0,
|
|
46
|
+
lastEventCursor: "",
|
|
47
|
+
activeTasks: [],
|
|
48
|
+
pendingConfirmations: [],
|
|
49
|
+
recentTaskIds: [],
|
|
50
|
+
processedRevisionKeys: [],
|
|
51
|
+
processedEventKeys: [],
|
|
52
|
+
sentMessageKeys: [],
|
|
53
|
+
bidTaskIds: []
|
|
54
|
+
};
|
|
55
|
+
var MAX_TRACKED_KEYS = 200;
|
|
56
|
+
function textResult(text) {
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text }]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function jsonResult(value) {
|
|
62
|
+
return textResult(JSON.stringify(value, null, 2));
|
|
63
|
+
}
|
|
64
|
+
function getPluginConfig(api) {
|
|
65
|
+
return api?.config?.plugins?.entries?.[PLUGIN_ID]?.config ?? {};
|
|
66
|
+
}
|
|
67
|
+
function getMcpServerName(api) {
|
|
68
|
+
const config = getPluginConfig(api);
|
|
69
|
+
return typeof config.mcpServerName === "string" && config.mcpServerName.trim() ? config.mcpServerName.trim() : DEFAULT_MCP_SERVER_NAME;
|
|
70
|
+
}
|
|
71
|
+
function getOpenClawConfigPath() {
|
|
72
|
+
return path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
73
|
+
}
|
|
74
|
+
function getStatePath() {
|
|
75
|
+
return path.join(os.homedir(), ".openclaw", "workspace", "memory", "agentpact-state.json");
|
|
76
|
+
}
|
|
77
|
+
function getTasksRoot() {
|
|
78
|
+
return path.join(os.homedir(), ".openclaw", "workspace", "agentpact", "tasks");
|
|
79
|
+
}
|
|
80
|
+
function getTaskRoot(taskId) {
|
|
81
|
+
return path.join(getTasksRoot(), taskId.trim());
|
|
82
|
+
}
|
|
83
|
+
function normalizeText(value) {
|
|
84
|
+
return typeof value === "string" ? value.trim() : "";
|
|
85
|
+
}
|
|
86
|
+
function normalizeStringArray(values) {
|
|
87
|
+
if (!Array.isArray(values)) return [];
|
|
88
|
+
return uniqueTrimmed(values);
|
|
89
|
+
}
|
|
90
|
+
function uniqueTrimmed(values) {
|
|
91
|
+
const seen = /* @__PURE__ */ new Set();
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const value of values) {
|
|
94
|
+
if (typeof value !== "string") continue;
|
|
95
|
+
const item = value.trim();
|
|
96
|
+
if (!item || seen.has(item)) continue;
|
|
97
|
+
seen.add(item);
|
|
98
|
+
out.push(item);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function keepRecent(values, max = MAX_TRACKED_KEYS) {
|
|
103
|
+
return values.slice(Math.max(0, values.length - max));
|
|
104
|
+
}
|
|
105
|
+
function appendReason(reasons, reason) {
|
|
106
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
107
|
+
}
|
|
108
|
+
async function ensureDir(dir) {
|
|
109
|
+
await fs.mkdir(dir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
async function pathExists(target) {
|
|
112
|
+
try {
|
|
113
|
+
await fs.access(target);
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function readJsonFile(target) {
|
|
120
|
+
try {
|
|
121
|
+
const raw = await fs.readFile(target, "utf8");
|
|
122
|
+
return JSON.parse(raw);
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function writeJsonFile(target, value) {
|
|
128
|
+
await ensureDir(path.dirname(target));
|
|
129
|
+
await fs.writeFile(target, JSON.stringify(value, null, 2), "utf8");
|
|
130
|
+
}
|
|
131
|
+
async function loadOpenClawConfig() {
|
|
132
|
+
const configPath = getOpenClawConfigPath();
|
|
133
|
+
const exists = await pathExists(configPath);
|
|
134
|
+
if (!exists) {
|
|
135
|
+
return { configPath, exists: false, config: {} };
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
139
|
+
const sanitized = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
140
|
+
const parsed = sanitized.trim() ? JSON.parse(sanitized) : {};
|
|
141
|
+
return { configPath, exists: true, config: parsed };
|
|
142
|
+
} catch {
|
|
143
|
+
return { configPath, exists: true, config: {} };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function loadState() {
|
|
147
|
+
const statePath = getStatePath();
|
|
148
|
+
const current = await readJsonFile(statePath);
|
|
149
|
+
const merged = {
|
|
150
|
+
...DEFAULT_STATE,
|
|
151
|
+
...current ?? {}
|
|
152
|
+
};
|
|
153
|
+
merged.activeTasks = uniqueTrimmed(Array.isArray(merged.activeTasks) ? merged.activeTasks : []);
|
|
154
|
+
merged.pendingConfirmations = uniqueTrimmed(Array.isArray(merged.pendingConfirmations) ? merged.pendingConfirmations : []);
|
|
155
|
+
merged.recentTaskIds = keepRecent(uniqueTrimmed(Array.isArray(merged.recentTaskIds) ? merged.recentTaskIds : []));
|
|
156
|
+
merged.processedRevisionKeys = keepRecent(uniqueTrimmed(Array.isArray(merged.processedRevisionKeys) ? merged.processedRevisionKeys : []));
|
|
157
|
+
merged.processedEventKeys = keepRecent(uniqueTrimmed(Array.isArray(merged.processedEventKeys) ? merged.processedEventKeys : []));
|
|
158
|
+
merged.sentMessageKeys = keepRecent(uniqueTrimmed(Array.isArray(merged.sentMessageKeys) ? merged.sentMessageKeys : []));
|
|
159
|
+
merged.bidTaskIds = keepRecent(uniqueTrimmed(Array.isArray(merged.bidTaskIds) ? merged.bidTaskIds : []));
|
|
160
|
+
return { statePath, state: merged };
|
|
161
|
+
}
|
|
162
|
+
async function saveState(state) {
|
|
163
|
+
const statePath = getStatePath();
|
|
164
|
+
await writeJsonFile(statePath, state);
|
|
165
|
+
return statePath;
|
|
166
|
+
}
|
|
167
|
+
async function ensureWorkspace(task) {
|
|
168
|
+
const taskId = task.taskId.trim();
|
|
169
|
+
if (!taskId) throw new Error("taskId is required");
|
|
170
|
+
const taskRoot = getTaskRoot(taskId);
|
|
171
|
+
const proposalDir = path.join(taskRoot, "proposal");
|
|
172
|
+
const workDir = path.join(taskRoot, "work");
|
|
173
|
+
const deliveryDir = path.join(taskRoot, "delivery");
|
|
174
|
+
const revisionsDir = path.join(taskRoot, "revisions");
|
|
175
|
+
const publicMaterialsDir = path.join(taskRoot, "public-materials");
|
|
176
|
+
const confidentialMaterialsDir = path.join(taskRoot, "confidential-materials");
|
|
177
|
+
await Promise.all([
|
|
178
|
+
ensureDir(taskRoot),
|
|
179
|
+
ensureDir(proposalDir),
|
|
180
|
+
ensureDir(workDir),
|
|
181
|
+
ensureDir(deliveryDir),
|
|
182
|
+
ensureDir(revisionsDir),
|
|
183
|
+
ensureDir(publicMaterialsDir),
|
|
184
|
+
ensureDir(confidentialMaterialsDir)
|
|
185
|
+
]);
|
|
186
|
+
const taskJsonPath = path.join(taskRoot, "task.json");
|
|
187
|
+
const summaryPath = path.join(taskRoot, "summary.md");
|
|
188
|
+
const notesPath = path.join(deliveryDir, "notes.md");
|
|
189
|
+
const manifestPath = path.join(deliveryDir, "manifest.json");
|
|
190
|
+
const proposalPath = path.join(proposalDir, "proposal.md");
|
|
191
|
+
const taskJson = {
|
|
192
|
+
taskId,
|
|
193
|
+
escrowId: task.escrowId ?? "",
|
|
194
|
+
category: task.category ?? "",
|
|
195
|
+
difficulty: task.difficulty ?? "",
|
|
196
|
+
reward: task.reward ?? "",
|
|
197
|
+
status: task.status ?? "",
|
|
198
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
199
|
+
};
|
|
200
|
+
await writeJsonFile(taskJsonPath, taskJson);
|
|
201
|
+
const summary = [
|
|
202
|
+
`# Task ${taskId}`,
|
|
203
|
+
"",
|
|
204
|
+
task.summary ? task.summary : "No summary yet.",
|
|
205
|
+
"",
|
|
206
|
+
"## Metadata",
|
|
207
|
+
`- Escrow ID: ${task.escrowId ?? ""}`,
|
|
208
|
+
`- Category: ${task.category ?? ""}`,
|
|
209
|
+
`- Difficulty: ${task.difficulty ?? ""}`,
|
|
210
|
+
`- Reward: ${task.reward ?? ""}`,
|
|
211
|
+
`- Status: ${task.status ?? ""}`,
|
|
212
|
+
"",
|
|
213
|
+
"## Public materials",
|
|
214
|
+
...task.publicMaterials?.length ? task.publicMaterials.map((item) => `- ${item}`) : ["- none"],
|
|
215
|
+
"",
|
|
216
|
+
"## Confidential materials",
|
|
217
|
+
...task.confidentialMaterials?.length ? task.confidentialMaterials.map((item) => `- ${item}`) : ["- none"],
|
|
218
|
+
""
|
|
219
|
+
].join("\n");
|
|
220
|
+
await fs.writeFile(summaryPath, summary, "utf8");
|
|
221
|
+
if (!await pathExists(notesPath)) {
|
|
222
|
+
await fs.writeFile(notesPath, "# Delivery Notes\n\n", "utf8");
|
|
223
|
+
}
|
|
224
|
+
if (!await pathExists(proposalPath)) {
|
|
225
|
+
await fs.writeFile(proposalPath, "# Proposal\n\n", "utf8");
|
|
226
|
+
}
|
|
227
|
+
if (!await pathExists(manifestPath)) {
|
|
228
|
+
await writeJsonFile(manifestPath, {
|
|
229
|
+
taskId,
|
|
230
|
+
escrowId: task.escrowId ?? "",
|
|
231
|
+
category: task.category ?? "",
|
|
232
|
+
revision: 0,
|
|
233
|
+
artifacts: [],
|
|
234
|
+
checks: [],
|
|
235
|
+
notes: "",
|
|
236
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const { state } = await loadState();
|
|
240
|
+
state.recentTaskIds = keepRecent(uniqueTrimmed([...state.recentTaskIds, taskId]));
|
|
241
|
+
if (task.status && ["working", "confirmed", "in_revision", "active"].includes(String(task.status).toLowerCase())) {
|
|
242
|
+
state.activeTasks = uniqueTrimmed([...state.activeTasks, taskId]);
|
|
243
|
+
}
|
|
244
|
+
if (task.status && ["pending_confirmation", "confirmation_pending"].includes(String(task.status).toLowerCase())) {
|
|
245
|
+
state.pendingConfirmations = uniqueTrimmed([...state.pendingConfirmations, taskId]);
|
|
246
|
+
}
|
|
247
|
+
await saveState(state);
|
|
248
|
+
return {
|
|
249
|
+
taskRoot,
|
|
250
|
+
taskJsonPath,
|
|
251
|
+
summaryPath,
|
|
252
|
+
proposalPath,
|
|
253
|
+
manifestPath,
|
|
254
|
+
notesPath
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function writeMarkdown(target, lines) {
|
|
258
|
+
await ensureDir(path.dirname(target));
|
|
259
|
+
await fs.writeFile(target, lines.join("\n"), "utf8");
|
|
260
|
+
}
|
|
261
|
+
function stateSummaryLines(state) {
|
|
262
|
+
return [
|
|
263
|
+
`activeTasks: ${state.activeTasks.length}`,
|
|
264
|
+
`pendingConfirmations: ${state.pendingConfirmations.length}`,
|
|
265
|
+
`recentTaskIds: ${state.recentTaskIds.length}`,
|
|
266
|
+
`processedRevisionKeys: ${state.processedRevisionKeys.length}`,
|
|
267
|
+
`processedEventKeys: ${state.processedEventKeys.length}`,
|
|
268
|
+
`sentMessageKeys: ${state.sentMessageKeys.length}`,
|
|
269
|
+
`bidTaskIds: ${state.bidTaskIds.length}`
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
function analyzeTriage(input) {
|
|
273
|
+
const reasons = [];
|
|
274
|
+
const category = normalizeText(input.category).toLowerCase();
|
|
275
|
+
const difficulty = normalizeText(input.difficulty).toLowerCase();
|
|
276
|
+
const summary = normalizeText(input.summary);
|
|
277
|
+
const title = normalizeText(input.title);
|
|
278
|
+
const publicMaterials = normalizeStringArray(input.publicMaterials);
|
|
279
|
+
const requiredTags = normalizeStringArray(input.requiredTags).map((item) => item.toLowerCase());
|
|
280
|
+
const capabilityTags = normalizeStringArray(input.capabilityTags).map((item) => item.toLowerCase());
|
|
281
|
+
const activeTaskCount = typeof input.activeTaskCount === "number" ? input.activeTaskCount : 0;
|
|
282
|
+
let numericReward = 0;
|
|
283
|
+
if (typeof input.reward === "number") numericReward = input.reward;
|
|
284
|
+
if (typeof input.reward === "string") {
|
|
285
|
+
const match = input.reward.match(/\d+(\.\d+)?/);
|
|
286
|
+
numericReward = match ? Number(match[0]) : 0;
|
|
287
|
+
}
|
|
288
|
+
let riskLevel = "low";
|
|
289
|
+
let shouldBid = true;
|
|
290
|
+
let needsHumanReview = Boolean(input.requiresHumanReview);
|
|
291
|
+
if (!summary && !title) {
|
|
292
|
+
shouldBid = false;
|
|
293
|
+
riskLevel = "high";
|
|
294
|
+
appendReason(reasons, "Task has almost no usable description.");
|
|
295
|
+
}
|
|
296
|
+
if (difficulty === "complex" || difficulty === "expert") {
|
|
297
|
+
needsHumanReview = true;
|
|
298
|
+
riskLevel = "high";
|
|
299
|
+
appendReason(reasons, `Difficulty is ${difficulty}, so a human gate is recommended.`);
|
|
300
|
+
}
|
|
301
|
+
if (activeTaskCount >= 3) {
|
|
302
|
+
needsHumanReview = true;
|
|
303
|
+
riskLevel = riskLevel === "high" ? "high" : "medium";
|
|
304
|
+
appendReason(reasons, `Current active task count is ${activeTaskCount}, so capacity is tighter.`);
|
|
305
|
+
}
|
|
306
|
+
if (publicMaterials.length === 0) {
|
|
307
|
+
riskLevel = riskLevel === "high" ? "high" : "medium";
|
|
308
|
+
appendReason(reasons, "No public materials were provided yet.");
|
|
309
|
+
}
|
|
310
|
+
if (numericReward > 0 && numericReward < 20) {
|
|
311
|
+
riskLevel = riskLevel === "high" ? "high" : "medium";
|
|
312
|
+
appendReason(reasons, `Reward looks low (${numericReward}).`);
|
|
313
|
+
}
|
|
314
|
+
if (requiredTags.length > 0) {
|
|
315
|
+
const matched = requiredTags.filter((tag) => capabilityTags.includes(tag));
|
|
316
|
+
if (matched.length === 0 && capabilityTags.length > 0) {
|
|
317
|
+
shouldBid = false;
|
|
318
|
+
riskLevel = "high";
|
|
319
|
+
appendReason(reasons, "Required tags do not match declared capability tags.");
|
|
320
|
+
} else if (matched.length < requiredTags.length) {
|
|
321
|
+
needsHumanReview = true;
|
|
322
|
+
riskLevel = riskLevel === "high" ? "high" : "medium";
|
|
323
|
+
appendReason(reasons, "Capability tags only partially match required tags.");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (!["software", "writing", "research", "data", "visual", "video", "audio", "general", ""].includes(category)) {
|
|
327
|
+
needsHumanReview = true;
|
|
328
|
+
riskLevel = riskLevel === "high" ? "high" : "medium";
|
|
329
|
+
appendReason(reasons, `Category '${category}' is unfamiliar to the current heuristics.`);
|
|
330
|
+
}
|
|
331
|
+
if (summary.length > 0 && summary.length < 40) {
|
|
332
|
+
riskLevel = riskLevel === "high" ? "high" : "medium";
|
|
333
|
+
appendReason(reasons, "Summary is very short; scope may be too vague.");
|
|
334
|
+
}
|
|
335
|
+
const recommendation = !shouldBid ? "skip" : needsHumanReview ? "review" : "bid";
|
|
336
|
+
return {
|
|
337
|
+
recommendation,
|
|
338
|
+
shouldBid,
|
|
339
|
+
needsHumanReview,
|
|
340
|
+
riskLevel,
|
|
341
|
+
reasons,
|
|
342
|
+
category: category || "unknown",
|
|
343
|
+
difficulty: difficulty || "unknown",
|
|
344
|
+
reward: input.reward ?? ""
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function classifyRevisionItems(originalScope, revisionItems) {
|
|
348
|
+
const valid = [];
|
|
349
|
+
const ambiguous = [];
|
|
350
|
+
const scopeRisk = [];
|
|
351
|
+
for (const item of revisionItems) {
|
|
352
|
+
const lower = item.toLowerCase();
|
|
353
|
+
const matched = originalScope.some((scope) => scope && lower.includes(scope.toLowerCase()));
|
|
354
|
+
if (matched) {
|
|
355
|
+
valid.push(item);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (/(new|additional|extra|expand|broader|more feature|another)/i.test(item)) {
|
|
359
|
+
scopeRisk.push(item);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
ambiguous.push(item);
|
|
363
|
+
}
|
|
364
|
+
return { valid, ambiguous, scopeRisk };
|
|
365
|
+
}
|
|
366
|
+
function buildDeliveryChecks(input) {
|
|
367
|
+
const checks = [];
|
|
368
|
+
const artifacts = Array.isArray(input.artifacts) ? input.artifacts : [];
|
|
369
|
+
const acceptanceCriteria = normalizeStringArray(input.acceptanceCriteria);
|
|
370
|
+
const notes = normalizeText(input.notes);
|
|
371
|
+
checks.push({
|
|
372
|
+
name: "artifacts_present",
|
|
373
|
+
passed: artifacts.length > 0,
|
|
374
|
+
detail: artifacts.length > 0 ? `${artifacts.length} artifact(s) listed.` : "No artifacts were listed."
|
|
375
|
+
});
|
|
376
|
+
checks.push({
|
|
377
|
+
name: "notes_present",
|
|
378
|
+
passed: notes.length > 0,
|
|
379
|
+
detail: notes.length > 0 ? "Delivery notes provided." : "Delivery notes are missing."
|
|
380
|
+
});
|
|
381
|
+
checks.push({
|
|
382
|
+
name: "acceptance_criteria_recorded",
|
|
383
|
+
passed: acceptanceCriteria.length > 0,
|
|
384
|
+
detail: acceptanceCriteria.length > 0 ? `${acceptanceCriteria.length} acceptance criteria recorded.` : "No acceptance criteria recorded."
|
|
385
|
+
});
|
|
386
|
+
const artifactNames = artifacts.map((artifact) => `${artifact.name ?? ""} ${artifact.path ?? ""}`).join(" ").toLowerCase();
|
|
387
|
+
const notesLower = notes.toLowerCase();
|
|
388
|
+
const secretRegex = /(agent_pk|private_key|seed phrase|mnemonic|jwt|token|0x[a-f0-9]{32,})/i;
|
|
389
|
+
const suspicious = secretRegex.test(`${artifactNames} ${notesLower}`);
|
|
390
|
+
checks.push({
|
|
391
|
+
name: "secret_scan_basic",
|
|
392
|
+
passed: !suspicious,
|
|
393
|
+
detail: suspicious ? "Potential secret-like content detected in names or notes." : "No obvious secret-like patterns detected in names or notes."
|
|
394
|
+
});
|
|
395
|
+
return checks;
|
|
396
|
+
}
|
|
397
|
+
function analyzeConfirmationDelta(input) {
|
|
398
|
+
const publicSummary = normalizeText(input.publicSummary);
|
|
399
|
+
const confidentialSummary = normalizeText(input.confidentialSummary);
|
|
400
|
+
const publicMaterials = normalizeStringArray(input.publicMaterials);
|
|
401
|
+
const confidentialMaterials = normalizeStringArray(input.confidentialMaterials);
|
|
402
|
+
const difficulty = normalizeText(input.difficulty).toLowerCase();
|
|
403
|
+
const reasons = [];
|
|
404
|
+
let riskLevel = "low";
|
|
405
|
+
let recommendation = "confirm";
|
|
406
|
+
const summaryDelta = confidentialSummary && publicSummary && confidentialSummary !== publicSummary;
|
|
407
|
+
const confidentialOnly = confidentialMaterials.filter((item) => !publicMaterials.includes(item));
|
|
408
|
+
if (!confidentialSummary && confidentialMaterials.length === 0) {
|
|
409
|
+
riskLevel = "medium";
|
|
410
|
+
recommendation = "clarify";
|
|
411
|
+
appendReason(reasons, "Confidential detail payload is still thin; review may be incomplete.");
|
|
412
|
+
}
|
|
413
|
+
if (summaryDelta) {
|
|
414
|
+
riskLevel = "medium";
|
|
415
|
+
appendReason(reasons, "Confidential summary differs from the public summary.");
|
|
416
|
+
}
|
|
417
|
+
if (confidentialOnly.length >= 3) {
|
|
418
|
+
riskLevel = "high";
|
|
419
|
+
recommendation = "review";
|
|
420
|
+
appendReason(reasons, "Several confidential-only materials were introduced after the public phase.");
|
|
421
|
+
}
|
|
422
|
+
if (/(new|additional|extra|migration|integration|deployment|production|full stack|admin panel)/i.test(confidentialSummary)) {
|
|
423
|
+
riskLevel = "high";
|
|
424
|
+
recommendation = "review";
|
|
425
|
+
appendReason(reasons, "Confidential summary suggests additional scope or complexity.");
|
|
426
|
+
}
|
|
427
|
+
if (difficulty === "complex" || difficulty === "expert") {
|
|
428
|
+
riskLevel = "high";
|
|
429
|
+
recommendation = "review";
|
|
430
|
+
appendReason(reasons, `Difficulty is ${difficulty}, so confirmation should usually pass a human gate.`);
|
|
431
|
+
}
|
|
432
|
+
if (/(api key|credential|secret|private key|seed phrase)/i.test(confidentialSummary)) {
|
|
433
|
+
riskLevel = "high";
|
|
434
|
+
recommendation = "decline";
|
|
435
|
+
appendReason(reasons, "Confidential summary appears to request sensitive credentials or secrets.");
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
recommendation,
|
|
439
|
+
riskLevel,
|
|
440
|
+
reasons,
|
|
441
|
+
publicMaterialsCount: publicMaterials.length,
|
|
442
|
+
confidentialMaterialsCount: confidentialMaterials.length,
|
|
443
|
+
confidentialOnlyMaterials: confidentialOnly
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function buildProposalMarkdown(input) {
|
|
447
|
+
const category = normalizeText(input.category).toLowerCase() || "general";
|
|
448
|
+
const title = normalizeText(input.title) || input.taskId;
|
|
449
|
+
const summary = normalizeText(input.summary) || "No summary provided yet.";
|
|
450
|
+
const deliverables = normalizeStringArray(input.deliverables);
|
|
451
|
+
const risks = normalizeStringArray(input.risks);
|
|
452
|
+
const assumptions = normalizeStringArray(input.assumptions);
|
|
453
|
+
const intro = category === "software" ? "## Approach\n- I will break the work into implementation, validation, and delivery steps." : category === "writing" ? "## Approach\n- I will align the output to the requested audience, tone, and format." : category === "research" ? "## Approach\n- I will structure the work around source collection, synthesis, and a clear final output." : "## Approach\n- I will turn the task into a concrete execution plan and deliverable set.";
|
|
454
|
+
return [
|
|
455
|
+
`# Proposal for ${title}`,
|
|
456
|
+
"",
|
|
457
|
+
"## Task understanding",
|
|
458
|
+
`- Category: ${category}`,
|
|
459
|
+
`- Difficulty: ${normalizeText(input.difficulty) || "unspecified"}`,
|
|
460
|
+
`- Reward: ${input.reward ?? ""}`,
|
|
461
|
+
`- Summary: ${summary}`,
|
|
462
|
+
"",
|
|
463
|
+
intro,
|
|
464
|
+
"",
|
|
465
|
+
"## Deliverables",
|
|
466
|
+
...deliverables.length ? deliverables.map((item) => `- ${item}`) : ["- Deliverables will follow the confirmed task requirements."],
|
|
467
|
+
"",
|
|
468
|
+
"## Risks and assumptions",
|
|
469
|
+
...risks.length ? risks.map((item) => `- Risk: ${item}`) : ["- Risk: scope or dependency changes after confidential review may require clarification."],
|
|
470
|
+
...assumptions.length ? assumptions.map((item) => `- Assumption: ${item}`) : ["- Assumption: provided materials are sufficient to begin and finish the task."],
|
|
471
|
+
"",
|
|
472
|
+
"## Delivery intent",
|
|
473
|
+
"- I will keep the work aligned to the stated criteria, communicate early when blocked, and submit with clear delivery notes.",
|
|
474
|
+
""
|
|
475
|
+
].join("\n");
|
|
476
|
+
}
|
|
477
|
+
function buildHeartbeatPlan(state) {
|
|
478
|
+
if ((state.pendingConfirmations?.length ?? 0) > 0) {
|
|
479
|
+
return {
|
|
480
|
+
priority: "pending_confirmations",
|
|
481
|
+
suggestedActions: [
|
|
482
|
+
"review pending confirmations first",
|
|
483
|
+
"run confirmation delta review before confirm/decline"
|
|
484
|
+
]
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if ((state.activeTasks?.length ?? 0) > 0) {
|
|
488
|
+
return {
|
|
489
|
+
priority: "active_tasks",
|
|
490
|
+
suggestedActions: [
|
|
491
|
+
"check active task deadlines",
|
|
492
|
+
"check for revisions or chat that need action"
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
priority: "discovery",
|
|
498
|
+
suggestedActions: [
|
|
499
|
+
"poll events",
|
|
500
|
+
"discover new tasks if capacity allows",
|
|
501
|
+
"triage before bidding"
|
|
502
|
+
]
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function register(api) {
|
|
506
|
+
api?.logger?.info?.("AgentPact OpenClaw integration loaded (MCP-first mode)");
|
|
507
|
+
api.registerTool({
|
|
508
|
+
name: "agentpact_openclaw_help",
|
|
509
|
+
description: "Explain how the AgentPact OpenClaw integration works in MCP-first mode and what to configure.",
|
|
510
|
+
parameters: {
|
|
511
|
+
type: "object",
|
|
512
|
+
additionalProperties: false,
|
|
513
|
+
properties: {}
|
|
514
|
+
},
|
|
515
|
+
optional: true,
|
|
516
|
+
execute: async () => {
|
|
517
|
+
const mcpServerName = getMcpServerName(api);
|
|
518
|
+
return textResult(
|
|
519
|
+
[
|
|
520
|
+
"AgentPact OpenClaw integration is running in MCP-first mode.",
|
|
521
|
+
"",
|
|
522
|
+
`Expected MCP server name: ${mcpServerName}`,
|
|
523
|
+
"",
|
|
524
|
+
"What this plugin now provides:",
|
|
525
|
+
"- bundled AgentPact skill files",
|
|
526
|
+
"- bundled heartbeat guidance",
|
|
527
|
+
"- OpenClaw-specific docs/templates/examples",
|
|
528
|
+
"- local task workspace helpers",
|
|
529
|
+
"- local state / idempotency helpers",
|
|
530
|
+
"- triage / revision / delivery preparation helpers",
|
|
531
|
+
"",
|
|
532
|
+
"What provides the actual AgentPact tools:",
|
|
533
|
+
"- @agentpactai/mcp-server"
|
|
534
|
+
].join("\n")
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
api.registerTool({
|
|
539
|
+
name: "agentpact_openclaw_status",
|
|
540
|
+
description: "Check MCP-related OpenClaw configuration, local state file presence, and task workspace root.",
|
|
541
|
+
parameters: {
|
|
542
|
+
type: "object",
|
|
543
|
+
additionalProperties: false,
|
|
544
|
+
properties: {}
|
|
545
|
+
},
|
|
546
|
+
optional: true,
|
|
547
|
+
execute: async () => {
|
|
548
|
+
const mcpServerName = getMcpServerName(api);
|
|
549
|
+
const { configPath, exists, config } = await loadOpenClawConfig();
|
|
550
|
+
const mcpServers = config?.mcpServers ?? {};
|
|
551
|
+
const mcpEntry = mcpServers?.[mcpServerName];
|
|
552
|
+
const statePath = getStatePath();
|
|
553
|
+
const taskRoot = getTasksRoot();
|
|
554
|
+
const stateExists = await pathExists(statePath);
|
|
555
|
+
const taskRootExists = await pathExists(taskRoot);
|
|
556
|
+
return jsonResult({
|
|
557
|
+
mode: "mcp-first",
|
|
558
|
+
openclawConfigPath: configPath,
|
|
559
|
+
openclawConfigExists: exists,
|
|
560
|
+
expectedMcpServerName: mcpServerName,
|
|
561
|
+
mcpServerConfigured: Boolean(mcpEntry),
|
|
562
|
+
mcpServerConfig: mcpEntry ?? null,
|
|
563
|
+
statePath,
|
|
564
|
+
stateExists,
|
|
565
|
+
tasksRoot: taskRoot,
|
|
566
|
+
tasksRootExists: taskRootExists
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
api.registerTool({
|
|
571
|
+
name: "agentpact_openclaw_state_get",
|
|
572
|
+
description: "Read the local AgentPact OpenClaw state file used for heartbeat/idempotency tracking.",
|
|
573
|
+
parameters: {
|
|
574
|
+
type: "object",
|
|
575
|
+
additionalProperties: false,
|
|
576
|
+
properties: {
|
|
577
|
+
ensureExists: { type: "boolean", default: true }
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
optional: true,
|
|
581
|
+
execute: async (params = {}) => {
|
|
582
|
+
const { statePath, state } = await loadState();
|
|
583
|
+
if (params.ensureExists !== false) {
|
|
584
|
+
await saveState(state);
|
|
585
|
+
}
|
|
586
|
+
return jsonResult({ statePath, state, summary: stateSummaryLines(state) });
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
api.registerTool({
|
|
590
|
+
name: "agentpact_openclaw_state_update",
|
|
591
|
+
description: "Update local AgentPact OpenClaw state fields such as active tasks, pending confirmations, or recent ids.",
|
|
592
|
+
parameters: {
|
|
593
|
+
type: "object",
|
|
594
|
+
additionalProperties: false,
|
|
595
|
+
properties: {
|
|
596
|
+
set: { type: "object" },
|
|
597
|
+
addActiveTaskIds: { type: "array", items: { type: "string" } },
|
|
598
|
+
removeActiveTaskIds: { type: "array", items: { type: "string" } },
|
|
599
|
+
addPendingConfirmationIds: { type: "array", items: { type: "string" } },
|
|
600
|
+
removePendingConfirmationIds: { type: "array", items: { type: "string" } },
|
|
601
|
+
addRecentTaskIds: { type: "array", items: { type: "string" } }
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
optional: true,
|
|
605
|
+
execute: async (params = {}) => {
|
|
606
|
+
const { state } = await loadState();
|
|
607
|
+
if (params.set && typeof params.set === "object") {
|
|
608
|
+
Object.assign(state, params.set);
|
|
609
|
+
}
|
|
610
|
+
if (Array.isArray(params.addActiveTaskIds)) {
|
|
611
|
+
state.activeTasks = uniqueTrimmed([...state.activeTasks, ...params.addActiveTaskIds]);
|
|
612
|
+
}
|
|
613
|
+
if (Array.isArray(params.removeActiveTaskIds)) {
|
|
614
|
+
const remove = new Set(uniqueTrimmed(params.removeActiveTaskIds));
|
|
615
|
+
state.activeTasks = state.activeTasks.filter((item) => !remove.has(item));
|
|
616
|
+
}
|
|
617
|
+
if (Array.isArray(params.addPendingConfirmationIds)) {
|
|
618
|
+
state.pendingConfirmations = uniqueTrimmed([...state.pendingConfirmations, ...params.addPendingConfirmationIds]);
|
|
619
|
+
}
|
|
620
|
+
if (Array.isArray(params.removePendingConfirmationIds)) {
|
|
621
|
+
const remove = new Set(uniqueTrimmed(params.removePendingConfirmationIds));
|
|
622
|
+
state.pendingConfirmations = state.pendingConfirmations.filter((item) => !remove.has(item));
|
|
623
|
+
}
|
|
624
|
+
if (Array.isArray(params.addRecentTaskIds)) {
|
|
625
|
+
state.recentTaskIds = keepRecent(uniqueTrimmed([...state.recentTaskIds, ...params.addRecentTaskIds]));
|
|
626
|
+
}
|
|
627
|
+
const statePath = await saveState(state);
|
|
628
|
+
return jsonResult({ statePath, state, summary: stateSummaryLines(state) });
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
api.registerTool({
|
|
632
|
+
name: "agentpact_openclaw_workspace_init",
|
|
633
|
+
description: "Create or update a local task workspace for an AgentPact task and seed basic task files.",
|
|
634
|
+
parameters: {
|
|
635
|
+
type: "object",
|
|
636
|
+
additionalProperties: false,
|
|
637
|
+
properties: {
|
|
638
|
+
taskId: { type: "string" },
|
|
639
|
+
escrowId: { type: "string" },
|
|
640
|
+
category: { type: "string" },
|
|
641
|
+
difficulty: { type: "string" },
|
|
642
|
+
reward: { type: ["string", "number"] },
|
|
643
|
+
status: { type: "string" },
|
|
644
|
+
summary: { type: "string" },
|
|
645
|
+
publicMaterials: { type: "array", items: { type: "string" } },
|
|
646
|
+
confidentialMaterials: { type: "array", items: { type: "string" } }
|
|
647
|
+
},
|
|
648
|
+
required: ["taskId"]
|
|
649
|
+
},
|
|
650
|
+
optional: true,
|
|
651
|
+
execute: async (params) => {
|
|
652
|
+
const result = await ensureWorkspace(params);
|
|
653
|
+
return jsonResult(result);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
api.registerTool({
|
|
657
|
+
name: "agentpact_openclaw_mark_processed",
|
|
658
|
+
description: "Record processed event/revision/message/bid keys to support idempotent semi-automated workflows.",
|
|
659
|
+
parameters: {
|
|
660
|
+
type: "object",
|
|
661
|
+
additionalProperties: false,
|
|
662
|
+
properties: {
|
|
663
|
+
kind: {
|
|
664
|
+
type: "string",
|
|
665
|
+
enum: ["event", "revision", "message", "bid"]
|
|
666
|
+
},
|
|
667
|
+
key: { type: "string" }
|
|
668
|
+
},
|
|
669
|
+
required: ["kind", "key"]
|
|
670
|
+
},
|
|
671
|
+
optional: true,
|
|
672
|
+
execute: async (params) => {
|
|
673
|
+
const { state } = await loadState();
|
|
674
|
+
const key = params.key.trim();
|
|
675
|
+
if (!key) throw new Error("key is required");
|
|
676
|
+
if (params.kind === "event") {
|
|
677
|
+
state.processedEventKeys = keepRecent(uniqueTrimmed([...state.processedEventKeys, key]));
|
|
678
|
+
} else if (params.kind === "revision") {
|
|
679
|
+
state.processedRevisionKeys = keepRecent(uniqueTrimmed([...state.processedRevisionKeys, key]));
|
|
680
|
+
} else if (params.kind === "message") {
|
|
681
|
+
state.sentMessageKeys = keepRecent(uniqueTrimmed([...state.sentMessageKeys, key]));
|
|
682
|
+
} else if (params.kind === "bid") {
|
|
683
|
+
state.bidTaskIds = keepRecent(uniqueTrimmed([...state.bidTaskIds, key]));
|
|
684
|
+
}
|
|
685
|
+
const statePath = await saveState(state);
|
|
686
|
+
return jsonResult({ statePath, state, summary: stateSummaryLines(state) });
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
api.registerTool({
|
|
690
|
+
name: "agentpact_openclaw_triage_task",
|
|
691
|
+
description: "Perform a local host-side triage pass for a task and persist the result in the task workspace.",
|
|
692
|
+
parameters: {
|
|
693
|
+
type: "object",
|
|
694
|
+
additionalProperties: false,
|
|
695
|
+
properties: {
|
|
696
|
+
taskId: { type: "string" },
|
|
697
|
+
title: { type: "string" },
|
|
698
|
+
summary: { type: "string" },
|
|
699
|
+
category: { type: "string" },
|
|
700
|
+
difficulty: { type: "string" },
|
|
701
|
+
reward: { type: ["string", "number"] },
|
|
702
|
+
publicMaterials: { type: "array", items: { type: "string" } },
|
|
703
|
+
activeTaskCount: { type: "number" },
|
|
704
|
+
capabilityTags: { type: "array", items: { type: "string" } },
|
|
705
|
+
requiredTags: { type: "array", items: { type: "string" } },
|
|
706
|
+
requiresHumanReview: { type: "boolean" }
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
optional: true,
|
|
710
|
+
execute: async (params = {}) => {
|
|
711
|
+
const triage = analyzeTriage(params);
|
|
712
|
+
if (params.taskId && normalizeText(params.taskId)) {
|
|
713
|
+
await ensureWorkspace({
|
|
714
|
+
taskId: params.taskId,
|
|
715
|
+
category: params.category,
|
|
716
|
+
difficulty: params.difficulty,
|
|
717
|
+
reward: params.reward,
|
|
718
|
+
summary: params.summary || params.title,
|
|
719
|
+
publicMaterials: params.publicMaterials
|
|
720
|
+
});
|
|
721
|
+
const triagePath = path.join(getTaskRoot(params.taskId), "triage.json");
|
|
722
|
+
await writeJsonFile(triagePath, {
|
|
723
|
+
...triage,
|
|
724
|
+
title: params.title ?? "",
|
|
725
|
+
summary: params.summary ?? "",
|
|
726
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return jsonResult(triage);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
api.registerTool({
|
|
733
|
+
name: "agentpact_openclaw_prepare_revision",
|
|
734
|
+
description: "Create a structured local revision analysis for a task and classify items into valid, ambiguous, or scope-risk buckets.",
|
|
735
|
+
parameters: {
|
|
736
|
+
type: "object",
|
|
737
|
+
additionalProperties: false,
|
|
738
|
+
properties: {
|
|
739
|
+
taskId: { type: "string" },
|
|
740
|
+
revision: { type: "number" },
|
|
741
|
+
originalScope: { type: "array", items: { type: "string" } },
|
|
742
|
+
revisionItems: { type: "array", items: { type: "string" } },
|
|
743
|
+
requesterComments: { type: "array", items: { type: "string" } }
|
|
744
|
+
},
|
|
745
|
+
required: ["taskId"]
|
|
746
|
+
},
|
|
747
|
+
optional: true,
|
|
748
|
+
execute: async (params) => {
|
|
749
|
+
const revisionNumber = typeof params.revision === "number" && params.revision > 0 ? params.revision : 1;
|
|
750
|
+
await ensureWorkspace({ taskId: params.taskId, status: "in_revision" });
|
|
751
|
+
const originalScope = normalizeStringArray(params.originalScope);
|
|
752
|
+
const revisionItems = normalizeStringArray(params.revisionItems);
|
|
753
|
+
const requesterComments = normalizeStringArray(params.requesterComments);
|
|
754
|
+
const classified = classifyRevisionItems(originalScope, revisionItems);
|
|
755
|
+
const recommendation = classified.scopeRisk.length > 0 ? "clarify_scope_before_full_execution" : classified.ambiguous.length > 0 ? "review_then_execute" : "execute_revision";
|
|
756
|
+
const revisionDir = path.join(getTaskRoot(params.taskId), "revisions", `rev-${revisionNumber}`);
|
|
757
|
+
await ensureDir(revisionDir);
|
|
758
|
+
const analysisPath = path.join(revisionDir, "analysis.md");
|
|
759
|
+
await writeMarkdown(analysisPath, [
|
|
760
|
+
"# Revision Analysis",
|
|
761
|
+
"",
|
|
762
|
+
`- Task: ${params.taskId}`,
|
|
763
|
+
`- Revision: ${revisionNumber}`,
|
|
764
|
+
`- Recommendation: ${recommendation}`,
|
|
765
|
+
"",
|
|
766
|
+
"## Original scope",
|
|
767
|
+
...originalScope.length ? originalScope.map((item) => `- ${item}`) : ["- none recorded"],
|
|
768
|
+
"",
|
|
769
|
+
"## Revision items: valid",
|
|
770
|
+
...classified.valid.length ? classified.valid.map((item) => `- ${item}`) : ["- none"],
|
|
771
|
+
"",
|
|
772
|
+
"## Revision items: ambiguous",
|
|
773
|
+
...classified.ambiguous.length ? classified.ambiguous.map((item) => `- ${item}`) : ["- none"],
|
|
774
|
+
"",
|
|
775
|
+
"## Revision items: possible scope expansion",
|
|
776
|
+
...classified.scopeRisk.length ? classified.scopeRisk.map((item) => `- ${item}`) : ["- none"],
|
|
777
|
+
"",
|
|
778
|
+
"## Requester comments",
|
|
779
|
+
...requesterComments.length ? requesterComments.map((item) => `- ${item}`) : ["- none"],
|
|
780
|
+
""
|
|
781
|
+
]);
|
|
782
|
+
const revisionKey = `${params.taskId}:rev:${revisionNumber}`;
|
|
783
|
+
const { state } = await loadState();
|
|
784
|
+
state.processedRevisionKeys = keepRecent(uniqueTrimmed([...state.processedRevisionKeys, revisionKey]));
|
|
785
|
+
await saveState(state);
|
|
786
|
+
return jsonResult({
|
|
787
|
+
taskId: params.taskId,
|
|
788
|
+
revision: revisionNumber,
|
|
789
|
+
recommendation,
|
|
790
|
+
classified,
|
|
791
|
+
analysisPath
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
api.registerTool({
|
|
796
|
+
name: "agentpact_openclaw_prepare_delivery",
|
|
797
|
+
description: "Run a local delivery preflight, update the delivery manifest, and return the checklist result.",
|
|
798
|
+
parameters: {
|
|
799
|
+
type: "object",
|
|
800
|
+
additionalProperties: false,
|
|
801
|
+
properties: {
|
|
802
|
+
taskId: { type: "string" },
|
|
803
|
+
escrowId: { type: "string" },
|
|
804
|
+
category: { type: "string" },
|
|
805
|
+
revision: { type: "number" },
|
|
806
|
+
artifacts: {
|
|
807
|
+
type: "array",
|
|
808
|
+
items: {
|
|
809
|
+
type: "object",
|
|
810
|
+
additionalProperties: false,
|
|
811
|
+
properties: {
|
|
812
|
+
name: { type: "string" },
|
|
813
|
+
path: { type: "string" },
|
|
814
|
+
type: { type: "string" }
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
checks: { type: "array", items: { type: "string" } },
|
|
819
|
+
notes: { type: "string" },
|
|
820
|
+
acceptanceCriteria: { type: "array", items: { type: "string" } }
|
|
821
|
+
},
|
|
822
|
+
required: ["taskId"]
|
|
823
|
+
},
|
|
824
|
+
optional: true,
|
|
825
|
+
execute: async (params) => {
|
|
826
|
+
await ensureWorkspace({
|
|
827
|
+
taskId: params.taskId,
|
|
828
|
+
escrowId: params.escrowId,
|
|
829
|
+
category: params.category,
|
|
830
|
+
status: "working"
|
|
831
|
+
});
|
|
832
|
+
const taskRoot = getTaskRoot(params.taskId);
|
|
833
|
+
const deliveryDir = path.join(taskRoot, "delivery");
|
|
834
|
+
const manifestPath = path.join(deliveryDir, "manifest.json");
|
|
835
|
+
const notesPath = path.join(deliveryDir, "notes.md");
|
|
836
|
+
const preflightChecks = buildDeliveryChecks(params);
|
|
837
|
+
const allPassed = preflightChecks.every((check) => check.passed);
|
|
838
|
+
const manifest = {
|
|
839
|
+
taskId: params.taskId,
|
|
840
|
+
escrowId: params.escrowId ?? "",
|
|
841
|
+
category: params.category ?? "",
|
|
842
|
+
revision: typeof params.revision === "number" ? params.revision : 0,
|
|
843
|
+
artifacts: Array.isArray(params.artifacts) ? params.artifacts : [],
|
|
844
|
+
checks: [
|
|
845
|
+
...normalizeStringArray(params.checks),
|
|
846
|
+
...preflightChecks.map((item) => `${item.name}:${item.passed ? "pass" : "fail"}`)
|
|
847
|
+
],
|
|
848
|
+
notes: params.notes ?? "",
|
|
849
|
+
acceptanceCriteria: normalizeStringArray(params.acceptanceCriteria),
|
|
850
|
+
preflightChecks,
|
|
851
|
+
readyToSubmit: allPassed,
|
|
852
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
853
|
+
};
|
|
854
|
+
await writeJsonFile(manifestPath, manifest);
|
|
855
|
+
await writeMarkdown(notesPath, [
|
|
856
|
+
"# Delivery Notes",
|
|
857
|
+
"",
|
|
858
|
+
params.notes && params.notes.trim() ? params.notes.trim() : "No delivery notes provided yet.",
|
|
859
|
+
"",
|
|
860
|
+
"## Preflight",
|
|
861
|
+
...preflightChecks.map((item) => `- ${item.name}: ${item.passed ? "PASS" : "FAIL"} \u2014 ${item.detail}`),
|
|
862
|
+
""
|
|
863
|
+
]);
|
|
864
|
+
return jsonResult({
|
|
865
|
+
taskId: params.taskId,
|
|
866
|
+
readyToSubmit: allPassed,
|
|
867
|
+
preflightChecks,
|
|
868
|
+
manifestPath,
|
|
869
|
+
notesPath
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
api.registerTool({
|
|
874
|
+
name: "agentpact_openclaw_review_confirmation",
|
|
875
|
+
description: "Compare public vs confidential task detail and write a local confirmation delta review.",
|
|
876
|
+
parameters: {
|
|
877
|
+
type: "object",
|
|
878
|
+
additionalProperties: false,
|
|
879
|
+
properties: {
|
|
880
|
+
taskId: { type: "string" },
|
|
881
|
+
publicSummary: { type: "string" },
|
|
882
|
+
confidentialSummary: { type: "string" },
|
|
883
|
+
publicMaterials: { type: "array", items: { type: "string" } },
|
|
884
|
+
confidentialMaterials: { type: "array", items: { type: "string" } },
|
|
885
|
+
difficulty: { type: "string" },
|
|
886
|
+
reward: { type: ["string", "number"] }
|
|
887
|
+
},
|
|
888
|
+
required: ["taskId"]
|
|
889
|
+
},
|
|
890
|
+
optional: true,
|
|
891
|
+
execute: async (params) => {
|
|
892
|
+
await ensureWorkspace({
|
|
893
|
+
taskId: params.taskId,
|
|
894
|
+
difficulty: params.difficulty,
|
|
895
|
+
reward: params.reward,
|
|
896
|
+
summary: params.confidentialSummary || params.publicSummary,
|
|
897
|
+
publicMaterials: params.publicMaterials,
|
|
898
|
+
confidentialMaterials: params.confidentialMaterials,
|
|
899
|
+
status: "confirmation_pending"
|
|
900
|
+
});
|
|
901
|
+
const review = analyzeConfirmationDelta(params);
|
|
902
|
+
const reviewPath = path.join(getTaskRoot(params.taskId), "confirmation-review.md");
|
|
903
|
+
await writeMarkdown(reviewPath, [
|
|
904
|
+
"# Confirmation Review",
|
|
905
|
+
"",
|
|
906
|
+
`- Task: ${params.taskId}`,
|
|
907
|
+
`- Recommendation: ${review.recommendation}`,
|
|
908
|
+
`- Risk level: ${review.riskLevel}`,
|
|
909
|
+
"",
|
|
910
|
+
"## Reasons",
|
|
911
|
+
...review.reasons.length ? review.reasons.map((item) => `- ${item}`) : ["- no major deltas detected"],
|
|
912
|
+
"",
|
|
913
|
+
"## Public summary",
|
|
914
|
+
normalizeText(params.publicSummary) || "(none)",
|
|
915
|
+
"",
|
|
916
|
+
"## Confidential summary",
|
|
917
|
+
normalizeText(params.confidentialSummary) || "(none)",
|
|
918
|
+
"",
|
|
919
|
+
"## Confidential-only materials",
|
|
920
|
+
...review.confidentialOnlyMaterials.length ? review.confidentialOnlyMaterials.map((item) => `- ${item}`) : ["- none"],
|
|
921
|
+
""
|
|
922
|
+
]);
|
|
923
|
+
const { state } = await loadState();
|
|
924
|
+
state.pendingConfirmations = uniqueTrimmed([...state.pendingConfirmations, params.taskId]);
|
|
925
|
+
await saveState(state);
|
|
926
|
+
return jsonResult({ ...review, reviewPath });
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
api.registerTool({
|
|
930
|
+
name: "agentpact_openclaw_prepare_proposal",
|
|
931
|
+
description: "Generate or refresh a local proposal draft for a task inside the task workspace.",
|
|
932
|
+
parameters: {
|
|
933
|
+
type: "object",
|
|
934
|
+
additionalProperties: false,
|
|
935
|
+
properties: {
|
|
936
|
+
taskId: { type: "string" },
|
|
937
|
+
title: { type: "string" },
|
|
938
|
+
summary: { type: "string" },
|
|
939
|
+
category: { type: "string" },
|
|
940
|
+
difficulty: { type: "string" },
|
|
941
|
+
reward: { type: ["string", "number"] },
|
|
942
|
+
deliverables: { type: "array", items: { type: "string" } },
|
|
943
|
+
risks: { type: "array", items: { type: "string" } },
|
|
944
|
+
assumptions: { type: "array", items: { type: "string" } }
|
|
945
|
+
},
|
|
946
|
+
required: ["taskId"]
|
|
947
|
+
},
|
|
948
|
+
optional: true,
|
|
949
|
+
execute: async (params) => {
|
|
950
|
+
const workspace = await ensureWorkspace({
|
|
951
|
+
taskId: params.taskId,
|
|
952
|
+
category: params.category,
|
|
953
|
+
difficulty: params.difficulty,
|
|
954
|
+
reward: params.reward,
|
|
955
|
+
summary: params.summary || params.title
|
|
956
|
+
});
|
|
957
|
+
const proposal = buildProposalMarkdown(params);
|
|
958
|
+
await fs.writeFile(workspace.proposalPath, proposal, "utf8");
|
|
959
|
+
return jsonResult({ proposalPath: workspace.proposalPath, taskId: params.taskId });
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
api.registerTool({
|
|
963
|
+
name: "agentpact_openclaw_heartbeat_plan",
|
|
964
|
+
description: "Return a minimal next-step plan for the current AgentPact heartbeat based on local state.",
|
|
965
|
+
parameters: {
|
|
966
|
+
type: "object",
|
|
967
|
+
additionalProperties: false,
|
|
968
|
+
properties: {}
|
|
969
|
+
},
|
|
970
|
+
optional: true,
|
|
971
|
+
execute: async () => {
|
|
972
|
+
const { statePath, state } = await loadState();
|
|
973
|
+
return jsonResult({ statePath, plan: buildHeartbeatPlan(state), summary: stateSummaryLines(state) });
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}
|