agents-relay 2.0.158 → 2.0.159
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 +8 -16
- package/dist/reconciler.js +2 -2
- package/dist/relayd-control.js +1 -2
- package/package.json +1 -1
- package/skills/agents-relay/agents/planner.agent.md +7 -1
- 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
|
@@ -13,6 +13,8 @@ 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';
|
|
16
18
|
export function semanticTaskTree(tasks) {
|
|
17
19
|
const work = tasks.filter(task => task.kind !== 'planner');
|
|
18
20
|
const ids = new Set(work.map(task => task.id));
|
|
@@ -268,6 +270,7 @@ export function parsePlannerResult(value) {
|
|
|
268
270
|
unblockKeys.add(key);
|
|
269
271
|
return { repository, prNumber: Number(prNumber), jobId, taskId, resolutionMode: resolutionMode };
|
|
270
272
|
});
|
|
273
|
+
const history_cleanup = parsePlannerHistoryCleanup(root.history_cleanup);
|
|
271
274
|
const ids = new Set();
|
|
272
275
|
const next_tasks = root.next_tasks.map((item, index) => {
|
|
273
276
|
const task = record(item, `next_tasks[${index}]`);
|
|
@@ -320,7 +323,7 @@ export function parsePlannerResult(value) {
|
|
|
320
323
|
continuation: task.continuation, maxAttempts, timeoutMs, resolution: resolutionProposal(task.resolution, `next_tasks[${index}].resolution`)
|
|
321
324
|
};
|
|
322
325
|
});
|
|
323
|
-
return { objective_status: status, assessment: root.assessment, next_tasks, unblocks };
|
|
326
|
+
return { objective_status: status, assessment: root.assessment, next_tasks, unblocks, ...(history_cleanup ? { history_cleanup } : {}) };
|
|
324
327
|
}
|
|
325
328
|
export function validatePlannerProgress(job, result) {
|
|
326
329
|
if (result.objective_status !== 'in_progress' || result.next_tasks.length > 0 || result.unblocks.length > 0)
|
|
@@ -420,6 +423,8 @@ export function plannerInput(context) {
|
|
|
420
423
|
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
424
|
plan_execution: context.planExecution, retry_feedback: context.retryFeedback,
|
|
422
425
|
caller_prompt: context.guidance,
|
|
426
|
+
force_clean_history: context.forceCleanHistory === true,
|
|
427
|
+
clean_history_task_ids: context.cleanHistoryTaskIds ?? [],
|
|
423
428
|
previous_planner_task_id: context.previousPlannerTaskId,
|
|
424
429
|
plan_trigger: context.trigger,
|
|
425
430
|
legacy_change_cycle: context.job.changeCycle,
|
|
@@ -438,24 +443,11 @@ export function plannerInput(context) {
|
|
|
438
443
|
})),
|
|
439
444
|
project: project ? { name: project.name, path: project.path } : undefined,
|
|
440
445
|
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 ? {
|
|
446
|
+
tasks: context.tasks.map(task => ({ ...plannerTaskContext(task), planner_result: task.plannerResult ? {
|
|
450
447
|
objective_status: task.plannerResult.objective_status,
|
|
451
448
|
assessment: boundedText(task.plannerResult.assessment),
|
|
452
449
|
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
|
-
})),
|
|
450
|
+
} : undefined })),
|
|
459
451
|
});
|
|
460
452
|
}
|
|
461
453
|
export class CommandObjectivePlanner {
|
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
|
@@ -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
|
-
}
|