forge-openclaw-plugin 0.2.15 → 0.2.18
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 +6 -3
- package/dist/assets/{board-C_m78kvK.js → board-2KevHCI0.js} +2 -2
- package/dist/assets/{board-C_m78kvK.js.map → board-2KevHCI0.js.map} +1 -1
- package/dist/assets/index-CDYW4WDH.js +36 -0
- package/dist/assets/index-CDYW4WDH.js.map +1 -0
- package/dist/assets/index-yroQr6YZ.css +1 -0
- package/dist/assets/{motion-CpZvZumD.js → motion-q19HPmWs.js} +2 -2
- package/dist/assets/{motion-CpZvZumD.js.map → motion-q19HPmWs.js.map} +1 -1
- package/dist/assets/{table-DtyXTw03.js → table-BDMHBY4a.js} +2 -2
- package/dist/assets/{table-DtyXTw03.js.map → table-BDMHBY4a.js.map} +1 -1
- package/dist/assets/{ui-BXbpiKyS.js → ui-CQ_AsFs8.js} +2 -2
- package/dist/assets/{ui-BXbpiKyS.js.map → ui-CQ_AsFs8.js.map} +1 -1
- package/dist/assets/{vendor-QBH6qVEe.js → vendor-5HifrnRK.js} +90 -75
- package/dist/assets/{vendor-QBH6qVEe.js.map → vendor-5HifrnRK.js.map} +1 -1
- package/dist/assets/{viz-w-IMeueL.js → viz-CQzkRnTu.js} +2 -2
- package/dist/assets/{viz-w-IMeueL.js.map → viz-CQzkRnTu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/local-runtime.js +142 -9
- package/dist/openclaw/plugin-entry-shared.js +7 -1
- package/dist/openclaw/tools.js +15 -0
- package/dist/server/app.js +129 -11
- package/dist/server/openapi.js +181 -4
- package/dist/server/repositories/habits.js +358 -0
- package/dist/server/repositories/rewards.js +62 -0
- package/dist/server/services/context.js +16 -6
- package/dist/server/services/dashboard.js +6 -3
- package/dist/server/services/entity-crud.js +23 -1
- package/dist/server/services/gamification.js +66 -18
- package/dist/server/services/insights.js +2 -1
- package/dist/server/services/reviews.js +2 -1
- package/dist/server/types.js +140 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/003_habits.sql +30 -0
- package/server/migrations/004_habit_links.sql +8 -0
- package/server/migrations/005_habit_psyche_links.sql +24 -0
- package/skills/forge-openclaw/SKILL.md +16 -2
- package/skills/forge-openclaw/cron_jobs.md +395 -0
- package/dist/assets/index-BWtLtXwb.js +0 -36
- package/dist/assets/index-BWtLtXwb.js.map +0 -1
- package/dist/assets/index-Dp5GXY_z.css +0 -1
package/dist/index.html
CHANGED
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
/>
|
|
14
14
|
<link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
15
15
|
<link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
16
|
-
<script type="module" crossorigin src="/forge/assets/index-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/motion-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/ui-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/table-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/viz-
|
|
22
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/board-
|
|
16
|
+
<script type="module" crossorigin src="/forge/assets/index-CDYW4WDH.js"></script>
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-5HifrnRK.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/motion-q19HPmWs.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/ui-CQ_AsFs8.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/table-BDMHBY4a.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/viz-CQzkRnTu.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/board-2KevHCI0.js">
|
|
23
23
|
<link rel="stylesheet" crossorigin href="/forge/assets/vendor-CRS-psbw.css">
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/forge/assets/index-
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/forge/assets/index-yroQr6YZ.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body class="bg-canvas text-ink antialiased">
|
|
27
27
|
<div id="root"></div>
|
|
@@ -47,6 +47,25 @@ function applyPortToConfig(config, port, portSource) {
|
|
|
47
47
|
config.webAppUrl = buildForgeWebAppUrl(config.origin, port);
|
|
48
48
|
config.portSource = portSource;
|
|
49
49
|
}
|
|
50
|
+
function getExpectedDataRoot(config) {
|
|
51
|
+
return config.dataRoot.trim().length > 0 ? path.resolve(config.dataRoot) : null;
|
|
52
|
+
}
|
|
53
|
+
function isExpectedDataRoot(expectedDataRoot, actualDataRoot) {
|
|
54
|
+
if (!expectedDataRoot) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (!actualDataRoot) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return path.resolve(actualDataRoot) === expectedDataRoot;
|
|
61
|
+
}
|
|
62
|
+
function formatRuntimeDataRootMismatch(config, expectedDataRoot, actualDataRoot) {
|
|
63
|
+
return [
|
|
64
|
+
`Forge is already responding on ${config.baseUrl}, but it is using storage root ${actualDataRoot ?? "(unknown)"}.`,
|
|
65
|
+
`The OpenClaw plugin is configured to use ${expectedDataRoot}.`,
|
|
66
|
+
"Restart the plugin-managed runtime or stop the conflicting Forge server so the configured dataRoot can take over."
|
|
67
|
+
].join(" ");
|
|
68
|
+
}
|
|
50
69
|
async function writePreferredPortState(config, port) {
|
|
51
70
|
const statePath = getPreferredPortStatePath(config.origin);
|
|
52
71
|
await mkdir(path.dirname(statePath), { recursive: true });
|
|
@@ -272,6 +291,43 @@ async function isForgeHealthy(config, timeoutMs) {
|
|
|
272
291
|
clearTimeout(timeout);
|
|
273
292
|
}
|
|
274
293
|
}
|
|
294
|
+
async function probeForgeRuntime(config, timeoutMs) {
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(new URL("/api/v1/health", config.baseUrl), {
|
|
299
|
+
method: "GET",
|
|
300
|
+
headers: {
|
|
301
|
+
accept: "application/json",
|
|
302
|
+
"x-forge-runtime-probe": "1"
|
|
303
|
+
},
|
|
304
|
+
signal: controller.signal
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
return { healthy: false, pid: null, storageRoot: null, basePath: null };
|
|
308
|
+
}
|
|
309
|
+
const payload = (await response.json());
|
|
310
|
+
return {
|
|
311
|
+
healthy: true,
|
|
312
|
+
pid: typeof payload.runtime?.pid === "number" && Number.isFinite(payload.runtime.pid) ? Math.trunc(payload.runtime.pid) : null,
|
|
313
|
+
storageRoot: typeof payload.runtime?.storageRoot === "string" ? path.resolve(payload.runtime.storageRoot) : null,
|
|
314
|
+
basePath: typeof payload.runtime?.basePath === "string" ? payload.runtime.basePath : null
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return { healthy: false, pid: null, storageRoot: null, basePath: null };
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
clearTimeout(timeout);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function adoptManagedRuntimeState(config, probe) {
|
|
325
|
+
if (probe.pid === null || !processExists(probe.pid)) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
await writeRuntimeState(config, probe.pid);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
275
331
|
async function spawnManagedRuntime(config, plan) {
|
|
276
332
|
const isPackagedServer = isPackagedServerPlan(plan);
|
|
277
333
|
const args = isPackagedServer ? [plan.entryFile] : [plan.entryFile, path.join(plan.packageRoot, "server", "src", "index.ts")];
|
|
@@ -353,7 +409,13 @@ export async function ensureForgeRuntimeReady(config) {
|
|
|
353
409
|
if (!isLocalOrigin(config.origin)) {
|
|
354
410
|
return;
|
|
355
411
|
}
|
|
356
|
-
|
|
412
|
+
const expectedDataRoot = getExpectedDataRoot(config);
|
|
413
|
+
const initialProbe = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
414
|
+
if (initialProbe.healthy && isExpectedDataRoot(expectedDataRoot, initialProbe.storageRoot)) {
|
|
415
|
+
const existingState = await readRuntimeState(config);
|
|
416
|
+
if (!existingState) {
|
|
417
|
+
await adoptManagedRuntimeState(config, initialProbe);
|
|
418
|
+
}
|
|
357
419
|
return;
|
|
358
420
|
}
|
|
359
421
|
const savedState = await readRuntimeState(config);
|
|
@@ -361,12 +423,29 @@ export async function ensureForgeRuntimeReady(config) {
|
|
|
361
423
|
await clearRuntimeState(config);
|
|
362
424
|
}
|
|
363
425
|
else if (savedState && processExists(savedState.pid)) {
|
|
426
|
+
if (initialProbe.healthy && !isExpectedDataRoot(expectedDataRoot, initialProbe.storageRoot)) {
|
|
427
|
+
await stopForgeRuntime(config);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
try {
|
|
431
|
+
await waitForRuntime(config, EXISTING_RUNTIME_GRACE_MS, null);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
await stopForgeRuntime(config);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else if (initialProbe.healthy) {
|
|
440
|
+
if (!isExpectedDataRoot(expectedDataRoot, initialProbe.storageRoot)) {
|
|
441
|
+
throw new Error(formatRuntimeDataRootMismatch(config, expectedDataRoot, initialProbe.storageRoot));
|
|
442
|
+
}
|
|
364
443
|
try {
|
|
365
444
|
await waitForRuntime(config, EXISTING_RUNTIME_GRACE_MS, null);
|
|
366
445
|
return;
|
|
367
446
|
}
|
|
368
447
|
catch {
|
|
369
|
-
|
|
448
|
+
// There is no plugin-managed pid to stop here; fall through into normal startup handling.
|
|
370
449
|
}
|
|
371
450
|
}
|
|
372
451
|
const key = runtimeKey(config);
|
|
@@ -378,14 +457,16 @@ export async function ensureForgeRuntimeReady(config) {
|
|
|
378
457
|
return;
|
|
379
458
|
}
|
|
380
459
|
startupPromise = (async () => {
|
|
381
|
-
|
|
460
|
+
const probeBeforeStart = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
461
|
+
if (probeBeforeStart.healthy && isExpectedDataRoot(expectedDataRoot, probeBeforeStart.storageRoot)) {
|
|
382
462
|
return;
|
|
383
463
|
}
|
|
384
464
|
startupRuntimeKey = runtimeKey(config);
|
|
385
465
|
if (!(await isPortAvailable("127.0.0.1", config.port))) {
|
|
386
466
|
await relocateLocalRuntimePort(config);
|
|
387
467
|
startupRuntimeKey = runtimeKey(config);
|
|
388
|
-
|
|
468
|
+
const probeAfterRelocation = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
469
|
+
if (probeAfterRelocation.healthy && isExpectedDataRoot(expectedDataRoot, probeAfterRelocation.storageRoot)) {
|
|
389
470
|
return;
|
|
390
471
|
}
|
|
391
472
|
}
|
|
@@ -394,6 +475,10 @@ export async function ensureForgeRuntimeReady(config) {
|
|
|
394
475
|
await spawnManagedRuntime(config, plan);
|
|
395
476
|
}
|
|
396
477
|
await waitForRuntime(config, STARTUP_TIMEOUT_MS, managedRuntimeChild?.pid ?? null);
|
|
478
|
+
const probeAfterStart = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
479
|
+
if (!probeAfterStart.healthy || !isExpectedDataRoot(expectedDataRoot, probeAfterStart.storageRoot)) {
|
|
480
|
+
throw new Error(formatRuntimeDataRootMismatch(config, expectedDataRoot, probeAfterStart.storageRoot));
|
|
481
|
+
}
|
|
397
482
|
})().finally(() => {
|
|
398
483
|
startupPromise = null;
|
|
399
484
|
startupRuntimeKey = null;
|
|
@@ -411,8 +496,26 @@ export async function startForgeRuntime(config) {
|
|
|
411
496
|
baseUrl: config.baseUrl
|
|
412
497
|
};
|
|
413
498
|
}
|
|
414
|
-
const
|
|
415
|
-
|
|
499
|
+
const expectedDataRoot = getExpectedDataRoot(config);
|
|
500
|
+
const probe = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
501
|
+
let existingState = await readRuntimeState(config);
|
|
502
|
+
if (!existingState && probe.healthy && isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
503
|
+
const adopted = await adoptManagedRuntimeState(config, probe);
|
|
504
|
+
if (adopted) {
|
|
505
|
+
existingState = await readRuntimeState(config);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (probe.healthy && !isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
started: false,
|
|
512
|
+
managed: Boolean(existingState),
|
|
513
|
+
message: formatRuntimeDataRootMismatch(config, expectedDataRoot, probe.storageRoot),
|
|
514
|
+
pid: existingState?.pid ?? null,
|
|
515
|
+
baseUrl: config.baseUrl
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
if (!existingState && probe.healthy) {
|
|
416
519
|
return {
|
|
417
520
|
ok: true,
|
|
418
521
|
started: false,
|
|
@@ -422,7 +525,7 @@ export async function startForgeRuntime(config) {
|
|
|
422
525
|
baseUrl: config.baseUrl
|
|
423
526
|
};
|
|
424
527
|
}
|
|
425
|
-
if (existingState && processExists(existingState.pid) &&
|
|
528
|
+
if (existingState && processExists(existingState.pid) && probe.healthy) {
|
|
426
529
|
return {
|
|
427
530
|
ok: true,
|
|
428
531
|
started: false,
|
|
@@ -517,8 +620,16 @@ export async function stopForgeRuntime(config) {
|
|
|
517
620
|
};
|
|
518
621
|
}
|
|
519
622
|
export async function getForgeRuntimeStatus(config) {
|
|
520
|
-
const
|
|
521
|
-
const
|
|
623
|
+
const expectedDataRoot = getExpectedDataRoot(config);
|
|
624
|
+
const probe = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
625
|
+
const healthy = probe.healthy;
|
|
626
|
+
let state = await readRuntimeState(config);
|
|
627
|
+
if (!state && healthy && isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
628
|
+
const adopted = await adoptManagedRuntimeState(config, probe);
|
|
629
|
+
if (adopted) {
|
|
630
|
+
state = await readRuntimeState(config);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
522
633
|
const pid = state?.pid ?? null;
|
|
523
634
|
const managed = Boolean(state);
|
|
524
635
|
const running = healthy || (pid !== null && processExists(pid));
|
|
@@ -548,6 +659,17 @@ export async function getForgeRuntimeStatus(config) {
|
|
|
548
659
|
};
|
|
549
660
|
}
|
|
550
661
|
if (healthy && managed) {
|
|
662
|
+
if (!isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
running: true,
|
|
666
|
+
healthy: true,
|
|
667
|
+
managed: true,
|
|
668
|
+
message: formatRuntimeDataRootMismatch(config, expectedDataRoot, probe.storageRoot),
|
|
669
|
+
pid,
|
|
670
|
+
baseUrl: config.baseUrl
|
|
671
|
+
};
|
|
672
|
+
}
|
|
551
673
|
return {
|
|
552
674
|
ok: true,
|
|
553
675
|
running: true,
|
|
@@ -559,6 +681,17 @@ export async function getForgeRuntimeStatus(config) {
|
|
|
559
681
|
};
|
|
560
682
|
}
|
|
561
683
|
if (healthy) {
|
|
684
|
+
if (!isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
running: true,
|
|
688
|
+
healthy: true,
|
|
689
|
+
managed: false,
|
|
690
|
+
message: formatRuntimeDataRootMismatch(config, expectedDataRoot, probe.storageRoot),
|
|
691
|
+
pid: null,
|
|
692
|
+
baseUrl: config.baseUrl
|
|
693
|
+
};
|
|
694
|
+
}
|
|
562
695
|
return {
|
|
563
696
|
ok: true,
|
|
564
697
|
running: true,
|
|
@@ -40,6 +40,12 @@ function normalizeTimeout(value, fallback) {
|
|
|
40
40
|
}
|
|
41
41
|
return Math.min(120_000, Math.max(1000, Math.round(value)));
|
|
42
42
|
}
|
|
43
|
+
function normalizeDataRoot(value) {
|
|
44
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
return path.resolve(value.trim());
|
|
48
|
+
}
|
|
43
49
|
function isLocalOrigin(origin) {
|
|
44
50
|
try {
|
|
45
51
|
return LOCAL_HOSTNAMES.has(new URL(origin).hostname.toLowerCase());
|
|
@@ -80,7 +86,7 @@ export function resolveForgePluginConfig(pluginConfig) {
|
|
|
80
86
|
baseUrl: buildForgeBaseUrl(origin, port),
|
|
81
87
|
webAppUrl: buildForgeWebAppUrl(origin, port),
|
|
82
88
|
portSource: hasConfiguredPort ? "configured" : preferredPort !== null ? "preferred" : "default",
|
|
83
|
-
dataRoot:
|
|
89
|
+
dataRoot: normalizeDataRoot(raw.dataRoot),
|
|
84
90
|
apiToken: typeof raw.apiToken === "string" ? raw.apiToken.trim() : "",
|
|
85
91
|
actorLabel: normalizeString(raw.actorLabel, "aurel"),
|
|
86
92
|
timeoutMs: normalizeTimeout(raw.timeoutMs, 15_000)
|
package/dist/openclaw/tools.js
CHANGED
|
@@ -238,6 +238,21 @@ export function registerForgePluginTools(api, config) {
|
|
|
238
238
|
method: "POST",
|
|
239
239
|
path: "/api/v1/entities/restore"
|
|
240
240
|
});
|
|
241
|
+
registerWriteTool(api, config, {
|
|
242
|
+
name: "forge_grant_reward_bonus",
|
|
243
|
+
label: "Forge Grant Reward Bonus",
|
|
244
|
+
description: "Grant an explicit manual XP bonus or penalty with provenance. Use only for auditable operator judgement beyond the normal task-run and habit reward flows.",
|
|
245
|
+
parameters: Type.Object({
|
|
246
|
+
entityType: Type.String({ minLength: 1 }),
|
|
247
|
+
entityId: Type.String({ minLength: 1 }),
|
|
248
|
+
deltaXp: Type.Number(),
|
|
249
|
+
reasonTitle: Type.String({ minLength: 1 }),
|
|
250
|
+
reasonSummary: optionalString(),
|
|
251
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Any()))
|
|
252
|
+
}),
|
|
253
|
+
method: "POST",
|
|
254
|
+
path: "/api/v1/rewards/bonus"
|
|
255
|
+
});
|
|
241
256
|
registerWriteTool(api, config, {
|
|
242
257
|
name: "forge_post_insight",
|
|
243
258
|
label: "Forge Post Insight",
|
package/dist/server/app.js
CHANGED
|
@@ -7,6 +7,7 @@ import { listActivityEvents, listActivityEventsForTask, removeActivityEvent } fr
|
|
|
7
7
|
import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
|
|
8
8
|
import { listEventLog } from "./repositories/event-log.js";
|
|
9
9
|
import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
|
|
10
|
+
import { createHabit, createHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
|
|
10
11
|
import { listDomains } from "./repositories/domains.js";
|
|
11
12
|
import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
|
|
12
13
|
import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
|
|
@@ -27,7 +28,7 @@ import { getWeeklyReviewPayload } from "./services/reviews.js";
|
|
|
27
28
|
import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
|
|
28
29
|
import { suggestTags } from "./services/tagging.js";
|
|
29
30
|
import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
|
|
30
|
-
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
|
|
31
|
+
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createHabitCheckInSchema, createHabitSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
|
|
31
32
|
import { buildOpenApiDocument } from "./openapi.js";
|
|
32
33
|
import { registerWebRoutes } from "./web.js";
|
|
33
34
|
import { createManagerRuntime } from "./managers/runtime.js";
|
|
@@ -186,6 +187,39 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
186
187
|
{ name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new task.", defaultValue: [] }
|
|
187
188
|
]
|
|
188
189
|
},
|
|
190
|
+
{
|
|
191
|
+
entityType: "habit",
|
|
192
|
+
purpose: "A recurring commitment or recurring slip with explicit cadence, graph links, and XP consequences.",
|
|
193
|
+
minimumCreateFields: ["title"],
|
|
194
|
+
relationshipRules: [
|
|
195
|
+
"Habits can link directly to goals, projects, tasks, values, patterns, behaviors, beliefs, modes, and trigger reports.",
|
|
196
|
+
"Habits are recurring records, not task variants, and they participate in search, notes, delete/restore, and XP.",
|
|
197
|
+
"linkedBehaviorId remains a compatibility alias; linkedBehaviorIds is the canonical array form."
|
|
198
|
+
],
|
|
199
|
+
searchHints: ["Search by title before creating a duplicate habit.", "Use linkedTo when the habit should already be attached to a goal, project, task, or Psyche entity."],
|
|
200
|
+
examples: ['{"title":"Morning training","frequency":"daily","polarity":"positive","linkedGoalIds":["goal_train_body"],"linkedValueIds":["value_steadiness"],"linkedBehaviorIds":["behavior_regulating_walk"]}'],
|
|
201
|
+
fieldGuide: [
|
|
202
|
+
{ name: "title", type: "string", required: true, description: "Concrete recurring behavior label." },
|
|
203
|
+
{ name: "description", type: "string", required: false, description: "What counts as success or failure for this habit.", defaultValue: "" },
|
|
204
|
+
{ name: "status", type: "active|paused|archived", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "archived"], defaultValue: "active" },
|
|
205
|
+
{ name: "polarity", type: "positive|negative", required: false, description: "Whether doing the behavior is aligned or misaligned.", enumValues: ["positive", "negative"], defaultValue: "positive" },
|
|
206
|
+
{ name: "frequency", type: "daily|weekly", required: false, description: "Recurrence cadence.", enumValues: ["daily", "weekly"], defaultValue: "daily" },
|
|
207
|
+
{ name: "targetCount", type: "integer", required: false, description: "How many repetitions define the cadence window.", defaultValue: 1 },
|
|
208
|
+
{ name: "weekDays", type: "integer[]", required: false, description: "Weekday numbers for weekly habits where Monday is 1 and Sunday is 0.", defaultValue: [] },
|
|
209
|
+
{ name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
|
|
210
|
+
{ name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
|
|
211
|
+
{ name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
|
|
212
|
+
{ name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
|
|
213
|
+
{ name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
|
|
214
|
+
{ name: "linkedBehaviorIds", type: "string[]", required: false, description: "Canonical linked behavior ids.", defaultValue: [] },
|
|
215
|
+
{ name: "linkedBehaviorId", type: "string|null", required: false, description: "Compatibility alias for the first linked behavior id.", defaultValue: null, nullable: true },
|
|
216
|
+
{ name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] },
|
|
217
|
+
{ name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
|
|
218
|
+
{ name: "linkedReportIds", type: "string[]", required: false, description: "Linked trigger report ids.", defaultValue: [] },
|
|
219
|
+
{ name: "rewardXp", type: "integer", required: false, description: "XP granted on aligned check-ins.", defaultValue: 12 },
|
|
220
|
+
{ name: "penaltyXp", type: "integer", required: false, description: "XP removed on misaligned check-ins.", defaultValue: 8 }
|
|
221
|
+
]
|
|
222
|
+
},
|
|
189
223
|
{
|
|
190
224
|
entityType: "note",
|
|
191
225
|
purpose: "A Markdown note that can link to one or many Forge entities.",
|
|
@@ -594,6 +628,15 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
594
628
|
notes: ["Restore only works for soft-deleted entities."],
|
|
595
629
|
example: '{"operations":[{"entityType":"goal","id":"goal_123","clientRef":"goal-restore-1"}]}'
|
|
596
630
|
},
|
|
631
|
+
{
|
|
632
|
+
toolName: "forge_grant_reward_bonus",
|
|
633
|
+
summary: "Grant an explicit manual XP bonus or penalty with clear provenance.",
|
|
634
|
+
whenToUse: "Use when the user or operator explicitly wants an auditable reward adjustment beyond the automatic task and habit reward paths.",
|
|
635
|
+
inputShape: "{ entityType: RewardableEntityType, entityId: string, deltaXp: integer, reasonTitle: string, reasonSummary?: string, metadata?: object }",
|
|
636
|
+
requiredFields: ["entityType", "entityId", "deltaXp", "reasonTitle"],
|
|
637
|
+
notes: ["Requires rewards.manage and write scopes.", "Use this for explicit operator judgement, not as a substitute for normal task_run or habit check-in rewards."],
|
|
638
|
+
example: '{"entityType":"habit","entityId":"habit_morning_training","deltaXp":18,"reasonTitle":"Operator bonus","reasonSummary":"Stayed with the habit through unusual travel friction.","metadata":{"manual":true,"source":"agent"}}'
|
|
639
|
+
},
|
|
597
640
|
{
|
|
598
641
|
toolName: "forge_post_insight",
|
|
599
642
|
summary: "Store an agent-authored insight.",
|
|
@@ -736,6 +779,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
736
779
|
"Goals are the top-level strategic layer.",
|
|
737
780
|
"Projects belong to one goal through goalId.",
|
|
738
781
|
"Tasks can belong to a goal, a project, both, or neither.",
|
|
782
|
+
"Habits are recurring records that can connect directly to goals, projects, tasks, and durable Psyche entities.",
|
|
739
783
|
"Task runs represent live work sessions on tasks and are separate from task status.",
|
|
740
784
|
"Notes can link to one or many entities and are the canonical place for Markdown progress context or close-out evidence.",
|
|
741
785
|
"Psyche values can link to goals, projects, and tasks.",
|
|
@@ -771,6 +815,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
771
815
|
"forge_delete_entities",
|
|
772
816
|
"forge_restore_entities"
|
|
773
817
|
],
|
|
818
|
+
rewardWorkflow: ["forge_grant_reward_bonus"],
|
|
774
819
|
workWorkflow: [
|
|
775
820
|
"forge_log_work",
|
|
776
821
|
"forge_start_task_run",
|
|
@@ -955,7 +1000,17 @@ function buildHealthPayload(taskRunWatchdog, extras = {}) {
|
|
|
955
1000
|
...extras
|
|
956
1001
|
};
|
|
957
1002
|
}
|
|
1003
|
+
function shouldIncludeRuntimeProbe(headers) {
|
|
1004
|
+
const probeHeader = headers["x-forge-runtime-probe"];
|
|
1005
|
+
if (Array.isArray(probeHeader)) {
|
|
1006
|
+
return probeHeader.some((value) => typeof value === "string" && value.trim() === "1");
|
|
1007
|
+
}
|
|
1008
|
+
return typeof probeHeader === "string" && probeHeader.trim() === "1";
|
|
1009
|
+
}
|
|
958
1010
|
function buildV1Context() {
|
|
1011
|
+
const goals = listGoals();
|
|
1012
|
+
const tasks = listTasks();
|
|
1013
|
+
const habits = listHabits();
|
|
959
1014
|
return {
|
|
960
1015
|
meta: {
|
|
961
1016
|
apiVersion: "v1",
|
|
@@ -964,15 +1019,16 @@ function buildV1Context() {
|
|
|
964
1019
|
backend: "forge-node-runtime",
|
|
965
1020
|
mode: "transitional-node"
|
|
966
1021
|
},
|
|
967
|
-
metrics: buildGamificationProfile(
|
|
1022
|
+
metrics: buildGamificationProfile(goals, tasks, habits),
|
|
968
1023
|
dashboard: getDashboard(),
|
|
969
1024
|
overview: getOverviewContext(),
|
|
970
1025
|
today: getTodayContext(),
|
|
971
1026
|
risk: getRiskContext(),
|
|
972
|
-
goals
|
|
1027
|
+
goals,
|
|
973
1028
|
projects: listProjectSummaries(),
|
|
974
1029
|
tags: listTags(),
|
|
975
|
-
tasks
|
|
1030
|
+
tasks,
|
|
1031
|
+
habits,
|
|
976
1032
|
activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
|
|
977
1033
|
activity: listActivityEvents({ limit: 25 })
|
|
978
1034
|
};
|
|
@@ -980,8 +1036,9 @@ function buildV1Context() {
|
|
|
980
1036
|
function buildXpMetricsPayload() {
|
|
981
1037
|
const goals = listGoals();
|
|
982
1038
|
const tasks = listTasks();
|
|
1039
|
+
const habits = listHabits();
|
|
983
1040
|
const rules = listRewardRules();
|
|
984
|
-
const gamificationOverview = buildGamificationOverview(goals, tasks);
|
|
1041
|
+
const gamificationOverview = buildGamificationOverview(goals, tasks, habits);
|
|
985
1042
|
const dailyAmbientCap = rules
|
|
986
1043
|
.filter((rule) => rule.family === "ambient")
|
|
987
1044
|
.reduce((max, rule) => Math.max(max, Number(rule.config.dailyCap ?? 0)), 0) || 12;
|
|
@@ -989,7 +1046,7 @@ function buildXpMetricsPayload() {
|
|
|
989
1046
|
profile: gamificationOverview.profile,
|
|
990
1047
|
achievements: gamificationOverview.achievements,
|
|
991
1048
|
milestoneRewards: gamificationOverview.milestoneRewards,
|
|
992
|
-
momentumPulse: buildXpMomentumPulse(goals, tasks),
|
|
1049
|
+
momentumPulse: buildXpMomentumPulse(goals, tasks, habits),
|
|
993
1050
|
recentLedger: listRewardLedger({ limit: 25 }),
|
|
994
1051
|
rules,
|
|
995
1052
|
dailyAmbientXp: getDailyAmbientXp(new Date().toISOString().slice(0, 10)),
|
|
@@ -998,6 +1055,7 @@ function buildXpMetricsPayload() {
|
|
|
998
1055
|
}
|
|
999
1056
|
function buildOperatorContext() {
|
|
1000
1057
|
const tasks = listTasks();
|
|
1058
|
+
const dueHabits = listHabits({ dueToday: true }).slice(0, 12);
|
|
1001
1059
|
const activeProjects = listProjectSummaries({ status: "active" }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
|
|
1002
1060
|
const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
|
|
1003
1061
|
const recommendedNextTask = focusTasks[0] ??
|
|
@@ -1008,6 +1066,7 @@ function buildOperatorContext() {
|
|
|
1008
1066
|
generatedAt: new Date().toISOString(),
|
|
1009
1067
|
activeProjects: activeProjects.slice(0, 8),
|
|
1010
1068
|
focusTasks: focusTasks.slice(0, 12),
|
|
1069
|
+
dueHabits,
|
|
1011
1070
|
currentBoard: {
|
|
1012
1071
|
backlog: tasks.filter((task) => task.status === "backlog").slice(0, 20),
|
|
1013
1072
|
focus: tasks.filter((task) => task.status === "focus").slice(0, 20),
|
|
@@ -1231,9 +1290,18 @@ export async function buildServer(options = {}) {
|
|
|
1231
1290
|
return context;
|
|
1232
1291
|
};
|
|
1233
1292
|
app.get("/api/health", async () => buildHealthPayload(taskRunWatchdog));
|
|
1234
|
-
app.get("/api/v1/health", async () => buildHealthPayload(taskRunWatchdog, {
|
|
1293
|
+
app.get("/api/v1/health", async (request) => buildHealthPayload(taskRunWatchdog, {
|
|
1235
1294
|
apiVersion: "v1",
|
|
1236
|
-
backend: "forge-node-runtime"
|
|
1295
|
+
backend: "forge-node-runtime",
|
|
1296
|
+
...(shouldIncludeRuntimeProbe(request.headers)
|
|
1297
|
+
? {
|
|
1298
|
+
runtime: {
|
|
1299
|
+
pid: process.pid,
|
|
1300
|
+
storageRoot: runtimeConfig.dataRoot ?? process.cwd(),
|
|
1301
|
+
basePath: runtimeConfig.basePath
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
: {})
|
|
1237
1305
|
}));
|
|
1238
1306
|
app.get("/api/v1/auth/operator-session", async (request, reply) => ({
|
|
1239
1307
|
session: managers.session.ensureLocalOperatorSession(request.headers, reply)
|
|
@@ -1721,6 +1789,19 @@ export async function buildServer(options = {}) {
|
|
|
1721
1789
|
const query = taskListQuerySchema.parse(request.query ?? {});
|
|
1722
1790
|
return { tasks: listTasks(query) };
|
|
1723
1791
|
});
|
|
1792
|
+
app.get("/api/v1/habits", async (request) => {
|
|
1793
|
+
const query = habitListQuerySchema.parse(request.query ?? {});
|
|
1794
|
+
return { habits: listHabits(query) };
|
|
1795
|
+
});
|
|
1796
|
+
app.get("/api/v1/habits/:id", async (request, reply) => {
|
|
1797
|
+
const { id } = request.params;
|
|
1798
|
+
const habit = getHabitById(id);
|
|
1799
|
+
if (!habit) {
|
|
1800
|
+
reply.code(404);
|
|
1801
|
+
return { error: "Habit not found" };
|
|
1802
|
+
}
|
|
1803
|
+
return { habit };
|
|
1804
|
+
});
|
|
1724
1805
|
app.get("/api/v1/projects/:id", async (request, reply) => {
|
|
1725
1806
|
const { id } = request.params;
|
|
1726
1807
|
const project = listProjectSummaries().find((entry) => entry.id === id);
|
|
@@ -1764,7 +1845,7 @@ export async function buildServer(options = {}) {
|
|
|
1764
1845
|
return { event };
|
|
1765
1846
|
});
|
|
1766
1847
|
app.get("/api/v1/metrics", async () => ({
|
|
1767
|
-
metrics: buildGamificationOverview(listGoals(), listTasks())
|
|
1848
|
+
metrics: buildGamificationOverview(listGoals(), listTasks(), listHabits())
|
|
1768
1849
|
}));
|
|
1769
1850
|
app.get("/api/v1/metrics/xp", async () => ({
|
|
1770
1851
|
metrics: buildXpMetricsPayload()
|
|
@@ -1944,6 +2025,12 @@ export async function buildServer(options = {}) {
|
|
|
1944
2025
|
reply.code(201);
|
|
1945
2026
|
return { project };
|
|
1946
2027
|
});
|
|
2028
|
+
app.post("/api/v1/habits", async (request, reply) => {
|
|
2029
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits" });
|
|
2030
|
+
const habit = createHabit(createHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
2031
|
+
reply.code(201);
|
|
2032
|
+
return { habit };
|
|
2033
|
+
});
|
|
1947
2034
|
app.patch("/api/v1/projects/:id", async (request, reply) => {
|
|
1948
2035
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects/:id" });
|
|
1949
2036
|
const { id } = request.params;
|
|
@@ -1964,6 +2051,36 @@ export async function buildServer(options = {}) {
|
|
|
1964
2051
|
}
|
|
1965
2052
|
return { project };
|
|
1966
2053
|
});
|
|
2054
|
+
app.patch("/api/v1/habits/:id", async (request, reply) => {
|
|
2055
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
|
|
2056
|
+
const { id } = request.params;
|
|
2057
|
+
const habit = updateHabit(id, updateHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
2058
|
+
if (!habit) {
|
|
2059
|
+
reply.code(404);
|
|
2060
|
+
return { error: "Habit not found" };
|
|
2061
|
+
}
|
|
2062
|
+
return { habit };
|
|
2063
|
+
});
|
|
2064
|
+
app.delete("/api/v1/habits/:id", async (request, reply) => {
|
|
2065
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
|
|
2066
|
+
const { id } = request.params;
|
|
2067
|
+
const habit = deleteEntity("habit", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
|
|
2068
|
+
if (!habit) {
|
|
2069
|
+
reply.code(404);
|
|
2070
|
+
return { error: "Habit not found" };
|
|
2071
|
+
}
|
|
2072
|
+
return { habit };
|
|
2073
|
+
});
|
|
2074
|
+
app.post("/api/v1/habits/:id/check-ins", async (request, reply) => {
|
|
2075
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id/check-ins" });
|
|
2076
|
+
const { id } = request.params;
|
|
2077
|
+
const habit = createHabitCheckIn(id, createHabitCheckInSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
2078
|
+
if (!habit) {
|
|
2079
|
+
reply.code(404);
|
|
2080
|
+
return { error: "Habit not found" };
|
|
2081
|
+
}
|
|
2082
|
+
return { habit, metrics: buildXpMetricsPayload() };
|
|
2083
|
+
});
|
|
1967
2084
|
app.patch("/api/v1/settings", async (request) => {
|
|
1968
2085
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings" });
|
|
1969
2086
|
return {
|
|
@@ -2072,7 +2189,7 @@ export async function buildServer(options = {}) {
|
|
|
2072
2189
|
app.get("/api/metrics", async (_request, reply) => {
|
|
2073
2190
|
markCompatibilityRoute(reply);
|
|
2074
2191
|
return {
|
|
2075
|
-
metrics: buildGamificationProfile(listGoals(), listTasks())
|
|
2192
|
+
metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits())
|
|
2076
2193
|
};
|
|
2077
2194
|
});
|
|
2078
2195
|
app.get("/api/task-runs", async (request, reply) => {
|
|
@@ -2102,7 +2219,7 @@ export async function buildServer(options = {}) {
|
|
|
2102
2219
|
markCompatibilityRoute(reply);
|
|
2103
2220
|
const query = taskListQuerySchema.parse(request.query ?? {});
|
|
2104
2221
|
return {
|
|
2105
|
-
metrics: buildGamificationProfile(listGoals(), listTasks()),
|
|
2222
|
+
metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits()),
|
|
2106
2223
|
dashboard: getDashboard(),
|
|
2107
2224
|
overview: getOverviewContext(),
|
|
2108
2225
|
today: getTodayContext(),
|
|
@@ -2111,6 +2228,7 @@ export async function buildServer(options = {}) {
|
|
|
2111
2228
|
projects: listProjectSummaries(),
|
|
2112
2229
|
tags: listTags(),
|
|
2113
2230
|
tasks: listTasks(query),
|
|
2231
|
+
habits: listHabits(),
|
|
2114
2232
|
activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
|
|
2115
2233
|
activity: listActivityEvents({ limit: 25 })
|
|
2116
2234
|
};
|