atris 3.74.27 → 3.74.28

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.
@@ -39,7 +39,10 @@ function readHere(deps = {}) {
39
39
  }
40
40
  if (!played || played.error || played.no_history) return null;
41
41
  const { rosterRows } = require('../lib/gm-season');
42
- const rows = rosterRows({ outcomes: played.outcomes || [], limit: Number.MAX_SAFE_INTEGER }).filter((row) => row.finished > 0);
42
+ // A member whose only misses were later confirmed not needed, or whose only jobs
43
+ // wait on a pull request, has no finished job, but still shows, so those jobs
44
+ // have somewhere to be counted.
45
+ const rows = rosterRows({ outcomes: played.outcomes || [], limit: Number.MAX_SAFE_INTEGER }).filter((row) => row.finished > 0 || row.not_needed > 0 || row.held > 0);
43
46
  if (!rows.length) return null;
44
47
  let lineup = deps.lineup;
45
48
  if (lineup === undefined) lineup = require('../lib/team-lineup').readLineupSafe(root);
@@ -54,7 +57,7 @@ function readHere(deps = {}) {
54
57
  }
55
58
  const miss_reasons = [...reasons].map(([reason, count]) => ({ reason, count }))
56
59
  .sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason)).slice(0, 3);
57
- return { member: row.name, ovr: row.ovr, landed: row.landed, finished: row.finished, job: seat.job || null, model: seat.model || null, ...(miss_reasons.length ? { miss_reasons } : {}) };
60
+ return { member: row.name, ovr: row.ovr, landed: row.landed, finished: row.finished, ...(row.not_needed ? { not_needed: row.not_needed } : {}), ...(row.held ? { held: row.held } : {}), job: seat.job || null, model: seat.model || null, ...(miss_reasons.length ? { miss_reasons } : {}) };
58
61
  });
59
62
  const buildJob = ((lineup && lineup.ok && lineup.jobs) || []).find((row) => row.job === 'build' && !row.key);
60
63
  const buildModel = buildJob && buildJob.now_runs ? buildJob.now_runs.model : null;
@@ -111,6 +114,8 @@ function renderMember(m, card) {
111
114
  '',
112
115
  ` landed ${m.landed} of ${m.finished} finished jobs in the last 30 days`,
113
116
  ];
117
+ if (m.not_needed) lines.push(` not needed: ${m.not_needed} (confirmed by someone else, so ${m.not_needed === 1 ? 'it does' : 'they do'} not count either way)`);
118
+ if (m.held) lines.push(` waiting on a pull request: ${m.held} (the work is done and a person has to land it, so ${m.held === 1 ? 'it does' : 'they do'} not count either way)`);
114
119
  if (m.ovr == null) lines.push(' rated once it has 3 finished jobs');
115
120
  if (m.miss_reasons && m.miss_reasons.length) {
116
121
  lines.push(' top miss reasons:');
package/commands/task.js CHANGED
@@ -27,6 +27,7 @@ const {
27
27
  evaluateAutoAccept,
28
28
  isAgentCertified,
29
29
  isAutoCertifyVerifyCommandAllowed,
30
+ openPullRequestRefusal,
30
31
  parseVerifyChain,
31
32
  parseVerifyCommand,
32
33
  runVerifyCommand,
@@ -226,7 +227,7 @@ atris task - durable local task state (SQLite, gitignored)
226
227
  atris task list [--all] [--everywhere] [--status <s>]
227
228
  list tasks in this workspace; --everywhere spans workspaces
228
229
  atris task claim <id> [--as <owner>] Atomic claim
229
- atris task release <id> [--as <owner>] Release your own mistaken claim back to open
230
+ atris task release <id> [--as <owner>] [--reason "<why>"] Release your own mistaken claim back to open
230
231
  atris task capabilities [--json] Read-only task CLI/API capability contract
231
232
  atris task capabilities-check [--json] Verify task capability contract conformance
232
233
  atris task review-lane-drain [--json] Pick next safe Review-lane agent action
@@ -252,9 +253,12 @@ atris task - durable local task state (SQLite, gitignored)
252
253
  atris task step <id> [--json] Refine chat, then advance one safe Plan/Do/Review step
253
254
  atris task done <id> --proof "..." Mark complete with proof
254
255
  atris task done <id> --failed [--proof "..."] Mark failed, optionally reviewed
255
- atris task archive <id> --reason "..." [--from-failed]
256
+ atris task archive <id> --reason "..." [--from-failed] [--not-needed]
256
257
  Sweep off-roadmap/duplicate work as archived (not failed);
257
258
  --from-failed opts in to relabel a fail-closed row (never done)
259
+ --not-needed: a person confirms the job did not need doing, so a
260
+ drop on it stops counting against the member (needs --reason;
261
+ refused inside an agent session or a dispatched worker)
258
262
  atris task reopen <id> [--reason "..."] Send a done, waiting, failed, or archived task back to open
259
263
  atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
260
264
  atris task prune-test-junk [--yes] [--vacuum] [--json]
@@ -366,6 +370,28 @@ function agentProofOnlyMode() {
366
370
  return AGENT_ENV_MARKERS.some((marker) => String(process.env[marker] || '').trim() !== '');
367
371
  }
368
372
 
373
+ // Everything an engine process carries: what the engine tools export in the
374
+ // shells they run, and what a mission flight sets on the drivers and engines it
375
+ // starts. ATRIS_AGENT_ID is left out on purpose: it is the owner's own id, which
376
+ // a person may export. The fleet's own dispatch adds no marker of its own, so
377
+ // it relies on the engine tool's.
378
+ const WORKER_ENV_MARKERS = [
379
+ ...AGENT_ENV_MARKERS,
380
+ 'CODEX_THREAD_ID',
381
+ 'CODEX_SESSION_ID',
382
+ 'ATRIS_MISSION_DRIVER_DETACHED',
383
+ 'ATRIS_MISSION_DRIVER_PARENT_PID',
384
+ 'ATRIS_DISPATCH_DEPTH',
385
+ ];
386
+
387
+ // The first worker marker set in this shell, or ''. Unlike agentProofOnlyMode
388
+ // it cannot be switched off with ATRIS_AGENT_PROOF_ONLY=0.
389
+ function workerEnvMarker(env = process.env) {
390
+ const found = WORKER_ENV_MARKERS.find((marker) => String(env[marker] || '').trim() !== '');
391
+ if (found) return found;
392
+ return String(env.ATRIS_AGENT_PROOF_ONLY || '').trim() === '1' ? 'ATRIS_AGENT_PROOF_ONLY' : '';
393
+ }
394
+
369
395
  function failAgentProofOnly(label, detail) {
370
396
  failTask(
371
397
  label,
@@ -575,15 +601,18 @@ function landingFlags(args) {
575
601
  };
576
602
  }
577
603
 
578
- // The pull request and commit a flight merged on its own, so the job record
579
- // names what landed. Only a merge commit makes it count.
604
+ // The pull request a flight opened or merged, so the job record names it.
605
+ // Only a merge commit makes it count as landed. A number or link alone is an
606
+ // open pull request still waiting for a person to land it.
580
607
  function readyMergeFlags(args) {
581
608
  const commit = textFlag(args, ['--merge-commit']);
582
- if (!commit) return null;
609
+ const number = textFlag(args, ['--pr-number']);
610
+ const url = textFlag(args, ['--pr-url']);
611
+ if (!commit && !number && !url) return null;
583
612
  return {
584
613
  merge_commit: commit,
585
- number: textFlag(args, ['--pr-number']),
586
- url: textFlag(args, ['--pr-url']),
614
+ number,
615
+ url,
587
616
  merged_at: textFlag(args, ['--merged-at']),
588
617
  self_landed: hasFlag(args, '--self-landed'),
589
618
  };
@@ -751,13 +780,18 @@ function positional(args) {
751
780
  });
752
781
  }
753
782
 
783
+ // options.root is for a caller that is not the command line, the dispatch flight: it
784
+ // keeps the list for its own workspace instead of for the directory this process is in.
754
785
  function writeDefaultProjection(taskDb, db, options = {}) {
755
- const workspaceRoot = scopedWorkspaceRoot(taskDb, [], options);
786
+ const root = options.root ? path.resolve(options.root) : null;
787
+ const workspaceRoot = root
788
+ ? (taskScopeEverywhere([], options) ? null : taskDb.workspaceRoot(root))
789
+ : scopedWorkspaceRoot(taskDb, [], options);
756
790
  const projection = enrichTaskProjection(taskDb.taskProjection(db, {
757
791
  workspaceRoot,
758
792
  limit: options.all ? null : 500,
759
793
  }));
760
- const outPath = path.resolve(path.join('.atris', 'state', 'tasks.projection.json'));
794
+ const outPath = path.resolve(root || process.cwd(), '.atris', 'state', 'tasks.projection.json');
761
795
  const output = JSON.stringify(projection, null, 2) + '\n';
762
796
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
763
797
  let shouldWrite = true;
@@ -6327,26 +6361,39 @@ function cmdRelease(args) {
6327
6361
  failTask('atris task release', 'missing_id', 'id required');
6328
6362
  }
6329
6363
  const owner = flag(args, '--as') || DEFAULT_OWNER;
6364
+ const reasonFlag = flag(args, '--reason');
6365
+ // This command takes no kind and no pull request. A release that says its work is
6366
+ // waiting on a person would keep a miss out of the rating, and any worker holding a
6367
+ // failed job could type that to excuse itself. Only the dispatch flight, which
6368
+ // opened the pull request itself, records that release, in its own process through
6369
+ // releaseTask. Flags this command does not know are ignored, so a release that still
6370
+ // passes them is a plain release and is rated as a drop.
6330
6371
  const taskDb = getTaskDb();
6331
6372
  const db = taskDb.open();
6332
6373
  const taskId = requireTaskId(taskDb, db, id, 'atris task release');
6333
- const result = taskDb.releaseTask(db, { id: taskId, actor: String(owner) });
6374
+ const result = taskDb.releaseTask(db, {
6375
+ id: taskId,
6376
+ actor: String(owner),
6377
+ reason: typeof reasonFlag === 'string' ? reasonFlag : undefined,
6378
+ });
6334
6379
  if (result.released) {
6335
6380
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
6336
6381
  const task = compactTaskFromProjection(projection, taskId);
6382
+ const keptReason = result.event && result.event.payload && result.event.payload.reason || null;
6337
6383
  if (wantsJson(args)) {
6338
6384
  printJson({
6339
6385
  ok: true,
6340
6386
  action: 'released',
6341
6387
  task_id: taskId,
6342
6388
  owner: String(owner),
6389
+ reason: keptReason,
6343
6390
  projection_path: outPath,
6344
6391
  task,
6345
6392
  event_version: result.event && result.event.version || null,
6346
6393
  });
6347
6394
  return;
6348
6395
  }
6349
- console.log(`released ${taskRef(task)} from ${owner}`);
6396
+ console.log(`released ${taskRef(task)} from ${owner}${keptReason ? ` (${keptReason})` : ''}`);
6350
6397
  console.log(`Next: atris task next --as ${owner}`);
6351
6398
  return;
6352
6399
  }
@@ -9251,9 +9298,27 @@ function cmdArchive(args) {
9251
9298
  if (!id) {
9252
9299
  failTask('atris task archive', 'missing_id', 'id required');
9253
9300
  }
9301
+ const notNeeded = hasFlag(args, '--not-needed');
9302
+ // The note clears a drop off the member's record, and --as is only a name. A
9303
+ // worker that dropped the job could name someone else and clear its own miss,
9304
+ // so inside an agent session or a dispatched worker it is refused outright.
9305
+ const workerMarker = notNeeded ? workerEnvMarker() : '';
9306
+ if (workerMarker) {
9307
+ failTask(
9308
+ 'atris task archive',
9309
+ 'not_needed_needs_a_person',
9310
+ `atris task archive --not-needed is for a person to confirm, and this shell looks like an agent session or a dispatched worker (${workerMarker} is set). Run it from a plain terminal.`,
9311
+ );
9312
+ }
9254
9313
  const reason = textFlag(args, ['--reason']);
9255
9314
  if (!reason) {
9256
- failTask('atris task archive', 'missing_reason', 'atris task archive requires --reason "<why this is being swept, not failed>"');
9315
+ failTask(
9316
+ 'atris task archive',
9317
+ 'missing_reason',
9318
+ notNeeded
9319
+ ? 'atris task archive --not-needed requires --reason "<why this job did not need doing>"'
9320
+ : 'atris task archive requires --reason "<why this is being swept, not failed>"',
9321
+ );
9257
9322
  }
9258
9323
  const taskDb = getTaskDb();
9259
9324
  const db = taskDb.open();
@@ -9263,8 +9328,13 @@ function cmdArchive(args) {
9263
9328
  // loop-tick orphans fail-closed before 'archived' existed). Without the
9264
9329
  // flag, failed rows stay failed; done rows are never archivable.
9265
9330
  const fromFailed = hasFlag(args, '--from-failed');
9266
- const result = taskDb.archiveTask(db, { id: taskId, actor, reason, fromFailed });
9267
- if (result.archived) {
9331
+ // --not-needed also writes who said so, which clears a dropped job off the
9332
+ // member's record when it is someone other than the member. A job that is
9333
+ // already archived only gets that note.
9334
+ const result = notNeeded
9335
+ ? taskDb.archiveNotNeeded(db, { id: taskId, actor, reason, fromFailed })
9336
+ : taskDb.archiveTask(db, { id: taskId, actor, reason, fromFailed });
9337
+ if (result.archived || result.recorded) {
9268
9338
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
9269
9339
  if (wantsJson(args)) {
9270
9340
  printJson({
@@ -9272,6 +9342,7 @@ function cmdArchive(args) {
9272
9342
  action: 'archived',
9273
9343
  task_id: taskId,
9274
9344
  reason,
9345
+ ...(notNeeded ? { not_needed: true, not_needed_by: actor, already_archived: !result.archived } : {}),
9275
9346
  archived_from: result.row && result.row.metadata && result.row.metadata.archived_from || null,
9276
9347
  projection_path: outPath,
9277
9348
  task: compactTaskFromProjection(projection, taskId),
@@ -9281,7 +9352,12 @@ function cmdArchive(args) {
9281
9352
  const fromNote = result.row && result.row.metadata && result.row.metadata.archived_from
9282
9353
  ? ` (was ${result.row.metadata.archived_from})`
9283
9354
  : '';
9284
- console.log(`archived ${taskRef(compactTaskFromProjection(projection, taskId))}${fromNote}: ${reason}`);
9355
+ const ref = taskRef(compactTaskFromProjection(projection, taskId));
9356
+ if (notNeeded && !result.archived) {
9357
+ console.log(`${ref} was already archived; recorded as not needed by ${actor}: ${reason}`);
9358
+ } else {
9359
+ console.log(`archived ${ref}${fromNote}${notNeeded ? ' as not needed' : ''}: ${reason}`);
9360
+ }
9285
9361
  } else {
9286
9362
  const hint = result.reason === 'already_failed'
9287
9363
  ? ' (use --from-failed to archive a fail-closed duplicate/off-roadmap row)'
@@ -10052,6 +10128,13 @@ function stampAutoAcceptMetadata(taskDb, db, task, actor, policy) {
10052
10128
 
10053
10129
  function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '', autoAccepted = false }) {
10054
10130
  const task = taskDetail(taskDb, db, taskId);
10131
+ // The last door for the machine: whatever decided this job was eligible, a job
10132
+ // whose pull request is still open is never completed without a person. A
10133
+ // person's own accept (autoAccepted false) is untouched.
10134
+ if (autoAccepted) {
10135
+ const pullRequestOpen = openPullRequestRefusal(task, taskId);
10136
+ if (pullRequestOpen) return { ok: false, reason: pullRequestOpen.reason, pull_request: pullRequestOpen.pull_request };
10137
+ }
10055
10138
  const missionXpIssue = missionXpEndToEndProofIssue(task, proof, taskDb.workspaceRoot());
10056
10139
  if (missionXpIssue) {
10057
10140
  return { ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue };
@@ -10735,6 +10818,10 @@ function evaluateSweepAutoAccept(task, root, includeProtected = false) {
10735
10818
  if (metadata.auto_accepted_at) return { eligible: false, ref, reason: 'already_auto_accepted' };
10736
10819
  const denied = autoAcceptSweepDeniedReason(task, includeProtected);
10737
10820
  if (denied) return { eligible: false, ref, reason: denied };
10821
+ // A job whose pull request is still open has not landed, whatever its stored check
10822
+ // says: the same refusal as auto-accept-certified, before any check is run.
10823
+ const pullRequestOpen = openPullRequestRefusal(task, ref);
10824
+ if (pullRequestOpen) return pullRequestOpen;
10738
10825
  const proof = autoAcceptSweepLatestProof(task);
10739
10826
  if (!proof) return { eligible: false, ref, reason: 'no_proof' };
10740
10827
  const storedVerify = typeof metadata.verify === 'string' ? metadata.verify.trim() : '';
@@ -13343,4 +13430,7 @@ module.exports = {
13343
13430
  certifyVerifyCandidate,
13344
13431
  landingProofVerify,
13345
13432
  stampAutoAcceptMetadata,
13433
+ acceptReviewTask,
13434
+ evaluateSweepAutoAccept,
13435
+ writeDefaultProjection,
13346
13436
  };