agents-relay 2.0.158 → 2.0.160
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/dist/cli-help.js +1 -1
- package/dist/cli.js +2 -5
- package/dist/manual-plan.js +9 -19
- package/dist/planner-history-cleanup.js +33 -0
- package/dist/planner-runtime.js +20 -1
- package/dist/planner-task-context.js +26 -0
- package/dist/planner.js +12 -18
- package/dist/reconciler.js +2 -2
- package/dist/relayd-control.js +1 -2
- package/package.json +3 -3
- package/skills/agents-relay/agents/planner.agent.md +8 -2
- package/dist/cli-plan-clean-history.js +0 -16
package/dist/cli-help.js
CHANGED
|
@@ -173,7 +173,7 @@ Required: --task-id ID and the normal Job storage options.`,
|
|
|
173
173
|
Agent submit: agents-relay plan apply --repo OWNER/REPO --pr PR_NUMBER --id JOB_ID --plan-id PLAN_ID --execution-id EXECUTION_ID --file plan.json. Validation errors reject without partial task writes and can be corrected inside the same Planner execution.
|
|
174
174
|
|
|
175
175
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
176
|
-
Optional: --prompt TEXT or --prompt-file PATH|- (mutually exclusive). The prompt is additional guidance for this planning round and never replaces the durable Job objective. --force-clean-history
|
|
176
|
+
Optional: --prompt TEXT or --prompt-file PATH|- (mutually exclusive). The prompt is additional guidance for this planning round and never replaces the durable Job objective. --force-clean-history asks Planner to inspect authoritative failed Tasks, decide whether evidence supports superseded/invalid classification or cancellation of a failed review, and apply selected changes through guarded task commands before submitting the Plan. Tasks without sufficient evidence remain unresolved. The durable Plan records the cleanup assessment, and plan apply remains the required receipt. Text responses are never applied as a Plan; unknown execution outcomes are not classified without proof.
|
|
177
177
|
|
|
178
178
|
The command persists one real plan-N round, materializes validated next_tasks with planner provenance, and wakes normal reconciliation. It works for zero-task DRAFT Jobs. Historical planner-N rounds remain readable, but new rounds use plan-N.
|
|
179
179
|
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,6 @@ import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
|
24
24
|
import { applyTaskHoldAction, applyTaskQueueAction, dispatchTaskAction } from './task-actions.js';
|
|
25
25
|
import { applyJobActivateAction, applyJobCancelAction } from './job-actions.js';
|
|
26
26
|
import { classifyFailedTask } from './task-actions.js';
|
|
27
|
-
import { cleanHistoryOption } from './cli-plan-clean-history.js';
|
|
28
27
|
import { resolveExecutionContext } from './execution-context.js';
|
|
29
28
|
import { startServiceExternalActionFlusher } from './service-external-actions.js';
|
|
30
29
|
import { handleServicePublication } from './service-publication.js';
|
|
@@ -807,8 +806,7 @@ async function main() {
|
|
|
807
806
|
throw new Error(`${command} requires --repo and --id`);
|
|
808
807
|
if (command === 'plan') {
|
|
809
808
|
const guidance = hasArg(args, '--prompt') || hasArg(args, '--prompt-file') ? await textOption(args, '--prompt', '--prompt-file') : undefined;
|
|
810
|
-
|
|
811
|
-
console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.plan', jobId, ...(guidance?.trim() ? { guidance: guidance.trim() } : {}), ...(cleanHistory ? { cleanHistory } : {}) }), null, 2));
|
|
809
|
+
console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.plan', jobId, ...(guidance?.trim() ? { guidance: guidance.trim() } : {}), forceCleanHistory: hasArg(args, '--force-clean-history') }), null, 2));
|
|
812
810
|
return;
|
|
813
811
|
}
|
|
814
812
|
if (command === 'reconcile') {
|
|
@@ -834,11 +832,10 @@ async function main() {
|
|
|
834
832
|
const { store, job, localFile } = loaded;
|
|
835
833
|
if (command === 'plan') {
|
|
836
834
|
const guidance = hasArg(args, '--prompt') || hasArg(args, '--prompt-file') ? await textOption(args, '--prompt', '--prompt-file') : undefined;
|
|
837
|
-
const cleanHistory = await cleanHistoryOption(args);
|
|
838
835
|
await withEventBus(args, async (bus) => {
|
|
839
836
|
const reconciler = runtime(store, args, bus);
|
|
840
837
|
try {
|
|
841
|
-
const result = await reconciler.planJob(job.id, guidance?.trim() || undefined,
|
|
838
|
+
const result = await reconciler.planJob(job.id, guidance?.trim() || undefined, hasArg(args, '--force-clean-history'));
|
|
842
839
|
await saveLocal(localFile, result.job);
|
|
843
840
|
await publishWake(bus, result.job, result.round.id, `Planning round ${result.round.id} completed`);
|
|
844
841
|
console.log(JSON.stringify(result.round, null, 2));
|
package/dist/manual-plan.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { dispatchJobLifecycleSync } from './workflow/lifecycle.js';
|
|
2
2
|
import { createPlanRound, pendingPlan } from './planner.js';
|
|
3
3
|
import { applyJobActivateAction } from './job-actions.js';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
export async function executeManualPlan(store, jobId, guidance, timeoutMs, launchPlanner, forceCleanHistory = false) {
|
|
5
|
+
if (forceCleanHistory && !store.mutateJob)
|
|
6
|
+
throw new Error('--force-clean-history requires atomic durable SQLite mutations');
|
|
6
7
|
let job = await store.load(jobId);
|
|
7
8
|
if (['COMPLETED', 'CANCELLED'].includes(job.state))
|
|
8
9
|
throw new Error(`Cannot plan terminal job ${job.id} (${job.state})`);
|
|
@@ -19,23 +20,8 @@ export async function executeManualPlan(store, jobId, guidance, timeoutMs, launc
|
|
|
19
20
|
if (current.pause)
|
|
20
21
|
throw new Error(`Cannot plan paused Job ${current.id}; resume it first`);
|
|
21
22
|
const concurrent = pendingPlan(current);
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
throw new Error(`--force-clean-history requires no pending Plan (${concurrent.id})`);
|
|
25
|
-
const failed = current.tasks.filter(task => task.kind !== 'planner' && task.state === 'FAILED' && !task.resolutionClassification);
|
|
26
|
-
const provided = new Map(cleanHistory.map(item => [item.taskId, item]));
|
|
27
|
-
if (provided.size !== cleanHistory.length || failed.length !== provided.size || failed.some(task => !provided.has(task.id)))
|
|
28
|
-
throw new Error('--force-clean-history requires exactly one explicit classification for every FAILED work Task');
|
|
29
|
-
for (const task of failed) {
|
|
30
|
-
const item = provided.get(task.id);
|
|
31
|
-
if (item.targetTaskId === task.id || !current.tasks.some(target => target.id === item.targetTaskId))
|
|
32
|
-
throw new Error(`Classification for ${task.id} must target a different durable Task in this Job`);
|
|
33
|
-
classifyTaskResolution(task, item);
|
|
34
|
-
}
|
|
35
|
-
const unknown = current.tasks.filter(task => task.kind !== 'planner' && task.state === 'FAILED' && task.failureReason === 'worker_outcome_unknown');
|
|
36
|
-
if (unknown.length)
|
|
37
|
-
throw new Error(`--force-clean-history cannot proceed while FAILED Task execution outcomes are unknown: ${unknown.map(task => task.id).join(', ')}`);
|
|
38
|
-
}
|
|
23
|
+
if (forceCleanHistory && concurrent)
|
|
24
|
+
throw new Error(`--force-clean-history requires no pending Plan (${concurrent.id})`);
|
|
39
25
|
if (concurrent) {
|
|
40
26
|
if (concurrent.executionId)
|
|
41
27
|
throw new Error(`Planning round ${concurrent.id} is already PLANNING`);
|
|
@@ -43,6 +29,10 @@ export async function executeManualPlan(store, jobId, guidance, timeoutMs, launc
|
|
|
43
29
|
}
|
|
44
30
|
else {
|
|
45
31
|
round = createPlanRound(current, { guidance, manual: true, timeoutMs });
|
|
32
|
+
if (forceCleanHistory) {
|
|
33
|
+
round.forceCleanHistory = true;
|
|
34
|
+
round.cleanHistoryTaskIds = current.tasks.filter(task => task.kind !== 'planner' && task.state === 'FAILED' && !task.resolutionClassification).map(task => task.id);
|
|
35
|
+
}
|
|
46
36
|
current.plans = [...(current.plans ?? []), structuredClone(round)].sort((a, b) => a.round - b.round);
|
|
47
37
|
}
|
|
48
38
|
if (current.state === 'DRAFT') {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function record(value, field) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3
|
+
throw new Error(`Planner ${field} must be an object`);
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
function requiredText(value, field) {
|
|
7
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
8
|
+
throw new Error(`Planner ${field} must be a non-empty string`);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function parsePlannerHistoryCleanup(value) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
throw new Error('Planner history_cleanup must be an array when provided');
|
|
16
|
+
const ids = new Set();
|
|
17
|
+
return value.map((item, index) => {
|
|
18
|
+
const row = record(item, `history_cleanup[${index}]`);
|
|
19
|
+
const taskId = requiredText(row.taskId, `history_cleanup[${index}].taskId`);
|
|
20
|
+
const outcome = row.outcome;
|
|
21
|
+
const evidence = requiredText(row.evidence, `history_cleanup[${index}].evidence`);
|
|
22
|
+
const reason = requiredText(row.reason, `history_cleanup[${index}].reason`);
|
|
23
|
+
if (!['superseded', 'invalid', 'cancelled_review', 'unresolved'].includes(String(outcome)))
|
|
24
|
+
throw new Error(`Planner history_cleanup[${index}].outcome is invalid`);
|
|
25
|
+
if (ids.has(taskId))
|
|
26
|
+
throw new Error(`Planner history_cleanup contains duplicate Task ${taskId}`);
|
|
27
|
+
ids.add(taskId);
|
|
28
|
+
const targetTaskId = row.targetTaskId === undefined ? undefined : requiredText(row.targetTaskId, `history_cleanup[${index}].targetTaskId`);
|
|
29
|
+
if ((outcome === 'superseded' || outcome === 'invalid') && !targetTaskId)
|
|
30
|
+
throw new Error(`Planner history_cleanup[${index}].targetTaskId is required for ${String(outcome)}`);
|
|
31
|
+
return { taskId, outcome: outcome, evidence, reason, ...(targetTaskId ? { targetTaskId } : {}) };
|
|
32
|
+
});
|
|
33
|
+
}
|
package/dist/planner-runtime.js
CHANGED
|
@@ -136,6 +136,25 @@ export async function finishPlanner(ports, jobId, planId, executionId, error, ra
|
|
|
136
136
|
let planCommitted = false;
|
|
137
137
|
try {
|
|
138
138
|
const parsed = parsePlannerResult(raw);
|
|
139
|
+
if (plan.forceCleanHistory) {
|
|
140
|
+
const cleanup = parsed.history_cleanup ?? [];
|
|
141
|
+
const expected = new Set(plan.cleanHistoryTaskIds ?? []);
|
|
142
|
+
if (cleanup.length !== expected.size || cleanup.some(item => !expected.delete(item.taskId)) || expected.size)
|
|
143
|
+
throw new Error('--force-clean-history requires exactly one Planner disposition for each failed Task in its durable snapshot');
|
|
144
|
+
for (const item of cleanup) {
|
|
145
|
+
const task = job.tasks.find(candidate => candidate.id === item.taskId);
|
|
146
|
+
if (!task)
|
|
147
|
+
throw new Error(`Clean history Task ${item.taskId} disappeared`);
|
|
148
|
+
if (task.failureReason === 'worker_outcome_unknown' && item.outcome !== 'unresolved')
|
|
149
|
+
throw new Error(`Unknown execution outcome for ${item.taskId} must remain unresolved without graph-verified proof`);
|
|
150
|
+
if ((item.outcome === 'superseded' || item.outcome === 'invalid') && (task.resolutionClassification?.kind !== item.outcome || task.resolutionClassification.targetTaskId !== item.targetTaskId || task.resolutionClassification.evidence !== item.evidence || task.resolutionClassification.reason !== item.reason))
|
|
151
|
+
throw new Error(`Clean history classification for ${item.taskId} was not applied through task classify with matching evidence`);
|
|
152
|
+
if (item.outcome === 'cancelled_review' && (task.agentName !== 'reviewer' || task.state !== 'CANCELLED'))
|
|
153
|
+
throw new Error(`Failed review ${item.taskId} must be cancelled through task cancel before Plan apply`);
|
|
154
|
+
if (item.outcome === 'unresolved' && (task.state !== 'FAILED' || task.resolutionClassification))
|
|
155
|
+
throw new Error(`Unresolved history item ${item.taskId} changed after Planner assessment`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
139
158
|
const result = assignPlannerTaskIds(parsed, job.repository, job.id, plan.id);
|
|
140
159
|
const executionContext = await ports.plannerContext(job, plan);
|
|
141
160
|
validatePlannerHierarchy(job.tasks, result.next_tasks);
|
|
@@ -334,7 +353,7 @@ export async function launchPlanExecution(ports, job, plan) {
|
|
|
334
353
|
let promise;
|
|
335
354
|
try {
|
|
336
355
|
const executionContext = await ports.plannerContext(job, plan);
|
|
337
|
-
const context = { planExecution: { planId: plan.id, executionId }, job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: plan.previousPlanId, trigger: plan.trigger, executionContext, retryFeedback: plan.retryFeedback, guidance: plan.guidance };
|
|
356
|
+
const context = { planExecution: { planId: plan.id, executionId }, forceCleanHistory: plan.forceCleanHistory, cleanHistoryTaskIds: plan.cleanHistoryTaskIds, job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: plan.previousPlanId, trigger: plan.trigger, executionContext, retryFeedback: plan.retryFeedback, guidance: plan.guidance };
|
|
338
357
|
promise = planner.plan(context, controller.signal);
|
|
339
358
|
}
|
|
340
359
|
catch (error) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function boundedText(value, max = 4000) {
|
|
2
|
+
if (value === null || value === undefined || value.length <= max)
|
|
3
|
+
return value;
|
|
4
|
+
return value.slice(0, max) + `\n...[truncated ${value.length - max} chars]`;
|
|
5
|
+
}
|
|
6
|
+
export function plannerTaskContext(task) {
|
|
7
|
+
return {
|
|
8
|
+
id: task.id,
|
|
9
|
+
title: task.title,
|
|
10
|
+
kind: task.kind ?? 'work',
|
|
11
|
+
parent_task_id: task.parentTaskId,
|
|
12
|
+
dependencies: task.dependencies,
|
|
13
|
+
state: task.state,
|
|
14
|
+
failure_reason: task.failureReason,
|
|
15
|
+
input: task.kind === 'planner' ? undefined : boundedText(task.input),
|
|
16
|
+
result_summary: boundedText(task.result?.summary),
|
|
17
|
+
result_evidence: task.result?.data ? boundedText(JSON.stringify(task.result.data), 2000) : undefined,
|
|
18
|
+
execution_history: task.executionHistory?.map(item => ({ id: item.id, attempt: item.attempt, started_at: item.startedAt })),
|
|
19
|
+
platform_failure: task.platformFailure ? { classification: task.platformFailure.classification, evidence: boundedText(task.platformFailure.evidence), safe_to_reroute: task.platformFailure.safeToReroute } : undefined,
|
|
20
|
+
comments: task.comments?.slice(-10).map(comment => ({ author: comment.author, text: boundedText(comment.text, 1000), created_at: comment.createdAt })),
|
|
21
|
+
resolution_classification: task.resolutionClassification,
|
|
22
|
+
error: boundedText(task.error, 2000),
|
|
23
|
+
blocked_baseline: task.blockedBaseline,
|
|
24
|
+
unblock: task.unblock,
|
|
25
|
+
};
|
|
26
|
+
}
|
package/dist/planner.js
CHANGED
|
@@ -2,7 +2,7 @@ import { buildReferenceFirstPrompt, renderAgentPrompt } from './agent-prompt-bui
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { parseTestInjection } from './e2e-injection.js';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
5
|
+
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
6
6
|
import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
|
|
7
7
|
import { validateResolutionProposal, unresolvedTask } from './workflow/task-resolution.js';
|
|
8
8
|
import { executionContextPrompt, BUNDLED_AGENTS_ROOT } from './execution-context.js';
|
|
@@ -13,6 +13,9 @@ import { createTaskEntity } from './workflow/entity-creation.js';
|
|
|
13
13
|
import { prepareReviewerThreadDelivery, chatgptThreadRequirements } from './reviewer-thread.js';
|
|
14
14
|
import { spawnOwnedProcess } from './owned-process.js';
|
|
15
15
|
import { requiredPlannerTaskTitle } from './planner-task-title.js';
|
|
16
|
+
import { parsePlannerHistoryCleanup } from './planner-history-cleanup.js';
|
|
17
|
+
import { plannerTaskContext } from './planner-task-context.js';
|
|
18
|
+
import { relayStoreDirectory } from './local-job-path.js';
|
|
16
19
|
export function semanticTaskTree(tasks) {
|
|
17
20
|
const work = tasks.filter(task => task.kind !== 'planner');
|
|
18
21
|
const ids = new Set(work.map(task => task.id));
|
|
@@ -268,6 +271,7 @@ export function parsePlannerResult(value) {
|
|
|
268
271
|
unblockKeys.add(key);
|
|
269
272
|
return { repository, prNumber: Number(prNumber), jobId, taskId, resolutionMode: resolutionMode };
|
|
270
273
|
});
|
|
274
|
+
const history_cleanup = parsePlannerHistoryCleanup(root.history_cleanup);
|
|
271
275
|
const ids = new Set();
|
|
272
276
|
const next_tasks = root.next_tasks.map((item, index) => {
|
|
273
277
|
const task = record(item, `next_tasks[${index}]`);
|
|
@@ -320,7 +324,7 @@ export function parsePlannerResult(value) {
|
|
|
320
324
|
continuation: task.continuation, maxAttempts, timeoutMs, resolution: resolutionProposal(task.resolution, `next_tasks[${index}].resolution`)
|
|
321
325
|
};
|
|
322
326
|
});
|
|
323
|
-
return { objective_status: status, assessment: root.assessment, next_tasks, unblocks };
|
|
327
|
+
return { objective_status: status, assessment: root.assessment, next_tasks, unblocks, ...(history_cleanup ? { history_cleanup } : {}) };
|
|
324
328
|
}
|
|
325
329
|
export function validatePlannerProgress(job, result) {
|
|
326
330
|
if (result.objective_status !== 'in_progress' || result.next_tasks.length > 0 || result.unblocks.length > 0)
|
|
@@ -420,6 +424,8 @@ export function plannerInput(context) {
|
|
|
420
424
|
apply_cli: context.planExecution ? `agents-relay plan apply --repo ${context.job.repository} --pr ${context.job.prNumber} --id ${context.job.id} --plan-id ${context.planExecution.planId} --execution-id ${context.planExecution.executionId} --file PATH` : undefined,
|
|
421
425
|
plan_execution: context.planExecution, retry_feedback: context.retryFeedback,
|
|
422
426
|
caller_prompt: context.guidance,
|
|
427
|
+
force_clean_history: context.forceCleanHistory === true,
|
|
428
|
+
clean_history_task_ids: context.cleanHistoryTaskIds ?? [],
|
|
423
429
|
previous_planner_task_id: context.previousPlannerTaskId,
|
|
424
430
|
plan_trigger: context.trigger,
|
|
425
431
|
legacy_change_cycle: context.job.changeCycle,
|
|
@@ -438,24 +444,11 @@ export function plannerInput(context) {
|
|
|
438
444
|
})),
|
|
439
445
|
project: project ? { name: project.name, path: project.path } : undefined,
|
|
440
446
|
visible_agents: visibleAgents ?? [],
|
|
441
|
-
tasks: context.tasks.map(task => ({
|
|
442
|
-
id: task.id,
|
|
443
|
-
title: task.title,
|
|
444
|
-
kind: task.kind ?? 'work',
|
|
445
|
-
parent_task_id: task.parentTaskId,
|
|
446
|
-
state: task.state,
|
|
447
|
-
input: task.kind === 'planner' ? undefined : boundedText(task.input),
|
|
448
|
-
result_summary: boundedText(task.result?.summary),
|
|
449
|
-
planner_result: task.plannerResult ? {
|
|
447
|
+
tasks: context.tasks.map(task => ({ ...plannerTaskContext(task), planner_result: task.plannerResult ? {
|
|
450
448
|
objective_status: task.plannerResult.objective_status,
|
|
451
449
|
assessment: boundedText(task.plannerResult.assessment),
|
|
452
450
|
next_task_ids: task.plannerResult.next_tasks.map(item => item.id),
|
|
453
|
-
} : undefined,
|
|
454
|
-
resolution_classification: task.resolutionClassification,
|
|
455
|
-
error: boundedText(task.error, 2000),
|
|
456
|
-
blocked_baseline: task.blockedBaseline,
|
|
457
|
-
unblock: task.unblock,
|
|
458
|
-
})),
|
|
451
|
+
} : undefined })),
|
|
459
452
|
});
|
|
460
453
|
}
|
|
461
454
|
export class CommandObjectivePlanner {
|
|
@@ -510,7 +503,8 @@ export class AgentObjectivePlanner {
|
|
|
510
503
|
const resolvedContext = context.executionContext ? executionContextPrompt(context.executionContext) + '\n\n' : '';
|
|
511
504
|
const prompt = renderAgentPrompt(await buildReferenceFirstPrompt({ role: 'planner', adapter: 'codex', system: (effectivePlanner ? `[Effective planner contract SHA-256: ${effectivePlanner.hash}]\n\n` : '') + instructions.trim() + '\n\n## Shared Task / Job Contract\n\n' + contract.trim(), user: resolvedContext + '## Durable planner input\n\n' + plannerInput(context) }), 'codex');
|
|
512
505
|
const runAgent = (input) => new Promise((resolve, reject) => {
|
|
513
|
-
|
|
506
|
+
// Plan apply writes unified relay.sqlite beside AGENTS_RELAY_STORE_DIR.
|
|
507
|
+
const child = spawnOwnedProcess(this.command, ['exec', '--json', '--sandbox', 'workspace-write', '--add-dir', resolvePath(dirname(relayStoreDirectory())), '--', input], { invocation: 'planner.agent', signal, correlation: { jobId: context.job.id }, spawn: { cwd: context.executionContext?.project.path, stdio: ['ignore', 'pipe', 'pipe'] } });
|
|
514
508
|
let stdoutBuffer = '', finalMessage = '', stderrTail = '';
|
|
515
509
|
const consume = (line) => {
|
|
516
510
|
const message = parsePlannerAgentMessage(line);
|
package/dist/reconciler.js
CHANGED
|
@@ -273,8 +273,8 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
273
273
|
}
|
|
274
274
|
return this.store.load(jobId);
|
|
275
275
|
}
|
|
276
|
-
async planJob(jobId, guidance,
|
|
277
|
-
return executeManualPlan(this.store, jobId, guidance, this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), (job, plan) => this.launchPlanner(job, plan),
|
|
276
|
+
async planJob(jobId, guidance, forceCleanHistory = false) {
|
|
277
|
+
return executeManualPlan(this.store, jobId, guidance, this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), (job, plan) => this.launchPlanner(job, plan), forceCleanHistory);
|
|
278
278
|
}
|
|
279
279
|
async maybeCreatePlanner(job) {
|
|
280
280
|
const pending = pendingPlan(job);
|
package/dist/relayd-control.js
CHANGED
|
@@ -109,8 +109,7 @@ export function createWorkspaceControl(deps) {
|
|
|
109
109
|
if (command === 'job.plan') {
|
|
110
110
|
const reconciler = makeReconciler(store, concurrency);
|
|
111
111
|
try {
|
|
112
|
-
const
|
|
113
|
-
const planned = await reconciler.planJob(jobId, typeof body.guidance === 'string' && body.guidance.trim() ? body.guidance : undefined, cleanHistory);
|
|
112
|
+
const planned = await reconciler.planJob(jobId, typeof body.guidance === 'string' && body.guidance.trim() ? body.guidance : undefined, body.forceCleanHistory === true);
|
|
114
113
|
await pool.reconcileLocalJob(jobId, 'control.job.plan');
|
|
115
114
|
return planned.round;
|
|
116
115
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-relay",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.160",
|
|
4
4
|
"description": "Durable async agent jobs coordinated through GitHub pull requests",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"test:publish": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 --test-force-exit test/*.test.js",
|
|
20
20
|
"test:fast": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=2 test/workflow-lifecycle.test.js test/workflow-v2-lifecycle-parity.test.js test/workflow-effects.test.js test/relay-store.test.js test/local-authority.test.js test/local-jobid-routing.test.js test/local-sqlite-jobid.test.js test/no-github.test.js test/scheduled-jobs.test.js test/reconcile-coordinator.test.js test/service-publication.test.js test/github-webhook.test.js",
|
|
21
21
|
"test:full": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 test/*.test.js && npm run test:verification:l3",
|
|
22
|
-
"test:verification:l1": "npm run build && node --test test/verification/harness.test.mjs test/verification/contract.test.mjs test/verification/graph-inventory.test.mjs test/verification/dashboard-drilldown.test.mjs test/verification/evidence-matrix.test.mjs test/verification/task-resolution-classification.test.mjs",
|
|
23
|
-
"test:verification:l3": "npm run build && node --test --test-concurrency=1 test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-cli-actions.test.mjs test/verification/l3-planner-round.test.mjs test/verification/l3-task-resolution.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs test/verification/l3-full-path.test.mjs test/verification/l3-projection-recovery.test.mjs",
|
|
22
|
+
"test:verification:l1": "npm run build && node --test test/verification/harness.test.mjs test/verification/contract.test.mjs test/verification/graph-inventory.test.mjs test/verification/dashboard-drilldown.test.mjs test/verification/evidence-matrix.test.mjs test/verification/task-resolution-classification.test.mjs test/verification/planner-contract.test.mjs",
|
|
23
|
+
"test:verification:l3": "npm run build && node --test --test-concurrency=1 test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-cli-actions.test.mjs test/verification/l3-planner-round.test.mjs test/verification/l3-planner-cli.test.mjs test/verification/l3-task-resolution.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs test/verification/l3-full-path.test.mjs test/verification/l3-projection-recovery.test.mjs",
|
|
24
24
|
"test:verification": "npm run test:verification:l1 && npm run test:verification:l2 && npm run test:verification:l3 && npm run test:verification:cross-a && node --test test/workflow-production-outbox.test.js && npm run test:verification:matrix",
|
|
25
25
|
"e2e": "node e2e/runner/run.mjs",
|
|
26
26
|
"test:verification:cross-a": "npm run build && RELAY_A_DIST=$PWD/dist node --test test/verification/action-ledger-cross-branch.test.mjs test/verification/outbox-cross-a.test.mjs",
|
|
@@ -4,7 +4,7 @@ You are the default objective planner for Agents Relay autonomous jobs.
|
|
|
4
4
|
|
|
5
5
|
Before producing tasks, read and follow `../contracts/task-job-contract.md`.
|
|
6
6
|
|
|
7
|
-
You are a planning agent. Relay invokes you with the durable Job, existing Tasks, previous planner rounds, and resolved project context. Your sole responsibility is to decompose the objective and create a valid Plan/Task Tree. Every proposal MUST be committed with the provided `agents-relay plan apply` CLI command. Write the proposed JSON to a temporary file and run that command with the exact Plan and execution IDs from `plan_execution` / `apply_cli`. If the CLI rejects it, correct the reported error and resubmit within the SAME Plan, up to three corrections. Do not edit SQLite or other durable storage directly. Do not treat a JSON/text
|
|
7
|
+
You are a planning agent. Relay invokes you with the durable Job, existing Tasks, previous planner rounds, and resolved project context. Your sole responsibility is to decompose the objective and create a valid Plan/Task Tree. Every proposal MUST be committed with the provided `agents-relay plan apply` CLI command. Write the proposed JSON to a temporary file and run that command with the exact Plan and execution IDs from `plan_execution` / `apply_cli`. If the CLI rejects it, correct the reported error and resubmit within the SAME Plan, up to three corrections. The CLI prints the persisted Plan JSON on success; that output is not your final receipt. Do not edit SQLite or other durable storage directly. Do not treat a JSON/text proposal as a plan submission and do not return the proposal as your final answer. After a successful CLI action, return exactly `PLAN_APPLIED:<plan-id>:<execution-id>` using those exact IDs. Never invent that receipt. If no live execution identity or CLI action is available, fail rather than returning a proposal.
|
|
8
8
|
|
|
9
9
|
Relay persists each planning round as a first-class Plan PR comment/marker before generated Task comments. The Plan carries its trigger/context (including unblock/change facts when applicable), assessment, and generated Task IDs. Legacy `kind=planner` Task records may still exist as execution history, but they are not the durable semantic container for the planning decision. Returned work Tasks are not automatically children of the Plan; use `parentTaskId` only when the business Task graph needs hierarchy.
|
|
10
10
|
|
|
@@ -99,6 +99,12 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
99
99
|
- **Resolution is mandatory for replacement work:** before returning any `next_tasks`, inspect every existing `FAILED` or `BLOCKED` work Task. If it lacks an active/waiting `resolution` or durable `resolutionClassification` and the proposed batch creates related/sibling work, explicitly cover it with a valid `resolution` on the new recovery Task. A superseded/invalid classification covers only planning validation; preserve the FAILED state and never infer success from the classification. For a same-Job recovery child, use `"resolution":{"path":"same_job_dependency","targetTaskId":"<existing failed/blocked task ID>","rootCause":"<confirmed cause or stated uncertainty>","solution":"<how the new task resolves the original>"}`. `targetTaskId` identifies the **existing unresolved Task**, not the new Task. Keep the recovered work semantically under the original artifact's subtree. Do not invent a resolution or silently cancel unrelated failed work.
|
|
100
100
|
- **On `retry_feedback`:** treat the feedback as Relay's authoritative validation error for the previous output in this **same Plan**. Correct the JSON proposal and all uncovered resolution targets; do not replay the same sibling Tasks unchanged. Preserve the intent and evidence already found, and return one fully corrected result. If no safe resolution can be justified, do not invent a successful outcome.
|
|
101
101
|
|
|
102
|
+
### Forced failed-history cleanup
|
|
103
|
+
|
|
104
|
+
When `force_clean_history` is true, inspect every ID in `clean_history_task_ids` against the authoritative Task records and dependency/evidence context before planning. Do not infer a result from text alone. For a failed review that is demonstrably stale, run the existing guarded `agents-relay task cancel` command. For a failed work Task with evidence that replacement work covers it, run the existing guarded `agents-relay task classify --classification superseded`; for proven invalid work, use `--classification invalid`. Supply a different durable `--target-task-id`, and the same concrete `--evidence` and `--reason` in your report. Use the normal repository, PR, and Job options from this invocation. Never change task state directly or claim an action succeeded unless the CLI command succeeds and a fresh `task show` confirms it.
|
|
105
|
+
|
|
106
|
+
If evidence is absent or execution outcome is unknown, leave the Task unresolved and do not classify it, cancel ordinary failed work, or claim success. Do not create related replacement work for an unresolved failure. After all selected guarded commands succeed, refresh authoritative Task state and submit the Plan through the standard `plan apply` command. Include exactly one `history_cleanup` entry for each `clean_history_task_ids` item: `{ "taskId", "outcome": "superseded"|"invalid"|"cancelled_review"|"unresolved", "evidence", "reason", "targetTaskId?" }`. For unresolved items, state what evidence is missing. Relay validates that reported mutations match durable SQLite state; an ordinary text response is never a Plan receipt.
|
|
107
|
+
|
|
102
108
|
- Prefer independently verifiable Tasks with stable IDs, but do not split work solely to make Tasks smaller.
|
|
103
109
|
- When a Task depends on mutable repository or product state, distinguish a frozen evidence target from the current execution head. A SHA may freeze that Task's own review/source evidence, but do not copy an upstream SHA into a dependent release, deploy, publication, or other follow-on Task unless the objective explicitly requires that same frozen target. After dependencies complete, downstream Tasks must resolve and verify the current durable Job/PR head before acting.
|
|
104
110
|
- When fresh UI, browser, store, runtime, or visual evidence is material to the Task outcome, say so explicitly in the Task input and require evidence from the real current state. Existing screenshots, prepared assets, or prior observations are not substitutes for fresh evidence unless the objective explicitly allows reuse.
|
|
@@ -117,7 +123,7 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
117
123
|
|
|
118
124
|
## Output
|
|
119
125
|
|
|
120
|
-
Return exactly one JSON object with `objective_status`, `assessment`, `next_tasks`, and optional top-level `unblocks`.
|
|
126
|
+
Return exactly one JSON object with `objective_status`, `assessment`, `next_tasks`, and optional top-level `unblocks`. In forced cleanup mode, also return required `history_cleanup` as specified above.
|
|
121
127
|
|
|
122
128
|
For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
|
|
123
129
|
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { arg, hasArg } from './cli-support.js';
|
|
3
|
-
export async function cleanHistoryOption(args) {
|
|
4
|
-
const force = hasArg([...args], '--force-clean-history');
|
|
5
|
-
const path = arg([...args], '--clean-history-file');
|
|
6
|
-
if (!force && path)
|
|
7
|
-
throw new Error('--clean-history-file requires --force-clean-history');
|
|
8
|
-
if (force && !path)
|
|
9
|
-
throw new Error('--force-clean-history requires --clean-history-file with one classification per FAILED Task');
|
|
10
|
-
if (!path)
|
|
11
|
-
return undefined;
|
|
12
|
-
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
13
|
-
if (!Array.isArray(value) || value.some(item => !item || typeof item !== 'object' || Array.isArray(item)))
|
|
14
|
-
throw new Error('--clean-history-file must contain a JSON array of classification objects');
|
|
15
|
-
return value;
|
|
16
|
-
}
|