agents-relay 2.0.128 → 2.0.130

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 CHANGED
@@ -12,6 +12,7 @@ Commands:
12
12
  task create|update|show|list|complete|retry|unblock|cancel Manage durable tasks
13
13
  review request Request graph-owned review (optional thread override)
14
14
  plan Invoke the Planner for an existing managed Job
15
+ plan apply Submit a validated Task Tree within one active Plan
15
16
  blocker add|resolve|show Manage cross-Job blocker dependencies
16
17
  projection conflicts|resolve Inspect and repair deterministic GitHub projection conflicts
17
18
  escalation Run the canonical escalation graph and Neo escalation executor
@@ -168,6 +169,8 @@ Required: --task-id ID and the normal Job storage options.`,
168
169
  Required: --task-id ID, --reason TEXT, and the normal managed Job storage options. This does not change Task state and cannot supersede FAILED/BLOCKED/runnable work.`,
169
170
  plan: `Invoke the Planner against the current durable Job state.
170
171
 
172
+ 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.
173
+
171
174
  Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
172
175
  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.
173
176
 
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { configureLocalGitMerge } from './cli-local-merge-config.js';
3
3
  import { noGitHubMode } from './no-github.js';
4
4
  import { discoverLocalRelayStoreSnapshots, relayStoreDirectory } from './relay-store.js';
5
+ import { readFile } from 'node:fs/promises';
5
6
  import { realpathSync } from 'node:fs';
6
7
  import { once } from 'node:events';
7
8
  import { fileURLToPath } from 'node:url';
@@ -673,6 +674,32 @@ async function main() {
673
674
  console.log(helpText(command, command === 'job' || command === 'task' || command === 'context' || command === 'review' ? args[0] : undefined));
674
675
  return;
675
676
  }
677
+ if (command === 'plan' && args[0] === 'apply') {
678
+ const options = args.slice(1);
679
+ const jobId = arg(options, '--id');
680
+ const planId = arg(options, '--plan-id');
681
+ const executionId = arg(options, '--execution-id');
682
+ const path = arg(options, '--file');
683
+ if (!jobId || !planId || !executionId || !path)
684
+ throw new Error('plan apply requires --id --plan-id --execution-id --file');
685
+ const proposal = JSON.parse(path === '-' ? await new Promise((resolve, reject) => { let value = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => value += chunk); process.stdin.on('end', () => resolve(value)); process.stdin.on('error', reject); }) : await readFile(path, 'utf8'));
686
+ const storageArgs = options.filter((value, index) => value !== '--file' && options[index - 1] !== '--file');
687
+ const { store } = await storeFor(storageArgs);
688
+ await withEventBus(storageArgs, async (bus) => {
689
+ const reconciler = runtime(store, storageArgs, bus);
690
+ try {
691
+ console.log(JSON.stringify(await reconciler.applyPlannerTree(jobId, planId, executionId, proposal), null, 2));
692
+ }
693
+ catch (error) {
694
+ console.error(JSON.stringify({ ok: false, code: 'PLAN_APPLY_REJECTED', message: error instanceof Error ? error.message : String(error) }));
695
+ process.exitCode = 1;
696
+ }
697
+ finally {
698
+ await reconciler.close();
699
+ }
700
+ });
701
+ return;
702
+ }
676
703
  if (command === 'job') {
677
704
  const [action, ...jobArgs] = args;
678
705
  await runJobCommand(action ?? '', jobArgs);
@@ -21,9 +21,11 @@ export function activateCommittedPlannerWork(job) {
21
21
  export async function drainPlannerUnblocks(ports, jobId, planId) {
22
22
  const job = await ports.store.load(jobId);
23
23
  const plan = (job.plans ?? []).find(item => item.id === planId);
24
+ const pending = [...(plan?.pendingUnblocks ?? [])];
25
+ if (!pending.length)
26
+ return [];
24
27
  if (!ports.store.registerBlockerDependent)
25
28
  throw new Error("Blocker registration unavailable");
26
- const pending = [...(plan?.pendingUnblocks ?? [])];
27
29
  // Persist only acknowledgments observed by this invocation. Subtract from
28
30
  // the latest transactional snapshot, never replace another drain's intents.
29
31
  const acknowledged = new Set();
@@ -61,7 +63,7 @@ export async function retryPendingPlannerUnblocks(ports, jobId) {
61
63
  await drainPlannerUnblocks(ports, jobId, plan.id);
62
64
  }
63
65
  }
64
- async function finishPlanner(ports, jobId, planId, executionId, error, raw) {
66
+ export async function finishPlanner(ports, jobId, planId, executionId, error, raw, cliApply = false) {
65
67
  const live = ports.live.get(planId);
66
68
  if (live?.executionId === executionId)
67
69
  clearTimeout(live.timer);
@@ -71,6 +73,8 @@ async function finishPlanner(ports, jobId, planId, executionId, error, raw) {
71
73
  const plan = (job.plans ?? []).find(item => item.id === planId);
72
74
  if (!plan || plan.status !== "PLANNING" || plan.executionId !== executionId) {
73
75
  releaseLive();
76
+ if (cliApply)
77
+ throw new Error(`Plan ${planId} is not owned by this active execution`);
74
78
  return;
75
79
  }
76
80
  const failOrRetry = async (message) => {
@@ -115,6 +119,8 @@ async function finishPlanner(ports, jobId, planId, executionId, error, raw) {
115
119
  ports.detach(ports.reconcile(jobId), jobId, planId, 'planner retry reconciliation');
116
120
  };
117
121
  if (error) {
122
+ if (cliApply)
123
+ throw error;
118
124
  await failOrRetry(error.message);
119
125
  releaseLive();
120
126
  return;
@@ -240,6 +246,8 @@ async function finishPlanner(ports, jobId, planId, executionId, error, raw) {
240
246
  releaseLive();
241
247
  throw caught;
242
248
  }
249
+ if (cliApply)
250
+ throw caught;
243
251
  await failOrRetry(caught instanceof Error ? caught.message : String(caught));
244
252
  releaseLive();
245
253
  return;
@@ -318,7 +326,7 @@ export async function launchPlanExecution(ports, job, plan) {
318
326
  let promise;
319
327
  try {
320
328
  const executionContext = await ports.plannerContext(job, plan);
321
- const context = { job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: plan.previousPlanId, trigger: plan.trigger, executionContext, retryFeedback: plan.retryFeedback, guidance: plan.guidance };
329
+ 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 };
322
330
  promise = planner.plan(context, controller.signal);
323
331
  }
324
332
  catch (error) {
package/dist/planner.js CHANGED
@@ -415,7 +415,7 @@ export function plannerInput(context) {
415
415
  const project = context.executionContext?.project;
416
416
  return JSON.stringify({
417
417
  objective: context.objective,
418
- retry_feedback: context.retryFeedback,
418
+ plan_execution: context.planExecution, retry_feedback: context.retryFeedback,
419
419
  caller_prompt: context.guidance,
420
420
  previous_planner_task_id: context.previousPlannerTaskId,
421
421
  plan_trigger: context.trigger,
@@ -529,15 +529,23 @@ export class Reconciler extends ReconcilerReviewBase {
529
529
  ? this.failLaunch(job.id, task.id, durableExecutionId, error.message, 'delivery')
530
530
  : this.runtimeSettled(job.id, task.id, durableExecutionId, null, error instanceof Error ? error.message : String(error))), job.id, task.id, 'worker runtime completion');
531
531
  }
532
+ async applyPlannerTree(jobId, planId, executionId, proposal) {
533
+ const { finishPlanner } = await import('./planner-runtime.js');
534
+ const { parsePlannerResult } = await import('./planner.js');
535
+ const ports = { store: this.store, options: this.options, live: this.plannerLive, plannerContext: (job, plan) => this.plannerContext(job, plan), emit: (event) => this.emit(event), detach: (promise, id, task, label) => this.detach(promise, id, task, label), reconcile: (id) => this.reconcile(id) };
536
+ await finishPlanner(ports, jobId, planId, executionId, null, parsePlannerResult(proposal), true);
537
+ const updated = (await this.store.load(jobId)).plans?.find(plan => plan.id === planId);
538
+ if (!updated || updated.status !== 'SUCCEEDED')
539
+ throw new Error(`Plan ${planId} was not committed`);
540
+ return updated;
541
+ }
532
542
  async launchPlanner(job, plan) {
533
543
  return launchPlanExecution({
534
544
  store: this.store,
535
545
  options: this.options,
536
546
  live: this.plannerLive,
537
- plannerContext: (currentJob, currentPlan) => this.plannerContext(currentJob, currentPlan),
538
- emit: event => this.emit(event),
539
- detach: (promise, jobId, planId, context) => this.detach(promise, jobId, planId, context),
540
- reconcile: jobId => this.reconcile(jobId),
547
+ plannerContext: (currentJob, currentPlan) => this.plannerContext(currentJob, currentPlan), emit: event => this.emit(event),
548
+ detach: (promise, jobId, planId, context) => this.detach(promise, jobId, planId, context), reconcile: jobId => this.reconcile(jobId),
541
549
  }, job, plan);
542
550
  }
543
551
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.128",
3
+ "version": "2.0.130",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 pure planning agent. Relay invokes you with the durable Job, existing Tasks, previous planner rounds, and resolved project context. You return one structured planning result. You do not call the Agents Relay CLI, persist state, create Jobs, or mutate Tasks yourself.
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. When the invocation provides a live Plan ID and execution ID, write the proposed JSON to a temporary file and submit it with `agents-relay plan apply --repo OWNER/REPO --pr PR_NUMBER --id JOB_ID --plan-id PLAN_ID --execution-id EXECUTION_ID --file PATH`. If the CLI rejects it, correct the reported error and re-submit within the SAME Plan, up to three corrections. Do not create new Jobs, run Tasks, change statuses, or perform scheduling/recovery. On successful apply, return the same JSON as your final answer for compatibility; Relay detects the committed round and does not create duplicate Tasks. If no live execution identity is provided, return JSON and let Relay materialize it as before.
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
 
@@ -19,6 +19,15 @@ For every non-satisfied objective:
19
19
  3. Mark the points in that graph that deserve durable Task boundaries.
20
20
  4. Create only the next useful executable Task batch from those boundaries.
21
21
 
22
+ ### Objective decomposition and convergence
23
+
24
+ - **Never bundle independently testable acceptance layers into one giant Task.** Break objectives into separately verifiable deliverables (for example, contract tests, real-provider compliance, scheduler-driven end-to-end scenarios, and integration evidence). Give each Task concrete acceptance checks and an observable output.
25
+ - Split a large Task when its remaining work spans distinct implementation areas, independently testable failure paths, or more than one meaningful review/recovery cycle. Put shared prerequisites in earlier Tasks and express true dependencies explicitly; independent work may run in parallel.
26
+ - For a failed Task with repeated deterministic acceptance gaps, do not reissue the identical broad assignment. Identify the *missing acceptance cases*, create focused same-Job repair Tasks with valid `same_job_dependency` resolutions, and specify how their evidence unblocks the original Task. Preserve the original failed Task and failure history.
27
+ - For event-driven workflows, separate basic ingress/persistence checks from real scheduler-driven multi-stage scenarios. Require negative-path, multi-failure recovery and no-false-completion evidence before claiming full objective coverage.
28
+ - In `assessment`, show the entire remaining objective as a dependency graph, clearly distinguishing already proven work, the executable batch, subsequent verification, and Relay-owned review/merge. Never report overall `satisfied` merely because one batch has passed.
29
+
30
+
22
31
  A Task is **not** just a workflow step. Create a Task when its outcome deserves an independent durable lifecycle because it may need to be:
23
32
 
24
33
  - discussed or referenced later;