@voybio/ace-swarm 2.4.9 → 2.4.10
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/CHANGELOG.md +52 -14
- package/README.md +14 -10
- package/assets/.agents/ACE/ACE_coders/AGENTS.md +23 -30
- package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +21 -30
- package/assets/.agents/skills/landing-review-watcher/SKILL.md +1 -1
- package/assets/.agents/skills/learn-from-work/SKILL.md +41 -0
- package/assets/agent-state/ACE_WORKFLOW.md +25 -2
- package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +143 -2
- package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +2 -2
- package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +10 -0
- package/assets/eval-battery/battery.json +1 -1
- package/assets/instructions/ACE_Coder.instructions.md +9 -13
- package/assets/scripts/ace-hook-dispatch.mjs +16 -411
- package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +1 -2
- package/assets/vendor/README.md +9 -0
- package/assets/vendor/vericify-reader.mjs +272 -0
- package/assets/vendor/vericify-reader.provenance.json +10 -0
- package/dist/ace-autonomy.d.ts +58 -0
- package/dist/ace-autonomy.js +211 -6
- package/dist/ace-internal-tools.d.ts +1 -0
- package/dist/ace-internal-tools.js +3 -3
- package/dist/ace-server-instructions.d.ts +3 -3
- package/dist/ace-server-instructions.js +46 -192
- package/dist/cli.js +44 -6
- package/dist/experience-reflector.d.ts +7 -0
- package/dist/experience-reflector.js +39 -0
- package/dist/goal-engine.d.ts +64 -7
- package/dist/goal-engine.js +197 -41
- package/dist/handoff-registry.d.ts +10 -0
- package/dist/handoff-registry.js +59 -0
- package/dist/helpers/artifacts.d.ts +6 -2
- package/dist/helpers/artifacts.js +8 -6
- package/dist/helpers/bootstrap.js +13 -20
- package/dist/helpers/path-utils.js +2 -0
- package/dist/helpers/store-resolution.js +18 -3
- package/dist/helpers/workspace-root.d.ts +2 -0
- package/dist/helpers/workspace-root.js +9 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +1 -0
- package/dist/hermes/bridge-protocol.d.ts +1 -1
- package/dist/hermes/session-manager.d.ts +17 -0
- package/dist/hermes/session-manager.js +67 -14
- package/dist/host-goal-engine.d.ts +138 -0
- package/dist/host-goal-engine.js +793 -0
- package/dist/host-goal-status.d.ts +113 -0
- package/dist/host-goal-status.js +175 -0
- package/dist/host-learning.d.ts +68 -0
- package/dist/host-learning.js +115 -0
- package/dist/host-observation.d.ts +12 -0
- package/dist/host-observation.js +91 -0
- package/dist/host-plan-revision.d.ts +14 -0
- package/dist/host-plan-revision.js +101 -0
- package/dist/host-verification-job.d.ts +12 -0
- package/dist/host-verification-job.js +183 -0
- package/dist/host-work-context.d.ts +12 -0
- package/dist/host-work-context.js +8 -0
- package/dist/job-scheduler.d.ts +48 -14
- package/dist/job-scheduler.js +169 -23
- package/dist/job-worker.d.ts +3 -1
- package/dist/job-worker.js +10 -3
- package/dist/kanban.d.ts +4 -1
- package/dist/kanban.js +44 -16
- package/dist/learned-artifacts.d.ts +64 -0
- package/dist/learned-artifacts.js +321 -0
- package/dist/learning-controls.d.ts +142 -0
- package/dist/learning-controls.js +135 -0
- package/dist/learning-input-context.d.ts +9 -0
- package/dist/learning-input-context.js +62 -0
- package/dist/learning-lifecycle.d.ts +10 -0
- package/dist/learning-lifecycle.js +11 -0
- package/dist/learning-overlays.d.ts +3 -0
- package/dist/learning-overlays.js +17 -0
- package/dist/learning-policy.d.ts +40 -0
- package/dist/learning-policy.js +38 -0
- package/dist/learning-records.d.ts +35 -0
- package/dist/learning-records.js +17 -0
- package/dist/learning-scorecard.d.ts +82 -0
- package/dist/learning-scorecard.js +86 -0
- package/dist/learning-serving.d.ts +29 -0
- package/dist/learning-serving.js +134 -0
- package/dist/linked-work.d.ts +9 -0
- package/dist/linked-work.js +28 -0
- package/dist/local-model-runtime.js +1 -1
- package/dist/mcp-command-progress.d.ts +16 -0
- package/dist/mcp-command-progress.js +70 -0
- package/dist/model-bridge.d.ts +14 -13
- package/dist/model-bridge.js +73 -41
- package/dist/orchestrator-supervisor.d.ts +108 -6
- package/dist/orchestrator-supervisor.js +4 -32
- package/dist/plan-proposal.js +3 -3
- package/dist/playbook-curator.d.ts +1 -0
- package/dist/playbook-curator.js +11 -9
- package/dist/process-identity.d.ts +8 -0
- package/dist/process-identity.js +38 -0
- package/dist/progress-command.d.ts +34 -0
- package/dist/progress-command.js +153 -0
- package/dist/prompts.js +15 -15
- package/dist/resources.js +2 -2
- package/dist/runtime-command.d.ts +8 -1
- package/dist/runtime-command.js +48 -7
- package/dist/runtime-executor.js +33 -11
- package/dist/runtime-profile-syntax.d.ts +6 -0
- package/dist/runtime-profile-syntax.js +121 -0
- package/dist/runtime-profile.d.ts +37 -5
- package/dist/runtime-profile.js +36 -129
- package/dist/runtime-tool-specs.js +1 -1
- package/dist/safe-edit.d.ts +7 -3
- package/dist/safe-edit.js +40 -47
- package/dist/schemas.js +4 -2
- package/dist/server.d.ts +2 -16
- package/dist/server.js +14 -20
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +2 -0
- package/dist/skill-auditor.d.ts +19 -0
- package/dist/skill-auditor.js +32 -2
- package/dist/skill-catalog.d.ts +4 -1
- package/dist/skill-catalog.js +6 -5
- package/dist/store/ace-packed-store.d.ts +5 -1
- package/dist/store/ace-packed-store.js +33 -1
- package/dist/store/bootstrap-store.js +19 -12
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +94 -27
- package/dist/store/importer.js +11 -20
- package/dist/store/knowledge-bake.d.ts +3 -1
- package/dist/store/knowledge-bake.js +10 -1
- package/dist/store/legacy-vericify-posts.d.ts +8 -0
- package/dist/store/legacy-vericify-posts.js +76 -0
- package/dist/store/materializers/hook-context-materializer.d.ts +2 -1
- package/dist/store/materializers/hook-context-materializer.js +6 -2
- package/dist/store/materializers/host-file-materializer.js +2 -2
- package/dist/store/materializers/projection-manager.d.ts +4 -1
- package/dist/store/materializers/projection-manager.js +10 -3
- package/dist/store/materializers/todo-syncer.d.ts +39 -20
- package/dist/store/materializers/todo-syncer.js +208 -110
- package/dist/store/materializers/vericify-projector.js +15 -26
- package/dist/store/repositories/goal-repository.d.ts +51 -2
- package/dist/store/repositories/goal-repository.js +114 -48
- package/dist/store/repositories/playbook-repository.d.ts +41 -3
- package/dist/store/repositories/playbook-repository.js +61 -5
- package/dist/store/repositories/todo-repository.d.ts +8 -2
- package/dist/store/repositories/todo-repository.js +1 -1
- package/dist/store/repositories/vericify-repository.d.ts +4 -0
- package/dist/store/repositories/vericify-repository.js +46 -0
- package/dist/store/skills-install.d.ts +1 -0
- package/dist/store/skills-install.js +20 -16
- package/dist/store/source-preservation.d.ts +24 -0
- package/dist/store/source-preservation.js +79 -0
- package/dist/store/store-artifacts.js +8 -1
- package/dist/store/test-write-boundary.d.ts +16 -0
- package/dist/store/test-write-boundary.js +175 -0
- package/dist/store/types.d.ts +5 -1
- package/dist/store/write-coordinator.js +16 -0
- package/dist/task-plan-context.d.ts +11 -0
- package/dist/task-plan-context.js +59 -0
- package/dist/todo-state.d.ts +19 -9
- package/dist/todo-state.js +204 -321
- package/dist/tools-agent.d.ts +1 -1
- package/dist/tools-agent.js +38 -62
- package/dist/tools-files.d.ts +0 -3
- package/dist/tools-files.js +21 -11
- package/dist/tools-framework.js +16 -25
- package/dist/tools-goal.d.ts +2 -4
- package/dist/tools-goal.js +119 -16
- package/dist/tools-handoff.js +28 -4
- package/dist/tools-lifecycle.js +1 -1
- package/dist/tools-memory.d.ts +0 -3
- package/dist/tools-memory.js +51 -6
- package/dist/tools-scheduler.js +19 -17
- package/dist/tools-todo.js +160 -148
- package/dist/vericify-bridge.d.ts +8 -0
- package/dist/vericify-bridge.js +18 -0
- package/dist/vericify-context.d.ts +4 -0
- package/dist/vericify-context.js +26 -5
- package/dist/vericify-native.d.ts +101 -0
- package/dist/vericify-native.js +221 -0
- package/dist/verification-worker.d.ts +2 -0
- package/dist/verification-worker.js +31 -0
- package/docs/mcp-tool-reference.md +44 -9
- package/package.json +19 -6
- package/scripts/dev/run-tests-isolated.mjs +165 -0
- package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
- package/scripts/dev/test-isolation-guard.mjs +20 -0
- package/scripts/dev/test-preload.mjs +153 -0
- package/scripts/dev/test-source-inventory.mjs +31 -0
- package/docs/development.md +0 -46
package/dist/job-scheduler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readHandoffRegistry } from "./handoff-registry.js";
|
|
3
|
-
import { resolveWorkspaceRoot, safeRead, safeWrite, withFileLock, wsPath } from "./helpers.js";
|
|
3
|
+
import { resolveWorkspaceRoot, withWorkspaceRoot, safeRead, safeWrite, withFileLock, wsPath } from "./helpers.js";
|
|
4
4
|
import { isReadError } from "./shared.js";
|
|
5
5
|
import { appendStatusEventSafe, readStatusEvents } from "./status-events.js";
|
|
6
6
|
import { appendRunLedgerEntrySafe } from "./run-ledger.js";
|
|
@@ -10,6 +10,7 @@ import { ProjectionManager } from "./store/materializers/projection-manager.js";
|
|
|
10
10
|
import { SchedulerRepository } from "./store/repositories/scheduler-repository.js";
|
|
11
11
|
import { getWorkspaceStorePath, readStoreJsonSync, storeExistsSync, } from "./store/store-snapshot.js";
|
|
12
12
|
import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
|
|
13
|
+
import { probeProcess, readProcessIdentity } from "./process-identity.js";
|
|
13
14
|
// ── Buffered transition-event infrastructure ────────────────────────
|
|
14
15
|
// Events are collected during the scheduler lock and flushed after
|
|
15
16
|
// the lock is released. Emission is failure-tolerant: a broken
|
|
@@ -204,6 +205,8 @@ function parseQueueFile(raw) {
|
|
|
204
205
|
evidence_ref: typeof job.evidence_ref === "string" ? job.evidence_ref : undefined,
|
|
205
206
|
error: typeof job.error === "string" ? job.error : undefined,
|
|
206
207
|
reason_code: typeof job.reason_code === "string" ? job.reason_code : undefined,
|
|
208
|
+
execution: job.execution && typeof job.execution === "object" ? { ...job.execution } : undefined,
|
|
209
|
+
execution_children: Array.isArray(job.execution_children) ? job.execution_children.map(identity => ({ ...identity })) : undefined,
|
|
207
210
|
});
|
|
208
211
|
}
|
|
209
212
|
return {
|
|
@@ -289,16 +292,28 @@ function parseLease(raw) {
|
|
|
289
292
|
return undefined;
|
|
290
293
|
}
|
|
291
294
|
}
|
|
292
|
-
export function getJobQueuePath() {
|
|
295
|
+
export function getJobQueuePath(workspaceRoot = resolveWorkspaceRoot()) {
|
|
296
|
+
return withWorkspaceRoot(workspaceRoot, () => getJobQueuePathInWorkspace());
|
|
297
|
+
}
|
|
298
|
+
function getJobQueuePathInWorkspace() {
|
|
293
299
|
return wsPath(JOB_QUEUE_REL);
|
|
294
300
|
}
|
|
295
|
-
export function getJobLockTablePath() {
|
|
301
|
+
export function getJobLockTablePath(workspaceRoot = resolveWorkspaceRoot()) {
|
|
302
|
+
return withWorkspaceRoot(workspaceRoot, () => getJobLockTablePathInWorkspace());
|
|
303
|
+
}
|
|
304
|
+
function getJobLockTablePathInWorkspace() {
|
|
296
305
|
return wsPath(JOB_LOCK_TABLE_REL);
|
|
297
306
|
}
|
|
298
|
-
export function getSchedulerLeasePath() {
|
|
307
|
+
export function getSchedulerLeasePath(workspaceRoot = resolveWorkspaceRoot()) {
|
|
308
|
+
return withWorkspaceRoot(workspaceRoot, () => getSchedulerLeasePathInWorkspace());
|
|
309
|
+
}
|
|
310
|
+
function getSchedulerLeasePathInWorkspace() {
|
|
299
311
|
return wsPath(SCHEDULER_LEASE_REL);
|
|
300
312
|
}
|
|
301
|
-
export function readJobQueue() {
|
|
313
|
+
export function readJobQueue(workspaceRoot = resolveWorkspaceRoot()) {
|
|
314
|
+
return withWorkspaceRoot(workspaceRoot, () => readJobQueueInWorkspace());
|
|
315
|
+
}
|
|
316
|
+
function readJobQueueInWorkspace() {
|
|
302
317
|
const root = resolveWorkspaceRoot();
|
|
303
318
|
if (storeExistsSync(root)) {
|
|
304
319
|
const stored = readStoreJsonSync(root, "state/scheduler/queue");
|
|
@@ -311,7 +326,10 @@ export function readJobQueue() {
|
|
|
311
326
|
return defaultQueueFile();
|
|
312
327
|
return parseQueueFile(raw) ?? defaultQueueFile();
|
|
313
328
|
}
|
|
314
|
-
export function readJobLockTable() {
|
|
329
|
+
export function readJobLockTable(workspaceRoot = resolveWorkspaceRoot()) {
|
|
330
|
+
return withWorkspaceRoot(workspaceRoot, () => readJobLockTableInWorkspace());
|
|
331
|
+
}
|
|
332
|
+
function readJobLockTableInWorkspace() {
|
|
315
333
|
const root = resolveWorkspaceRoot();
|
|
316
334
|
if (storeExistsSync(root)) {
|
|
317
335
|
const stored = readStoreJsonSync(root, "state/scheduler/locks");
|
|
@@ -324,7 +342,10 @@ export function readJobLockTable() {
|
|
|
324
342
|
return defaultJobLockFile();
|
|
325
343
|
return parseJobLockFile(raw) ?? defaultJobLockFile();
|
|
326
344
|
}
|
|
327
|
-
export function readSchedulerLease() {
|
|
345
|
+
export function readSchedulerLease(workspaceRoot = resolveWorkspaceRoot()) {
|
|
346
|
+
return withWorkspaceRoot(workspaceRoot, () => readSchedulerLeaseInWorkspace());
|
|
347
|
+
}
|
|
348
|
+
function readSchedulerLeaseInWorkspace() {
|
|
328
349
|
const root = resolveWorkspaceRoot();
|
|
329
350
|
if (storeExistsSync(root)) {
|
|
330
351
|
const stored = readStoreJsonSync(root, "state/scheduler/lease");
|
|
@@ -341,9 +362,17 @@ async function withSchedulerTransaction(fn) {
|
|
|
341
362
|
const storePath = getWorkspaceStorePath(resolveWorkspaceRoot());
|
|
342
363
|
return withFileLock(SCHEDULER_LOCK_REL, () => withStoreWriteCoordinator(storePath, fn, { operation_label: "scheduler_transaction" }));
|
|
343
364
|
}
|
|
365
|
+
function probeJobExecution(job) {
|
|
366
|
+
const identities = [...(job.execution ? [job.execution] : []), ...(job.execution_children ?? [])];
|
|
367
|
+
const states = identities.map(probeProcess);
|
|
368
|
+
if (states.includes("alive"))
|
|
369
|
+
return "alive";
|
|
370
|
+
return states.includes("unknown") ? "unknown" : "dead";
|
|
371
|
+
}
|
|
344
372
|
function hasExecutionOwnership(queue, locks) {
|
|
345
373
|
return queue.jobs.some((job) => job.status === "running" ||
|
|
346
|
-
(job.status === "unknown_recovery" && job.reason_code === "handoff_execution_unverified"
|
|
374
|
+
(job.status === "unknown_recovery" && (job.reason_code === "handoff_execution_unverified" ||
|
|
375
|
+
probeJobExecution(job) !== "dead"))) ||
|
|
347
376
|
locks.locks.some((lock) => isJobLockActive(lock));
|
|
348
377
|
}
|
|
349
378
|
function leaseForAcquisition() {
|
|
@@ -601,6 +630,11 @@ function acquireLease(existing, owner, ttlSeconds, now) {
|
|
|
601
630
|
}
|
|
602
631
|
const existingExpiry = parseDate(existing.expires_at);
|
|
603
632
|
const expired = !existingExpiry || existingExpiry.getTime() <= now.getTime();
|
|
633
|
+
// A missed heartbeat is not evidence that an executing process has exited.
|
|
634
|
+
// Do not steal its owner lease while its identity is live or uncertain.
|
|
635
|
+
if (expired && existing.owner !== owner && readJobQueue().jobs.some((job) => (job.status === "running" || job.status === "unknown_recovery") &&
|
|
636
|
+
probeJobExecution(job) !== "dead"))
|
|
637
|
+
return { acquired: false, lease: existing, recovery_mode: false };
|
|
604
638
|
if (expired || existing.owner === owner) {
|
|
605
639
|
const lease = {
|
|
606
640
|
version: 1,
|
|
@@ -718,11 +752,18 @@ function startJobNow(job, table, lease, now, buf, sourceTool) {
|
|
|
718
752
|
});
|
|
719
753
|
return { acquired_at: acquiredAt, resources };
|
|
720
754
|
}
|
|
721
|
-
function reconcileRecoveryState(queue, table, now, recoveryMode, buf) {
|
|
755
|
+
function reconcileRecoveryState(queue, table, now, recoveryMode, buf, selected) {
|
|
722
756
|
let recovered = 0;
|
|
723
757
|
for (const job of queue.jobs) {
|
|
758
|
+
if (selected && !selected.has(job.job_id))
|
|
759
|
+
continue;
|
|
724
760
|
if (job.kind === "handoff.lifecycle")
|
|
725
761
|
continue;
|
|
762
|
+
if ((job.status === "running" || job.status === "unknown_recovery") && probeJobExecution(job) !== "dead") {
|
|
763
|
+
// Preserve ownership even during lease recovery or missing lock metadata.
|
|
764
|
+
// A retained uncertain execution must never be dispatched a second time.
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
726
767
|
if (job.status === "unknown_recovery") {
|
|
727
768
|
releaseLocksForRecoveredJob(table, job.job_id, "recovery_hold", now);
|
|
728
769
|
continue;
|
|
@@ -769,11 +810,13 @@ function reconcileRecoveryState(queue, table, now, recoveryMode, buf) {
|
|
|
769
810
|
* Missing-lock jobs are intentionally left to `reconcileRecoveryState`, which
|
|
770
811
|
* runs first; this pass only inspects jobs that still hold active locks.
|
|
771
812
|
*/
|
|
772
|
-
function reapStaleRunningJobs(queue, table, now, staleMs, buf) {
|
|
813
|
+
function reapStaleRunningJobs(queue, table, now, staleMs, buf, selected) {
|
|
773
814
|
if (staleMs <= 0)
|
|
774
815
|
return [];
|
|
775
816
|
const reaped = [];
|
|
776
817
|
for (const job of queue.jobs) {
|
|
818
|
+
if (selected && !selected.has(job.job_id))
|
|
819
|
+
continue;
|
|
777
820
|
if (job.kind === "handoff.lifecycle")
|
|
778
821
|
continue;
|
|
779
822
|
if (job.status !== "running")
|
|
@@ -787,6 +830,14 @@ function reapStaleRunningJobs(queue, table, now, staleMs, buf) {
|
|
|
787
830
|
const ageMs = now.getTime() - lastBeat;
|
|
788
831
|
if (ageMs <= staleMs)
|
|
789
832
|
continue;
|
|
833
|
+
if (job.execution || job.execution_children?.length) {
|
|
834
|
+
const processState = probeJobExecution(job);
|
|
835
|
+
if (processState !== "dead") {
|
|
836
|
+
job.reason_code = `heartbeat_stale_process_${processState}`;
|
|
837
|
+
job.error = `Worker heartbeat is stale, but execution process is ${processState}; execution and resource locks are retained pending recovery.`;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
790
841
|
const previousStatus = job.status;
|
|
791
842
|
job.status = "unknown_recovery";
|
|
792
843
|
job.reason_code = "heartbeat_stale";
|
|
@@ -821,7 +872,10 @@ function reapStaleRunningJobs(queue, table, now, staleMs, buf) {
|
|
|
821
872
|
}
|
|
822
873
|
return reaped;
|
|
823
874
|
}
|
|
824
|
-
export async function enqueueJob(input) {
|
|
875
|
+
export async function enqueueJob(input, workspaceRoot = resolveWorkspaceRoot()) {
|
|
876
|
+
return withWorkspaceRoot(workspaceRoot, () => enqueueJobInWorkspace(input));
|
|
877
|
+
}
|
|
878
|
+
async function enqueueJobInWorkspace(input) {
|
|
825
879
|
const buf = newEventBuffer();
|
|
826
880
|
const result = await withSchedulerTransaction(async () => {
|
|
827
881
|
if (!input.kind || input.kind.trim().length === 0) {
|
|
@@ -923,7 +977,10 @@ export async function enqueueJob(input) {
|
|
|
923
977
|
return result;
|
|
924
978
|
}
|
|
925
979
|
/** Mirror handoff lifecycle without admitting or executing a job. */
|
|
926
|
-
export async function syncHandoffLifecycleJob(input) {
|
|
980
|
+
export async function syncHandoffLifecycleJob(input, workspaceRoot = resolveWorkspaceRoot()) {
|
|
981
|
+
return withWorkspaceRoot(workspaceRoot, () => syncHandoffLifecycleJobInWorkspace(input));
|
|
982
|
+
}
|
|
983
|
+
async function syncHandoffLifecycleJobInWorkspace(input) {
|
|
927
984
|
return withSchedulerTransaction(async () => {
|
|
928
985
|
const queue = readJobQueue();
|
|
929
986
|
const locks = readJobLockTable();
|
|
@@ -986,7 +1043,10 @@ export async function syncHandoffLifecycleJob(input) {
|
|
|
986
1043
|
};
|
|
987
1044
|
});
|
|
988
1045
|
}
|
|
989
|
-
export async function acknowledgeJob(input) {
|
|
1046
|
+
export async function acknowledgeJob(input, workspaceRoot = resolveWorkspaceRoot()) {
|
|
1047
|
+
return withWorkspaceRoot(workspaceRoot, () => acknowledgeJobInWorkspace(input));
|
|
1048
|
+
}
|
|
1049
|
+
async function acknowledgeJobInWorkspace(input) {
|
|
990
1050
|
const buf = newEventBuffer();
|
|
991
1051
|
const result = await withSchedulerTransaction(async () => {
|
|
992
1052
|
const queue = readJobQueue();
|
|
@@ -1068,8 +1128,12 @@ export async function acknowledgeJob(input) {
|
|
|
1068
1128
|
await flushTransitionEvents(buf);
|
|
1069
1129
|
return result;
|
|
1070
1130
|
}
|
|
1071
|
-
export async function dispatchJobs(input = {}) {
|
|
1131
|
+
export async function dispatchJobs(input = {}, workspaceRoot = resolveWorkspaceRoot()) {
|
|
1132
|
+
return withWorkspaceRoot(workspaceRoot, () => dispatchJobsInWorkspace(input));
|
|
1133
|
+
}
|
|
1134
|
+
async function dispatchJobsInWorkspace(input = {}) {
|
|
1072
1135
|
const buf = newEventBuffer();
|
|
1136
|
+
const selected = input.job_ids === undefined ? undefined : new Set(input.job_ids);
|
|
1073
1137
|
const result = await withSchedulerTransaction(async () => {
|
|
1074
1138
|
const owner = deriveOwner(input.owner);
|
|
1075
1139
|
const now = parseDate(input.now_iso) ?? new Date();
|
|
@@ -1109,15 +1173,17 @@ export async function dispatchJobs(input = {}) {
|
|
|
1109
1173
|
handoffState: readHandoffRegistry(),
|
|
1110
1174
|
statusEvents: readStatusEvents(500),
|
|
1111
1175
|
};
|
|
1112
|
-
const recoveredJobs = reconcileRecoveryState(queue, table, now, leaseResult.recovery_mode, buf);
|
|
1176
|
+
const recoveredJobs = reconcileRecoveryState(queue, table, now, leaseResult.recovery_mode, buf, selected);
|
|
1113
1177
|
// Heartbeat watchdog: reap running jobs whose worker heartbeat is stale.
|
|
1114
1178
|
// NOTE: dispatch deliberately does NOT re-stamp running-job heartbeats here.
|
|
1115
1179
|
// Only a live worker (heartbeatJob) or a fresh start proves liveness; if the
|
|
1116
1180
|
// dispatcher renewed heartbeats it would mask worker death (root cause of D3).
|
|
1117
1181
|
const staleMs = resolveStaleMs(input.heartbeat_stale_seconds);
|
|
1118
|
-
const reapedJobIds = reapStaleRunningJobs(queue, table, now, staleMs, buf);
|
|
1182
|
+
const reapedJobIds = reapStaleRunningJobs(queue, table, now, staleMs, buf, selected);
|
|
1119
1183
|
const reapedJobs = reapedJobIds.length;
|
|
1120
1184
|
for (const job of queue.jobs) {
|
|
1185
|
+
if (selected && !selected.has(job.job_id))
|
|
1186
|
+
continue;
|
|
1121
1187
|
if (job.kind === "handoff.lifecycle")
|
|
1122
1188
|
continue;
|
|
1123
1189
|
if (TERMINAL_STATUSES.has(job.status))
|
|
@@ -1142,7 +1208,7 @@ export async function dispatchJobs(input = {}) {
|
|
|
1142
1208
|
}
|
|
1143
1209
|
const occupancy = buildOccupancy(table.locks);
|
|
1144
1210
|
const readyJobs = queue.jobs
|
|
1145
|
-
.filter((job) => job.status === "ready" && job.kind !== "handoff.lifecycle")
|
|
1211
|
+
.filter((job) => job.status === "ready" && job.kind !== "handoff.lifecycle" && (!selected || selected.has(job.job_id)))
|
|
1146
1212
|
.sort(compareJobs);
|
|
1147
1213
|
let startedJobs = 0;
|
|
1148
1214
|
for (const job of readyJobs) {
|
|
@@ -1220,7 +1286,10 @@ export async function dispatchJobs(input = {}) {
|
|
|
1220
1286
|
await flushTransitionEvents(buf);
|
|
1221
1287
|
return result;
|
|
1222
1288
|
}
|
|
1223
|
-
export async function dispatchJobNow(input) {
|
|
1289
|
+
export async function dispatchJobNow(input, workspaceRoot = resolveWorkspaceRoot()) {
|
|
1290
|
+
return withWorkspaceRoot(workspaceRoot, () => dispatchJobNowInWorkspace(input));
|
|
1291
|
+
}
|
|
1292
|
+
async function dispatchJobNowInWorkspace(input) {
|
|
1224
1293
|
const buf = newEventBuffer();
|
|
1225
1294
|
const result = await withSchedulerTransaction(async () => {
|
|
1226
1295
|
const owner = deriveOwner(input.owner);
|
|
@@ -1355,7 +1424,70 @@ export async function dispatchJobNow(input) {
|
|
|
1355
1424
|
await flushTransitionEvents(buf);
|
|
1356
1425
|
return result;
|
|
1357
1426
|
}
|
|
1358
|
-
|
|
1427
|
+
/** Atomically claim the side-effecting handler, independently of scheduler owner. */
|
|
1428
|
+
export async function claimJobExecution(input, workspaceRoot = resolveWorkspaceRoot()) {
|
|
1429
|
+
return withWorkspaceRoot(workspaceRoot, () => withSchedulerTransaction(async () => {
|
|
1430
|
+
const now = parseDate(input.now_iso) ?? new Date();
|
|
1431
|
+
const owner = deriveOwner(input.owner);
|
|
1432
|
+
const lease = ensureLeaseForOwner(owner, now);
|
|
1433
|
+
if (!lease.acquired)
|
|
1434
|
+
return { ok: false, reason_code: "scheduler_lease_held", error: "Execution owner does not hold the scheduler lease." };
|
|
1435
|
+
const queue = readJobQueue();
|
|
1436
|
+
const locks = readJobLockTable();
|
|
1437
|
+
const job = queue.jobs.find(row => row.job_id === input.job_id);
|
|
1438
|
+
if (!job || job.status !== "running")
|
|
1439
|
+
return { ok: false, reason_code: "job_not_running", job };
|
|
1440
|
+
if (job.execution || job.execution_children?.length) {
|
|
1441
|
+
const state = probeJobExecution(job);
|
|
1442
|
+
if (state === "dead") {
|
|
1443
|
+
// The prior process may have performed side effects before it exited.
|
|
1444
|
+
// Preserve uncertainty instead of replaying the handler automatically.
|
|
1445
|
+
job.status = "unknown_recovery";
|
|
1446
|
+
job.reason_code = "execution_process_exited";
|
|
1447
|
+
job.error = "Claimed execution process exited; inspect retained outcomes before retrying.";
|
|
1448
|
+
job.next_attempt_at = undefined;
|
|
1449
|
+
releaseLocksForRecoveredJob(locks, job.job_id, "execution_process_exited", now);
|
|
1450
|
+
await persistSchedulerState({ queue, locks, lease: lease.lease });
|
|
1451
|
+
}
|
|
1452
|
+
return { ok: false, reason_code: state === "alive" ? "execution_claimed" : state === "unknown" ? "execution_uncertain" : "execution_dead", job };
|
|
1453
|
+
}
|
|
1454
|
+
if (!hasActiveLockForJob(locks.locks, job.job_id))
|
|
1455
|
+
return { ok: false, reason_code: "execution_missing_locks", job };
|
|
1456
|
+
const identity = readProcessIdentity(process.pid);
|
|
1457
|
+
if (!identity)
|
|
1458
|
+
return { ok: false, reason_code: "identity_unavailable", error: "Cannot safely identify the worker process; handler was not invoked.", job };
|
|
1459
|
+
const token = randomUUID();
|
|
1460
|
+
job.execution = { ...identity, token };
|
|
1461
|
+
job.execution_children = undefined;
|
|
1462
|
+
await persistSchedulerState({ queue, locks, lease: lease.lease });
|
|
1463
|
+
return { ok: true, token, job };
|
|
1464
|
+
}));
|
|
1465
|
+
}
|
|
1466
|
+
/** Register a command before waiting on it; only its claimed attempt may do so. */
|
|
1467
|
+
export async function recordJobExecutionChild(input, workspaceRoot = resolveWorkspaceRoot()) {
|
|
1468
|
+
return withWorkspaceRoot(workspaceRoot, () => withSchedulerTransaction(async () => {
|
|
1469
|
+
const queue = readJobQueue();
|
|
1470
|
+
const job = queue.jobs.find(row => row.job_id === input.job_id);
|
|
1471
|
+
if (!job || job.status !== "running")
|
|
1472
|
+
return { ok: false, reason_code: "job_not_running" };
|
|
1473
|
+
if (!input.token || job.execution?.token !== input.token)
|
|
1474
|
+
return { ok: false, reason_code: "execution_token_mismatch" };
|
|
1475
|
+
const identity = input.identity;
|
|
1476
|
+
if (!Number.isSafeInteger(identity.pid) || identity.pid <= 1 || !identity.started_at || !identity.hostname) {
|
|
1477
|
+
return { ok: false, reason_code: "execution_identity_invalid" };
|
|
1478
|
+
}
|
|
1479
|
+
job.execution_children ??= [];
|
|
1480
|
+
if (!job.execution_children.some(child => child.pid === identity.pid && child.started_at === identity.started_at && child.hostname === identity.hostname)) {
|
|
1481
|
+
job.execution_children.push({ ...identity });
|
|
1482
|
+
await persistSchedulerState({ queue, locks: readJobLockTable(), lease: readSchedulerLease() });
|
|
1483
|
+
}
|
|
1484
|
+
return { ok: true };
|
|
1485
|
+
}));
|
|
1486
|
+
}
|
|
1487
|
+
export async function completeJob(input, workspaceRoot = resolveWorkspaceRoot()) {
|
|
1488
|
+
return withWorkspaceRoot(workspaceRoot, () => completeJobInWorkspace(input));
|
|
1489
|
+
}
|
|
1490
|
+
async function completeJobInWorkspace(input) {
|
|
1359
1491
|
const buf = newEventBuffer();
|
|
1360
1492
|
const result = await withSchedulerTransaction(async () => {
|
|
1361
1493
|
const owner = deriveOwner(input.owner);
|
|
@@ -1406,6 +1538,9 @@ export async function completeJob(input) {
|
|
|
1406
1538
|
};
|
|
1407
1539
|
}
|
|
1408
1540
|
const success = input.success !== false;
|
|
1541
|
+
// The claim belongs to this attempt only. A deliberate retry gets a new one.
|
|
1542
|
+
job.execution = undefined;
|
|
1543
|
+
job.execution_children = undefined;
|
|
1409
1544
|
for (const lock of table.locks) {
|
|
1410
1545
|
if (lock.job_id !== input.job_id)
|
|
1411
1546
|
continue;
|
|
@@ -1497,7 +1632,10 @@ export async function completeJob(input) {
|
|
|
1497
1632
|
* job's heartbeat — the dispatcher deliberately does not, so a missing
|
|
1498
1633
|
* heartbeat reliably signals a dead worker.
|
|
1499
1634
|
*/
|
|
1500
|
-
export async function heartbeatJob(input) {
|
|
1635
|
+
export async function heartbeatJob(input, workspaceRoot = resolveWorkspaceRoot()) {
|
|
1636
|
+
return withWorkspaceRoot(workspaceRoot, () => heartbeatJobInWorkspace(input));
|
|
1637
|
+
}
|
|
1638
|
+
async function heartbeatJobInWorkspace(input) {
|
|
1501
1639
|
return withSchedulerTransaction(async () => {
|
|
1502
1640
|
const owner = deriveOwner(input.owner);
|
|
1503
1641
|
const now = parseDate(input.now_iso) ?? new Date();
|
|
@@ -1542,6 +1680,10 @@ export async function heartbeatJob(input) {
|
|
|
1542
1680
|
};
|
|
1543
1681
|
}
|
|
1544
1682
|
const beatAt = nowIso(now);
|
|
1683
|
+
if (job.reason_code?.startsWith("heartbeat_stale_process_")) {
|
|
1684
|
+
job.reason_code = undefined;
|
|
1685
|
+
job.error = undefined;
|
|
1686
|
+
}
|
|
1545
1687
|
for (const lock of activeLocks) {
|
|
1546
1688
|
lock.heartbeat_at = beatAt;
|
|
1547
1689
|
lock.lease_id = leaseResult.lease.lease_id;
|
|
@@ -1566,21 +1708,25 @@ export async function heartbeatJob(input) {
|
|
|
1566
1708
|
* watchdog cadence separate from dispatch. Does not require lease ownership —
|
|
1567
1709
|
* reaping a dead worker's job must not depend on holding the lease.
|
|
1568
1710
|
*/
|
|
1569
|
-
export async function reapStaleRunningJobsTick(input = {}) {
|
|
1711
|
+
export async function reapStaleRunningJobsTick(input = {}, workspaceRoot = resolveWorkspaceRoot()) {
|
|
1712
|
+
return withWorkspaceRoot(workspaceRoot, () => reapStaleRunningJobsTickInWorkspace(input));
|
|
1713
|
+
}
|
|
1714
|
+
async function reapStaleRunningJobsTickInWorkspace(input = {}) {
|
|
1570
1715
|
const buf = newEventBuffer();
|
|
1571
1716
|
const result = await withSchedulerTransaction(async () => {
|
|
1572
1717
|
const now = parseDate(input.now_iso) ?? new Date();
|
|
1573
1718
|
const staleMs = resolveStaleMs(input.heartbeat_stale_seconds);
|
|
1574
1719
|
const queue = readJobQueue();
|
|
1575
1720
|
const table = readJobLockTable();
|
|
1721
|
+
const priorJobs = JSON.stringify(queue.jobs);
|
|
1576
1722
|
const reapedJobIds = reapStaleRunningJobs(queue, table, now, staleMs, buf);
|
|
1577
|
-
|
|
1723
|
+
// Retained live/unknown claims also carry a persisted diagnostic.
|
|
1724
|
+
if (priorJobs !== JSON.stringify(queue.jobs))
|
|
1578
1725
|
await persistSchedulerState({
|
|
1579
1726
|
queue,
|
|
1580
1727
|
locks: table,
|
|
1581
1728
|
lease: readSchedulerLease(),
|
|
1582
1729
|
});
|
|
1583
|
-
}
|
|
1584
1730
|
return {
|
|
1585
1731
|
ok: true,
|
|
1586
1732
|
queue_path: getJobQueuePath(),
|
package/dist/job-worker.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export declare function listJobHandlerKinds(): string[];
|
|
|
35
35
|
export declare function hasJobHandler(kind: string): boolean;
|
|
36
36
|
export interface RunJobWorkerInput {
|
|
37
37
|
owner?: string;
|
|
38
|
+
/** Execute only these jobs; unrelated jobs are not admitted by this tick. */
|
|
39
|
+
job_ids?: string[];
|
|
38
40
|
/** Deterministic clock override; threaded through every scheduler call. */
|
|
39
41
|
now_iso?: string;
|
|
40
42
|
lease_ttl_seconds?: number;
|
|
@@ -60,7 +62,7 @@ export interface WorkerJobOutcome {
|
|
|
60
62
|
export interface WorkerSkippedJob {
|
|
61
63
|
job_id: string;
|
|
62
64
|
kind: string;
|
|
63
|
-
reason: "no_handler" | "max_jobs";
|
|
65
|
+
reason: "no_handler" | "max_jobs" | "execution_claimed" | "execution_uncertain";
|
|
64
66
|
}
|
|
65
67
|
export interface RunJobWorkerResult {
|
|
66
68
|
owner: string;
|
package/dist/job-worker.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* across a handler invocation, so a slow or hung handler cannot wedge the
|
|
18
18
|
* scheduler for other owners.
|
|
19
19
|
*/
|
|
20
|
-
import { completeJob, dispatchJobs, heartbeatJob, readJobQueue, readSchedulerLease, } from "./job-scheduler.js";
|
|
20
|
+
import { completeJob, claimJobExecution, dispatchJobs, heartbeatJob, readJobQueue, readSchedulerLease, } from "./job-scheduler.js";
|
|
21
21
|
import { resolveWorkspaceRoot } from "./helpers.js";
|
|
22
22
|
const handlers = new Map();
|
|
23
23
|
export function registerJobHandler(kind, handler) {
|
|
@@ -66,6 +66,7 @@ export async function runJobWorkerOnce(input = {}) {
|
|
|
66
66
|
now_iso: input.now_iso,
|
|
67
67
|
lease_ttl_seconds: input.lease_ttl_seconds,
|
|
68
68
|
heartbeat_stale_seconds: input.heartbeat_stale_seconds,
|
|
69
|
+
job_ids: input.job_ids,
|
|
69
70
|
});
|
|
70
71
|
const executed = [];
|
|
71
72
|
const skipped = [];
|
|
@@ -83,7 +84,8 @@ export async function runJobWorkerOnce(input = {}) {
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
const queue = readJobQueue();
|
|
86
|
-
const
|
|
87
|
+
const selected = input.job_ids === undefined ? undefined : new Set(input.job_ids);
|
|
88
|
+
const runningJobs = queue.jobs.filter((job) => job.status === "running" && (!selected || selected.has(job.job_id)));
|
|
87
89
|
const maxJobs = typeof input.max_jobs === "number" && input.max_jobs > 0
|
|
88
90
|
? Math.floor(input.max_jobs)
|
|
89
91
|
: Number.POSITIVE_INFINITY;
|
|
@@ -98,6 +100,11 @@ export async function runJobWorkerOnce(input = {}) {
|
|
|
98
100
|
skipped.push({ job_id: job.job_id, kind: job.kind, reason: "max_jobs" });
|
|
99
101
|
continue;
|
|
100
102
|
}
|
|
103
|
+
const claim = await claimJobExecution({ job_id: job.job_id, owner, now_iso: input.now_iso });
|
|
104
|
+
if (!claim.ok) {
|
|
105
|
+
skipped.push({ job_id: job.job_id, kind: job.kind, reason: claim.reason_code === "execution_claimed" ? "execution_claimed" : "execution_uncertain" });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
101
108
|
executedCount += 1;
|
|
102
109
|
// Prove liveness immediately, then keep beating while the handler runs.
|
|
103
110
|
await heartbeatJob({ job_id: job.job_id, owner, now_iso: input.now_iso });
|
|
@@ -113,7 +120,7 @@ export async function runJobWorkerOnce(input = {}) {
|
|
|
113
120
|
}
|
|
114
121
|
let handlerResult;
|
|
115
122
|
try {
|
|
116
|
-
handlerResult = await handler({ job, owner });
|
|
123
|
+
handlerResult = await handler({ job: claim.job ?? job, owner });
|
|
117
124
|
}
|
|
118
125
|
catch (err) {
|
|
119
126
|
handlerResult = { success: false, error: errorMessage(err) };
|
package/dist/kanban.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type KanbanColumn = "backlog" | "blocked" | "in_progress" | "gated" | "done";
|
|
1
|
+
export type KanbanColumn = "backlog" | "blocked" | "in_progress" | "gated" | "done" | "cancelled";
|
|
2
2
|
export interface AceKanbanCard {
|
|
3
3
|
id: string;
|
|
4
4
|
title: string;
|
|
@@ -10,8 +10,10 @@ export interface AceKanbanBoard {
|
|
|
10
10
|
in_progress: AceKanbanCard[];
|
|
11
11
|
gated: AceKanbanCard[];
|
|
12
12
|
done: AceKanbanCard[];
|
|
13
|
+
cancelled: AceKanbanCard[];
|
|
13
14
|
}
|
|
14
15
|
export interface AceKanbanSnapshot {
|
|
16
|
+
refresh_scheduled?: boolean;
|
|
15
17
|
version: number;
|
|
16
18
|
generated_at: string;
|
|
17
19
|
trigger: string;
|
|
@@ -21,6 +23,7 @@ export interface AceKanbanSnapshot {
|
|
|
21
23
|
json_path: string;
|
|
22
24
|
}
|
|
23
25
|
export declare function renderKanbanHtml(snapshot: AceKanbanSnapshot): string;
|
|
26
|
+
export declare function formatKanbanRefresh(snapshot: AceKanbanSnapshot): string;
|
|
24
27
|
export declare function refreshKanbanArtifacts(trigger: string): AceKanbanSnapshot;
|
|
25
28
|
export declare function getOrRefreshKanbanSnapshot(trigger: string): AceKanbanSnapshot;
|
|
26
29
|
export declare function shouldAutoRefreshKanbanForPath(path: string): boolean;
|
package/dist/kanban.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { ACE_ROOT_REL, ACE_TASKS_ROOT_REL, safeRead, safeWrite, wsPath } from "./helpers.js";
|
|
1
|
+
import { ACE_ROOT_REL, ACE_TASKS_ROOT_REL, safeRead, safeWrite, wsPath, resolveWorkspaceRoot, withWorkspaceRoot } from "./helpers.js";
|
|
2
2
|
import { isReadError, slugify } from "./shared.js";
|
|
3
3
|
import { readHandoffRegistry } from "./handoff-registry.js";
|
|
4
4
|
import { readTodoState } from "./todo-state.js";
|
|
5
5
|
/** Debounce threshold in milliseconds. Skip re-render if within this window. */
|
|
6
6
|
const KANBAN_DEBOUNCE_MS = 2000;
|
|
7
|
-
|
|
7
|
+
const renderState = new Map();
|
|
8
8
|
const KANBAN_JSON_REL = "agent-state/kanban.json";
|
|
9
9
|
const KANBAN_HTML_REL = "agent-state/kanban.html";
|
|
10
|
-
const COLUMNS = ["backlog", "blocked", "in_progress", "gated", "done"];
|
|
10
|
+
const COLUMNS = ["backlog", "blocked", "in_progress", "gated", "done", "cancelled"];
|
|
11
11
|
function nowIso() {
|
|
12
12
|
return new Date().toISOString();
|
|
13
13
|
}
|
|
@@ -18,6 +18,7 @@ function emptyBoard() {
|
|
|
18
18
|
in_progress: [],
|
|
19
19
|
gated: [],
|
|
20
20
|
done: [],
|
|
21
|
+
cancelled: [],
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
function escapeHtml(input) {
|
|
@@ -236,7 +237,7 @@ export function renderKanbanHtml(snapshot) {
|
|
|
236
237
|
"@keyframes spin{to{transform:rotate(360deg)}}",
|
|
237
238
|
"h1{margin:0;font-family:'Space Grotesk','Avenir Next',sans-serif;font-size:24px}",
|
|
238
239
|
".stamp{margin:0;font-size:12px;opacity:.75}",
|
|
239
|
-
".grid{display:grid;grid-template-columns:repeat(
|
|
240
|
+
".grid{display:grid;grid-template-columns:repeat(6,minmax(220px,1fr));gap:12px}",
|
|
240
241
|
".column{background:rgba(255,255,255,.62);border:1px solid var(--line);border-radius:12px;min-height:220px}",
|
|
241
242
|
".column>header{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid var(--line)}",
|
|
242
243
|
".column h2{margin:0;font-size:14px;text-transform:capitalize}",
|
|
@@ -265,16 +266,38 @@ export function renderKanbanHtml(snapshot) {
|
|
|
265
266
|
"</html>",
|
|
266
267
|
].join("");
|
|
267
268
|
}
|
|
269
|
+
export function formatKanbanRefresh(snapshot) {
|
|
270
|
+
return snapshot.refresh_scheduled
|
|
271
|
+
? `Kanban refresh scheduled: ${snapshot.html_path} (last generated ${snapshot.generated_at})`
|
|
272
|
+
: `Kanban updated: ${snapshot.html_path}`;
|
|
273
|
+
}
|
|
268
274
|
export function refreshKanbanArtifacts(trigger) {
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
275
|
+
const root = resolveWorkspaceRoot();
|
|
276
|
+
const state = renderState.get(root) ?? { last: 0 };
|
|
277
|
+
const delay = KANBAN_DEBOUNCE_MS - (Date.now() - state.last);
|
|
278
|
+
const cached = readKanbanSnapshot();
|
|
279
|
+
if (delay > 0 && cached) {
|
|
280
|
+
if (state.timer)
|
|
281
|
+
clearTimeout(state.timer);
|
|
282
|
+
state.timer = setTimeout(() => withWorkspaceRoot(root, () => {
|
|
283
|
+
state.timer = undefined;
|
|
284
|
+
try {
|
|
285
|
+
renderKanbanNow(trigger);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
state.last = 0;
|
|
289
|
+
console.error(`Kanban refresh failed for ${root}: ${error instanceof Error ? error.message : String(error)}`);
|
|
290
|
+
}
|
|
291
|
+
}), delay);
|
|
292
|
+
state.timer.unref();
|
|
293
|
+
renderState.set(root, state);
|
|
294
|
+
return { ...cached, refresh_scheduled: true };
|
|
276
295
|
}
|
|
277
|
-
|
|
296
|
+
if (state.timer)
|
|
297
|
+
clearTimeout(state.timer);
|
|
298
|
+
return renderKanbanNow(trigger);
|
|
299
|
+
}
|
|
300
|
+
function renderKanbanNow(trigger) {
|
|
278
301
|
const board = emptyBoard();
|
|
279
302
|
addTodoCards(board);
|
|
280
303
|
addStatusCard(board);
|
|
@@ -292,15 +315,17 @@ export function refreshKanbanArtifacts(trigger) {
|
|
|
292
315
|
in_progress: board.in_progress.length,
|
|
293
316
|
gated: board.gated.length,
|
|
294
317
|
done: board.done.length,
|
|
318
|
+
cancelled: board.cancelled.length,
|
|
295
319
|
},
|
|
296
320
|
html_path: wsPath(KANBAN_HTML_REL),
|
|
297
321
|
json_path: wsPath(KANBAN_JSON_REL),
|
|
298
322
|
};
|
|
299
323
|
safeWrite(KANBAN_HTML_REL, renderKanbanHtml(snapshot));
|
|
300
324
|
safeWrite(KANBAN_JSON_REL, JSON.stringify(snapshot, null, 2));
|
|
325
|
+
renderState.set(resolveWorkspaceRoot(), { last: Date.now() });
|
|
301
326
|
return snapshot;
|
|
302
327
|
}
|
|
303
|
-
|
|
328
|
+
function readKanbanSnapshot() {
|
|
304
329
|
const raw = safeRead(KANBAN_JSON_REL);
|
|
305
330
|
if (!isReadError(raw)) {
|
|
306
331
|
const parsed = tryParseJson(raw);
|
|
@@ -316,14 +341,17 @@ export function getOrRefreshKanbanSnapshot(trigger) {
|
|
|
316
341
|
version,
|
|
317
342
|
generated_at: generated,
|
|
318
343
|
trigger: savedTrigger,
|
|
319
|
-
board,
|
|
320
|
-
counts,
|
|
344
|
+
board: { ...emptyBoard(), ...board },
|
|
345
|
+
counts: { ...Object.fromEntries(COLUMNS.map((key) => [key, 0])), ...counts },
|
|
321
346
|
html_path: htmlPath,
|
|
322
347
|
json_path: jsonPath,
|
|
323
348
|
};
|
|
324
349
|
}
|
|
325
350
|
}
|
|
326
|
-
return
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
export function getOrRefreshKanbanSnapshot(trigger) {
|
|
354
|
+
return readKanbanSnapshot() ?? refreshKanbanArtifacts(trigger);
|
|
327
355
|
}
|
|
328
356
|
export function shouldAutoRefreshKanbanForPath(path) {
|
|
329
357
|
const normalized = path.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type LearnedArtifact } from './learning-records.js';
|
|
2
|
+
export { decodeLearned, type LearnedArtifact } from './learning-records.js';
|
|
3
|
+
import { type PlaybookScope, type PlaybookSection } from './store/repositories/playbook-repository.js';
|
|
4
|
+
import { type IAcePackedStore } from './store/types.js';
|
|
5
|
+
import type { GoalEngineDeps } from './goal-engine.js';
|
|
6
|
+
export interface LearnedPayload {
|
|
7
|
+
lessons?: Array<{
|
|
8
|
+
op: 'add' | 'edit' | 'retire';
|
|
9
|
+
bullet_id?: string;
|
|
10
|
+
section: PlaybookSection;
|
|
11
|
+
content: string;
|
|
12
|
+
scope: PlaybookScope;
|
|
13
|
+
episodes: string[];
|
|
14
|
+
retired_episodes?: string[];
|
|
15
|
+
}>;
|
|
16
|
+
skill?: {
|
|
17
|
+
op: 'add' | 'edit';
|
|
18
|
+
slug: string;
|
|
19
|
+
skill_md: string;
|
|
20
|
+
references?: Record<string, string>;
|
|
21
|
+
episodes: string[];
|
|
22
|
+
retired_episodes?: string[];
|
|
23
|
+
};
|
|
24
|
+
instruction?: {
|
|
25
|
+
role: string;
|
|
26
|
+
text: string;
|
|
27
|
+
episodes: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface LearningAcceptance {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
ids: string[];
|
|
33
|
+
errors: string[];
|
|
34
|
+
reason_code?: string;
|
|
35
|
+
options?: Array<{
|
|
36
|
+
tool: string;
|
|
37
|
+
args: Record<string, unknown>;
|
|
38
|
+
why: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
export declare function listLearnedArtifacts(root: string): LearnedArtifact[];
|
|
42
|
+
export declare function withLearnedStore<T>(root: string, apply: (store: IAcePackedStore) => Promise<T>): Promise<T>;
|
|
43
|
+
export declare function saveLearnedArtifact(root: string, item: LearnedArtifact): Promise<void>;
|
|
44
|
+
export declare function updateLearnedArtifact(root: string, key: string, update: (item: LearnedArtifact) => LearnedArtifact | undefined): Promise<void>;
|
|
45
|
+
export declare function prepareLearned(root: string, payload: LearnedPayload, context: {
|
|
46
|
+
goal_id: string;
|
|
47
|
+
step_id: string;
|
|
48
|
+
attempt: number;
|
|
49
|
+
}, deps?: GoalEngineDeps): Promise<{
|
|
50
|
+
result: LearningAcceptance;
|
|
51
|
+
publish: () => Promise<void>;
|
|
52
|
+
}>;
|
|
53
|
+
export declare function acceptLearned(root: string, payload: LearnedPayload, context: {
|
|
54
|
+
goal_id: string;
|
|
55
|
+
step_id: string;
|
|
56
|
+
attempt: number;
|
|
57
|
+
}, deps?: GoalEngineDeps): Promise<LearningAcceptance>;
|
|
58
|
+
export declare function regenerateLearnedProjection(root: string): Promise<void>;
|
|
59
|
+
export declare function ensureLearningStep(root: string, force?: {
|
|
60
|
+
scope: PlaybookScope;
|
|
61
|
+
reason: string;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
export declare function reopenLearningCorrection(root: string, errors: string[]): Promise<void>;
|
|
64
|
+
//# sourceMappingURL=learned-artifacts.d.ts.map
|