atris 3.74.28 → 3.74.29

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.
Files changed (2) hide show
  1. package/lib/fleet.js +39 -29
  2. package/package.json +1 -1
package/lib/fleet.js CHANGED
@@ -53,18 +53,27 @@ const {
53
53
  } = require('./secret-gateway');
54
54
  const { appendEngineLiveLogChunk, createEngineLiveLog, engineTerminalReason } = require('./engine-job-lifecycle');
55
55
  const { recordEngineResource } = require('./engine-resource');
56
+ const {
57
+ taskTagTokens,
58
+ isDecisionHoldTag,
59
+ isDecisionTask,
60
+ DECISION_REFUSE_REASON,
61
+ } = require('./task-decision');
56
62
 
57
63
  // Lanes a fleet may never staff on its own: the human keeps irreversible
58
64
  // calls. Mirrors the autoland denied lanes.
59
65
  const DENIED_TAGS = ['billing', 'money', 'payments', 'deploy', 'security', 'customer', 'external', 'feedback'];
60
66
 
61
- // The protected lane a task sits in (a denied tag on the task or its todo
62
- // tags), or null. The ship gate and the dispatch flight both ask this, so they
63
- // cannot disagree about which work a person must land.
67
+ // The protected lane a task sits in, or null. This is the one place that
68
+ // decides protection: staffing (isSafeLane), the ship gate, the dispatch flight
69
+ // and the builder prompt all ask it, so a task one of them holds back as
70
+ // protected is never one that another would merge. A task's tags are its tag,
71
+ // its tags list, the tags `atris task tag` adds later and hashtags in its
72
+ // title (all through taskTagTokens), plus the todo tags it was imported with.
64
73
  function protectedLaneOfTask(task) {
65
74
  if (!task || typeof task !== 'object') return null;
66
75
  const todoTags = task.metadata && Array.isArray(task.metadata.todo_tags) ? task.metadata.todo_tags : [];
67
- const tags = [task.tag, ...todoTags].filter(Boolean).map((tag) => String(tag).toLowerCase());
76
+ const tags = [...taskTagTokens(task), ...todoTags.filter(Boolean).map((tag) => String(tag).trim().toLowerCase())];
68
77
  return tags.find((tag) => DENIED_TAGS.includes(tag)) || null;
69
78
  }
70
79
 
@@ -126,7 +135,8 @@ function shellQuote(value) {
126
135
 
127
136
  // The bounded prompt every engine gets: isolated worktree, MAP first, the
128
137
  // task's own check, one landing order per mode. Yolo: the engine lands it
129
- // itself with worktree ship. Otherwise: commit only, the conductor lands it.
138
+ // itself with worktree ship, unless the task is in a protected lane. Otherwise:
139
+ // commit only, the conductor lands it.
130
140
  function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
131
141
  const ref = task.display_id || task.id || 'TASK';
132
142
  const title = String(task.title || '').trim();
@@ -134,11 +144,27 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
134
144
  const done = titleDone || String((task.metadata && task.metadata.done_looks_like) || '').trim();
135
145
  const check = taskDispatchCheck(task, worktreePath || process.cwd());
136
146
  const shipMessage = title.split(/[.:](?:\s|$)/)[0].replace(/"/g, "'").slice(0, 90) || ref;
137
- const commitRule = yolo
138
- ? `Land it yourself. Commit on the current branch with a plain-English message, then run: atris worktree ship --message "${shipMessage} (${ref})" --verify ${shellQuote(check)} --merge, and report the PR URL. That ship command is the only push allowed. If it reports a conflict or a failed check, stop and report; never resolve conflicts yourself.`
139
- : 'Commit on the current branch with a clear message. Do not push and do not create branches; the conductor lands your commit.';
147
+ // A protected-lane task is never shipped by its builder, yolo or not. The
148
+ // builder commits and stops, and the dispatch flight ships that commit once,
149
+ // without --merge, so a pull request opens and a person lands it. A builder
150
+ // that shipped too would open the pull request before the flight's own
151
+ // check, and if the flight's check or ship then failed, the claim would be
152
+ // released with that pull request open and unrecorded. Yolo still gives the
153
+ // builder its network for running checks.
154
+ const lane = yolo ? protectedLaneOfTask(task) : null;
155
+ let commitRule;
156
+ if (lane) {
157
+ commitRule = `This task is in the protected ${lane} lane, so a person lands it, not you. Commit on the current branch with a plain-English message. Do not push, do not run \`atris worktree ship\`, and do not create branches; the conductor ships your commit for a person to review.`;
158
+ } else if (yolo) {
159
+ commitRule = `Land it yourself. Commit on the current branch with a plain-English message, then run: atris worktree ship --message "${shipMessage} (${ref})" --verify ${shellQuote(check)} --merge, and report the PR URL. That ship command is the only push allowed. If it reports a conflict or a failed check, stop and report; never resolve conflicts yourself.`;
160
+ } else {
161
+ commitRule = 'Commit on the current branch with a clear message. Do not push and do not create branches; the conductor lands your commit.';
162
+ }
163
+ // The guard refuses a checkout that has edits in it, so it can only pass at
164
+ // the start. A builder that re-ran it in its review step, after its edits,
165
+ // took the failure as a stop order and left finished work uncommitted.
140
166
  const lines = [
141
- 'First, run `atris worktree guard`; if it fails, stop immediately, report back, and do not edit anything. Do this before any file edit.',
167
+ 'Before your first edit, run `atris worktree guard` once; if it fails then, stop immediately, report back, and do not edit anything. It checks for a clean start, so it fails once edits exist: never run it again after editing.',
142
168
  '',
143
169
  ATRIS_BUILD_PROCESS_PREAMBLE,
144
170
  '',
@@ -162,7 +188,7 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
162
188
  `- ${commitRule}`,
163
189
  ...METHOD_KERNEL.map((rule) => `- ${rule}`),
164
190
  '',
165
- `Final report (plain text): files changed, check command + result, commit sha${yolo ? ', PR URL' : ''} (or say what failed and why).`
191
+ `Final report (plain text): files changed, check command + result, commit sha${yolo && !lane ? ', PR URL' : ''} (or say what failed and why).`
166
192
  );
167
193
  const lessonFiles = [
168
194
  ...(Array.isArray(task.files) ? task.files : []),
@@ -1358,20 +1384,6 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
1358
1384
  // ---------------------------------------------------------------------------
1359
1385
  // T2: staffing
1360
1386
 
1361
- const {
1362
- taskTagTokens,
1363
- isDecisionHoldTag,
1364
- isDecisionTask,
1365
- DECISION_REFUSE_REASON,
1366
- } = require('./task-decision');
1367
-
1368
- function taskTags(task) {
1369
- // Tags added after creation live in metadata.tags (`atris task tag`); a
1370
- // fleet that only read task.tags/title hashtags would ignore an owner-hold
1371
- // flag stamped on a live task and keep restaffing it (CLI-879).
1372
- return taskTagTokens(task);
1373
- }
1374
-
1375
1387
  // A task flagged for a human decision is never fleet-staffable, whatever its
1376
1388
  // lane. Mirrors the sweep's needs-human hold so both loops agree. Also honors
1377
1389
  // the clearer `decision` tag so policy questions stay off autonomous lanes.
@@ -1379,10 +1391,10 @@ function isHumanHoldTag(tag) {
1379
1391
  return isDecisionHoldTag(tag);
1380
1392
  }
1381
1393
 
1394
+ // The fleet may staff a task only when no person has to decide it and it sits
1395
+ // in no protected lane. Protection is protectedLaneOfTask's call alone.
1382
1396
  function isSafeLane(task) {
1383
- if (isDecisionTask(task)) return false;
1384
- const tags = taskTags(task);
1385
- return !tags.some((t) => DENIED_TAGS.includes(t));
1397
+ return !isDecisionTask(task) && !protectedLaneOfTask(task);
1386
1398
  }
1387
1399
 
1388
1400
  // Rough file-surface guess from a task's text: paths and bare `commands/x.js`
@@ -1713,12 +1725,10 @@ module.exports = {
1713
1725
  assertIsolatedWorktree,
1714
1726
  buildEngineCommand,
1715
1727
  isSafeLane,
1716
- taskTags,
1717
1728
  nextInstalledFleetEngine,
1718
1729
  rankFleetEnginesDetailed,
1719
1730
  dispatchToEngine,
1720
1731
  recordDispatchOutcome,
1721
- taskTags,
1722
1732
  isHumanHoldTag,
1723
1733
  isDecisionTask,
1724
1734
  DECISION_REFUSE_REASON,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.74.28",
3
+ "version": "3.74.29",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "mcpName": "io.github.atrislabs/atris",
6
6
  "main": "bin/atris.js",