atris 3.34.0 → 3.36.0

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 (163) hide show
  1. package/AGENTS.md +35 -0
  2. package/FOR_AGENTS.md +5 -3
  3. package/README.md +5 -3
  4. package/atris/GETTING_STARTED.md +1 -1
  5. package/atris/atris.md +3 -0
  6. package/atris/policies/day-loop-voice.md +102 -0
  7. package/atris/policies/outbound-artifact-gate.md +2 -0
  8. package/atris/skills/design/SKILL.md +56 -32
  9. package/atris/skills/endgame/SKILL.md +12 -6
  10. package/atris/skills/engines/SKILL.md +35 -8
  11. package/atris/skills/fable-method/SKILL.md +66 -0
  12. package/atris/skills/improve/SKILL.md +65 -45
  13. package/atris/skills/render-cli/SKILL.md +88 -0
  14. package/atris/skills/youtube/SKILL.md +10 -1
  15. package/atris.md +6 -2
  16. package/ax +617 -31
  17. package/bin/atris.js +719 -266
  18. package/commands/activate.js +194 -88
  19. package/commands/aeo.js +52 -0
  20. package/commands/agents.js +166 -0
  21. package/commands/autoland.js +718 -72
  22. package/commands/autopilot-front.js +20 -2
  23. package/commands/autopilot.js +118 -2
  24. package/commands/avail.js +407 -0
  25. package/commands/bench.js +188 -0
  26. package/commands/brain.js +3 -0
  27. package/commands/brief.js +651 -0
  28. package/commands/business-sync.js +192 -6
  29. package/commands/business.js +91 -8
  30. package/commands/clean.js +50 -24
  31. package/commands/close.js +1083 -0
  32. package/commands/cloud.js +245 -0
  33. package/commands/codex-goal.js +26 -2
  34. package/commands/compile.js +292 -1
  35. package/commands/computer.js +150 -3
  36. package/commands/dream.js +365 -0
  37. package/commands/drill.js +371 -0
  38. package/commands/drive.js +187 -0
  39. package/commands/engine.js +1061 -29
  40. package/commands/experiments.js +28 -0
  41. package/commands/feed.js +202 -0
  42. package/commands/feedback.js +34 -12
  43. package/commands/fleet-report.js +206 -0
  44. package/commands/github.js +38 -0
  45. package/commands/gm.js +285 -3
  46. package/commands/goal.js +247 -0
  47. package/commands/improve.js +642 -26
  48. package/commands/init.js +83 -43
  49. package/commands/integrations.js +39 -11
  50. package/commands/interview.js +209 -0
  51. package/commands/land.js +253 -52
  52. package/commands/lesson.js +112 -1
  53. package/commands/lifecycle.js +39 -3
  54. package/commands/linear.js +38 -0
  55. package/commands/log.js +84 -1
  56. package/commands/loops.js +220 -16
  57. package/commands/meet.js +220 -0
  58. package/commands/member.js +899 -66
  59. package/commands/mission.js +3512 -332
  60. package/commands/next.js +137 -0
  61. package/commands/now.js +240 -21
  62. package/commands/one-lap.js +776 -0
  63. package/commands/orb.js +314 -0
  64. package/commands/pack-craft.js +179 -0
  65. package/commands/pack.js +823 -0
  66. package/commands/play.js +3 -2
  67. package/commands/probe.js +30 -3
  68. package/commands/pulse.js +241 -46
  69. package/commands/push.js +260 -82
  70. package/commands/radar.js +259 -14
  71. package/commands/rainmaker.js +49 -0
  72. package/commands/report.js +415 -0
  73. package/commands/scout.js +147 -0
  74. package/commands/search.js +363 -0
  75. package/commands/serve.js +54 -0
  76. package/commands/skill.js +47 -3
  77. package/commands/slop.js +50 -2
  78. package/commands/soul.js +1 -1
  79. package/commands/status.js +50 -5
  80. package/commands/stream.js +861 -0
  81. package/commands/stripe.js +38 -0
  82. package/commands/study.js +693 -0
  83. package/commands/supabase.js +39 -0
  84. package/commands/sync.js +67 -54
  85. package/commands/task.js +2275 -182
  86. package/commands/team.js +73 -0
  87. package/commands/truth.js +29 -3
  88. package/commands/unknowns.js +627 -0
  89. package/commands/update.js +44 -0
  90. package/commands/vercel.js +38 -0
  91. package/commands/verify.js +96 -0
  92. package/commands/watch.js +303 -0
  93. package/commands/wish.js +500 -0
  94. package/commands/workflow.js +11 -5
  95. package/commands/worktree.js +299 -20
  96. package/commands/write.js +399 -0
  97. package/commands/xp.js +29 -11
  98. package/lib/auto-accept-certified.js +391 -43
  99. package/lib/autoland.js +353 -52
  100. package/lib/ax-auto-lane.js +79 -0
  101. package/lib/bench/context.js +147 -0
  102. package/lib/bench/engines.js +141 -0
  103. package/lib/bench/report.js +140 -0
  104. package/lib/bench/runner.js +512 -0
  105. package/lib/brief-ledger.js +350 -0
  106. package/lib/cloud-mission.js +259 -0
  107. package/lib/codex-flight.js +154 -0
  108. package/lib/default-runner.js +45 -0
  109. package/lib/default-verifier.js +70 -0
  110. package/lib/engine-registry.js +232 -0
  111. package/lib/experiments/daily.js +640 -0
  112. package/lib/fleet.js +2431 -38
  113. package/lib/improve-vitals-html.js +171 -0
  114. package/lib/known-commands.js +58 -0
  115. package/lib/loop-doctor.js +416 -0
  116. package/lib/member-switches.js +144 -0
  117. package/lib/memory-view.js +14 -5
  118. package/lib/mission-room.js +1 -0
  119. package/lib/mission-root.js +52 -0
  120. package/lib/mission-runtime-loop.js +7 -0
  121. package/lib/next-moves.js +327 -10
  122. package/lib/official-cli-integration.js +174 -0
  123. package/lib/one-lap-validator.js +60 -0
  124. package/lib/orb-context.js +477 -0
  125. package/lib/orb-scorecard.js +224 -0
  126. package/lib/outbound-send-gate.js +165 -0
  127. package/lib/permission-grants.js +293 -0
  128. package/lib/policy-lessons.js +52 -1
  129. package/lib/pulse.js +277 -3
  130. package/lib/receipt-block.js +168 -0
  131. package/lib/receipt-evidence.js +65 -4
  132. package/lib/review-integrity.js +147 -0
  133. package/lib/router-brain.js +352 -0
  134. package/lib/runner-command.js +33 -0
  135. package/lib/self-drive.js +258 -0
  136. package/lib/short-name.js +103 -0
  137. package/lib/spawn-env.js +18 -0
  138. package/lib/state-detection.js +56 -1
  139. package/lib/sync-status.js +59 -0
  140. package/lib/task-db.js +319 -27
  141. package/lib/task-proof.js +43 -1
  142. package/lib/task-receipt.js +93 -0
  143. package/lib/team-presence.js +260 -0
  144. package/lib/tool-result-encode.js +7 -0
  145. package/lib/trust-tiers.js +90 -0
  146. package/lib/usage.js +107 -0
  147. package/lib/voice-gate.js +163 -0
  148. package/lib/wish-audit.js +1368 -0
  149. package/lib/wish-delegate.js +1840 -0
  150. package/lib/wish-design.js +110 -0
  151. package/lib/wish-stats.js +183 -0
  152. package/lib/wish-store.js +354 -0
  153. package/lib/zip.js +221 -0
  154. package/package.json +3 -1
  155. package/templates/loops/atris/loops/LOOPS.md +55 -0
  156. package/templates/loops/atris/loops/TICK.md +24 -0
  157. package/templates/loops/atris/loops/feedback.md +22 -0
  158. package/templates/loops/atris/loops/quality.md +22 -0
  159. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  160. package/utils/api.js +5 -1
  161. package/utils/auth.js +57 -21
  162. package/utils/update-check.js +27 -6
  163. package/atris/learnings.jsonl +0 -1
package/commands/push.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
+ const readline = require('readline');
4
5
  const { loadCredentials } = require('../utils/auth');
5
6
  const { apiRequestJson } = require('../utils/api');
6
7
  const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
@@ -54,6 +55,102 @@ function pathInScope(filePath, onlyPrefixes) {
54
55
  return onlyPrefixes.some(prefix => filePath.startsWith(prefix));
55
56
  }
56
57
 
58
+ const PUSH_VALUE_FLAGS = new Set(['--from', '--only', '--timeout']);
59
+
60
+ function looksLikePushPath(token) {
61
+ const value = String(token || '');
62
+ if (!value || value.startsWith('-')) return false;
63
+ if (value.includes('/') || value.includes('\\')) return true;
64
+ return value.includes('.');
65
+ }
66
+
67
+ function normalizePushScopePath(raw) {
68
+ const wikiPrefix = normalizeWikiOnlyPrefix(raw);
69
+ if (wikiPrefix) return `/${wikiPrefix.replace(/^\//, '')}`;
70
+ let n = '/' + String(raw || '').replace(/^\//, '').replace(/\\/g, '/');
71
+ if (!n.endsWith('/') && !n.includes('.')) n += '/';
72
+ return n;
73
+ }
74
+
75
+ /**
76
+ * Parse push argv into optional business slug, positional file paths, and --changed.
77
+ * `atris push [business] [file...] [--changed] [--only <prefix>]`
78
+ */
79
+ function parsePushArgv(argv = process.argv) {
80
+ const args = argv.slice(3);
81
+ const flags = new Set();
82
+ const values = {};
83
+ const positionals = [];
84
+
85
+ for (let i = 0; i < args.length; i++) {
86
+ const a = args[i];
87
+ if (!a.startsWith('-')) {
88
+ positionals.push(a);
89
+ continue;
90
+ }
91
+ const eq = a.indexOf('=');
92
+ if (eq !== -1) {
93
+ values[a.slice(0, eq)] = a.slice(eq + 1);
94
+ continue;
95
+ }
96
+ if (PUSH_VALUE_FLAGS.has(a) && args[i + 1] && !args[i + 1].startsWith('-')) {
97
+ values[a] = args[++i];
98
+ continue;
99
+ }
100
+ flags.add(a);
101
+ }
102
+
103
+ let slug = null;
104
+ const pathArgs = [];
105
+ for (const token of positionals) {
106
+ if (!slug && !looksLikePushPath(token)) slug = token;
107
+ else pathArgs.push(token);
108
+ }
109
+
110
+ return {
111
+ slug,
112
+ pathArgs,
113
+ changed: flags.has('--changed'),
114
+ onlyRaw: values['--only'] || null,
115
+ };
116
+ }
117
+
118
+ /** Local files whose hash differs from (or is absent in) the last-synced manifest. */
119
+ function resolveChangedScopePaths(localFiles = {}, baseFiles = {}) {
120
+ const paths = [];
121
+ for (const [filePath, info] of Object.entries(localFiles || {})) {
122
+ const base = baseFiles[filePath];
123
+ if (!base || base.hash !== info.hash) paths.push(filePath.startsWith('/') ? filePath : `/${filePath}`);
124
+ }
125
+ return paths.sort();
126
+ }
127
+
128
+ /**
129
+ * Merge --only, positional paths, and --changed into onlyPrefixes for buildPushChangePlan.
130
+ * Returns null when nothing scopes the push (full workspace plan).
131
+ * `--changed` with no diffs returns [] so the plan stays empty instead of widening.
132
+ */
133
+ function mergePushScopes({ onlyRaw = null, pathArgs = [], changed = false, localFiles = {}, baseFiles = {} } = {}) {
134
+ const scopes = [];
135
+ if (onlyRaw) {
136
+ for (const part of String(onlyRaw).split(',')) {
137
+ const trimmed = part.trim();
138
+ if (trimmed) scopes.push(normalizePushScopePath(trimmed));
139
+ }
140
+ }
141
+ for (const raw of pathArgs || []) {
142
+ const trimmed = String(raw || '').trim();
143
+ if (trimmed) scopes.push(normalizePushScopePath(trimmed));
144
+ }
145
+ if (changed) {
146
+ for (const filePath of resolveChangedScopePaths(localFiles, baseFiles)) {
147
+ scopes.push(filePath.startsWith('/') ? filePath : `/${filePath}`);
148
+ }
149
+ }
150
+ if (scopes.length === 0) return changed ? [] : null;
151
+ return [...new Set(scopes)];
152
+ }
153
+
57
154
  function buildPushChangePlan({
58
155
  localFiles = {},
59
156
  baseFiles = {},
@@ -98,6 +195,34 @@ function shouldRetrySyncIndividually(result, filesToPush) {
98
195
  return result.status !== 403 && result.status !== 409;
99
196
  }
100
197
 
198
+ function buildPushUploadBatches(filesToPush = [], { maxBatchBytes = 256 * 1024 } = {}) {
199
+ const batches = [];
200
+ let current = [];
201
+ let currentBytes = 0;
202
+ const limit = Number.isFinite(maxBatchBytes) && maxBatchBytes > 0 ? maxBatchBytes : 256 * 1024;
203
+
204
+ const flush = () => {
205
+ if (current.length > 0) batches.push(current);
206
+ current = [];
207
+ currentBytes = 0;
208
+ };
209
+
210
+ for (const file of filesToPush) {
211
+ const bytes = Buffer.byteLength(file && file.content ? String(file.content) : '', 'utf8');
212
+ if (bytes > limit) {
213
+ flush();
214
+ batches.push([file]);
215
+ continue;
216
+ }
217
+ if (current.length > 0 && currentBytes + bytes > limit) flush();
218
+ current.push(file);
219
+ currentBytes += bytes;
220
+ }
221
+
222
+ flush();
223
+ return batches;
224
+ }
225
+
101
226
  function isMassDeletePlan({ deletedPaths = [], filesToPush = [], unchangedCount = 0 } = {}) {
102
227
  const deleteCount = deletedPaths.length;
103
228
  if (deleteCount === 0) return false;
@@ -217,6 +342,36 @@ function renderPushSafetyBlock(report, slug) {
217
342
  return `${lines.join('\n')}\n`;
218
343
  }
219
344
 
345
+ function renderDriftBlock(driftFiles) {
346
+ const lines = [];
347
+ const count = driftFiles.length;
348
+ lines.push('');
349
+ lines.push(` ${count} file${count === 1 ? '' : 's'} conflict. run atris sync --review to pick local/cloud/merge, or atris push --only <path> to ship just what changed.`);
350
+ lines.push('');
351
+ lines.push(' Files that differ on cloud:');
352
+ driftFiles.slice(0, 8).forEach((p) => lines.push(` ~ ${p.replace(/^\//, '')}`));
353
+ if (driftFiles.length > 8) lines.push(` ... +${driftFiles.length - 8} more`);
354
+ lines.push('');
355
+ lines.push(' To override (force-push, may clobber cloud edits): atris push --force');
356
+ lines.push('');
357
+ return `${lines.join('\n')}\n`;
358
+ }
359
+
360
+ function needsForceBroadWorkspaceConfirm({ force = false, allowBroadWorkspace = false, yes = false, dryRun = false } = {}) {
361
+ return force && allowBroadWorkspace && !yes && !dryRun;
362
+ }
363
+
364
+ async function promptConfirm(message, { input = process.stdin, output = process.stdout } = {}) {
365
+ return new Promise((resolve) => {
366
+ const rl = readline.createInterface({ input, output });
367
+ rl.question(message, (answer) => {
368
+ rl.close();
369
+ const normalized = String(answer).trim().toLowerCase();
370
+ resolve(normalized === 'y' || normalized === 'yes');
371
+ });
372
+ });
373
+ }
374
+
220
375
  function parsePushTimeoutSec(argv = process.argv, defaultSec = 120) {
221
376
  let raw = null;
222
377
  const eqArg = argv.find(a => a.startsWith('--timeout='));
@@ -233,17 +388,20 @@ function parsePushTimeoutSec(argv = process.argv, defaultSec = 120) {
233
388
 
234
389
  async function pushAtris() {
235
390
  const elapsedMs = startTimer();
236
- let slug = process.argv[3];
391
+ const parsed = parsePushArgv(process.argv);
392
+ let slug = parsed.slug;
237
393
  let _coldWake = false;
238
394
 
239
395
  if (process.argv.includes('--help') || process.argv.includes('-h') || slug === 'help') {
240
- console.log('Usage: atris push [business] [--from <path>] [--only <prefix>] [--force] [--delete] [--delete-all]');
396
+ console.log('Usage: atris push [business] [file...] [--changed] [--from <path>] [--only <prefix>] [--force] [--delete] [--delete-all]');
241
397
  console.log('');
242
398
  console.log(' Push requires a fresh pull. If cloud has changed since your last pull,');
243
399
  console.log(' the push will be blocked until you run `atris pull`. Use --force to override.');
244
400
  console.log('');
245
401
  console.log(' atris push Push from current folder (auto-detect business)');
246
402
  console.log(' atris push example-co Push example-co/ or atris/example-co/');
403
+ console.log(' atris push atris/now.md Push only the named file(s)');
404
+ console.log(' atris push --changed Push only files that differ from the last-synced manifest');
247
405
  console.log(' atris push example-co --only team/nate Only push files in team/nate/');
248
406
  console.log(' atris push --force Bypass freshness check (force-push, may overwrite cloud changes)');
249
407
  console.log(' atris push --delete Allow small cloud deletes shown by --dry-run');
@@ -251,6 +409,7 @@ async function pushAtris() {
251
409
  console.log(' --allow-broad-workspace Publish a large unscoped workspace plan after review');
252
410
  console.log(' --allow-nested-workspace Explicitly allow nested <slug>/ paths');
253
411
  console.log(' --allow-sync-artifacts Explicitly allow *.remote/*.local/*.base/*.cloud files');
412
+ console.log(' --yes Skip interactive confirmation (e.g. force broad workspace)');
254
413
  process.exit(0);
255
414
  }
256
415
 
@@ -267,13 +426,15 @@ async function pushAtris() {
267
426
  }
268
427
 
269
428
  if (!slug || slug === '--help') {
270
- console.log('Usage: atris push [business] [--from <path>] [--only <prefix>] [--force] [--delete] [--delete-all]');
429
+ console.log('Usage: atris push [business] [file...] [--changed] [--from <path>] [--only <prefix>] [--force] [--delete] [--delete-all]');
271
430
  console.log('');
272
431
  console.log(' Push requires a fresh pull. If cloud has changed since your last pull,');
273
432
  console.log(' the push will be blocked until you run `atris pull`. Use --force to override.');
274
433
  console.log('');
275
434
  console.log(' atris push Push from current folder (auto-detect business)');
276
435
  console.log(' atris push example-co Push example-co/ or atris/example-co/');
436
+ console.log(' atris push atris/now.md Push only the named file(s)');
437
+ console.log(' atris push --changed Push only files that differ from the last-synced manifest');
277
438
  console.log(' atris push example-co --only team/nate Only push files in team/nate/');
278
439
  console.log(' atris push --force Bypass freshness check (force-push, may overwrite cloud changes)');
279
440
  console.log(' atris push --delete Allow small cloud deletes shown by --dry-run');
@@ -281,11 +442,13 @@ async function pushAtris() {
281
442
  console.log(' --allow-broad-workspace Publish a large unscoped workspace plan after review');
282
443
  console.log(' --allow-nested-workspace Explicitly allow nested <slug>/ paths');
283
444
  console.log(' --allow-sync-artifacts Explicitly allow *.remote/*.local/*.base/*.cloud files');
445
+ console.log(' --yes Skip interactive confirmation (e.g. force broad workspace)');
284
446
  process.exit(0);
285
447
  }
286
448
 
287
449
  const force = process.argv.includes('--force');
288
450
  const dryRun = process.argv.includes('--dry-run');
451
+ const yes = process.argv.includes('--yes');
289
452
  const allowDelete = process.argv.includes('--delete');
290
453
  const allowMassDelete = process.argv.includes('--delete-all');
291
454
  const allowBroadWorkspace = process.argv.includes('--allow-broad-workspace');
@@ -293,24 +456,7 @@ async function pushAtris() {
293
456
  const allowSyncArtifacts = process.argv.includes('--allow-sync-artifacts');
294
457
  const allowCrossRootManifest = process.argv.includes('--allow-cross-root-manifest');
295
458
  const timeoutSec = parsePushTimeoutSec(process.argv);
296
-
297
- // Parse --only
298
- let onlyRaw = null;
299
- const onlyEq = process.argv.find(a => a.startsWith('--only='));
300
- if (onlyEq) onlyRaw = onlyEq.slice(7);
301
- else {
302
- const oi = process.argv.indexOf('--only');
303
- if (oi !== -1 && process.argv[oi + 1] && !process.argv[oi + 1].startsWith('-')) onlyRaw = process.argv[oi + 1];
304
- }
305
- let onlyPrefixes = onlyRaw
306
- ? onlyRaw.split(',').map(p => {
307
- const wikiPrefix = normalizeWikiOnlyPrefix(p);
308
- if (wikiPrefix) return `/${wikiPrefix.replace(/^\//, '')}`;
309
- let n = '/' + p.replace(/^\//, '');
310
- if (!n.endsWith('/') && !n.includes('.')) n += '/';
311
- return n;
312
- })
313
- : null;
459
+ let onlyPrefixes = null;
314
460
 
315
461
  const creds = loadCredentials();
316
462
  if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
@@ -328,6 +474,20 @@ async function pushAtris() {
328
474
  // Refuse to walk/upload dangerous paths ($HOME, /, /Users, system dirs).
329
475
  assertSafeWorkspaceRoot(sourceDir, { slug, op: 'push from' });
330
476
 
477
+ // Force-pushing a broad workspace is the most destructive push combo.
478
+ // Require an interactive y/N confirm unless the caller passed --yes.
479
+ if (needsForceBroadWorkspaceConfirm({ force, allowBroadWorkspace, yes, dryRun })) {
480
+ if (!process.stdin.isTTY) {
481
+ console.log(' --force --allow-broad-workspace requires an interactive terminal or --yes');
482
+ process.exit(1);
483
+ }
484
+ const ok = await promptConfirm('This will force push a broad workspace. Continue? (y/N) ');
485
+ if (!ok) {
486
+ console.log(' aborted');
487
+ process.exit(1);
488
+ }
489
+ }
490
+
331
491
  // Push the whole business workspace by default. Brain-only sync must be
332
492
  // explicit with --only atris/ so root workspace files cannot be missed.
333
493
 
@@ -412,6 +572,17 @@ async function pushAtris() {
412
572
  return;
413
573
  }
414
574
 
575
+ // Resolve scoped push filters before freshness so drift checks honor
576
+ // positional paths / --changed the same way --only already did.
577
+ const baseFiles = filterSyncFiles((manifest && manifest.files) ? manifest.files : {});
578
+ onlyPrefixes = mergePushScopes({
579
+ onlyRaw: parsed.onlyRaw,
580
+ pathArgs: parsed.pathArgs,
581
+ changed: parsed.changed,
582
+ localFiles,
583
+ baseFiles,
584
+ });
585
+
415
586
  console.log('');
416
587
  console.log(`Pushing to ${businessName}...`);
417
588
 
@@ -466,16 +637,7 @@ async function pushAtris() {
466
637
  }
467
638
  }
468
639
  if (driftFiles.length > 0) {
469
- console.log(`drift detected (${driftFiles.length} file${driftFiles.length === 1 ? '' : 's'})`);
470
- console.log('');
471
- console.log(` ✗ Cloud has changed since your last pull. Refusing to push stale state.`);
472
- console.log('');
473
- console.log(' Files that differ on cloud:');
474
- driftFiles.slice(0, 8).forEach((p) => console.log(` ~ ${p.replace(/^\//, '')}`));
475
- if (driftFiles.length > 8) console.log(` ... +${driftFiles.length - 8} more`);
476
- console.log('');
477
- console.log(' Run `atris pull` first, then push your changes.');
478
- console.log(' To override (force-push, may clobber cloud edits): atris push --force');
640
+ process.stdout.write(renderDriftBlock(driftFiles));
479
641
  await emit('drift', { error_detail: `${driftFiles.length} file(s) drifted` });
480
642
  process.exit(1);
481
643
  }
@@ -498,7 +660,11 @@ async function pushAtris() {
498
660
 
499
661
  // Compare local hashes to manifest — NO server call needed
500
662
  // Files where local hash differs from manifest = changed locally
501
- const baseFiles = filterSyncFiles((manifest && manifest.files) ? manifest.files : {});
663
+ if (parsed.changed && Array.isArray(onlyPrefixes) && onlyPrefixes.length === 0 && (!parsed.pathArgs || parsed.pathArgs.length === 0) && !parsed.onlyRaw) {
664
+ console.log('\n Already up to date.\n');
665
+ await emit('success', { files_unchanged: Object.keys(localFiles).length });
666
+ return;
667
+ }
502
668
  const { filesToPush, deletedPaths, unchangedCount } = buildPushChangePlan({
503
669
  localFiles,
504
670
  baseFiles,
@@ -667,64 +833,68 @@ async function pushAtris() {
667
833
 
668
834
  if (filesToPush.length > 0) {
669
835
  // Push files to server (strip leading slash — server requires workspace-relative paths)
670
- result = await syncFiles(filesToPush);
671
-
672
- if (!result.ok) {
673
- if (result.status === 403) {
674
- const detail = result.errorMessage || result.error || (result.data && result.data.detail) || '';
675
- if (detail && /plan required|business, max, or enterprise/i.test(detail)) {
676
- console.error(`\n Access denied: ${detail}`);
677
- await emit('access_denied', { error_detail: detail });
678
- process.exit(1);
679
- }
680
- // Permission denied — retry with only team/ and journal/ files
681
- const allowed = filesToPush.filter(f => f.path.startsWith('/team/') || f.path.startsWith('/journal/'));
682
- skipped = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));
683
-
684
- if (allowed.length > 0) {
685
- const retry = await syncFiles(allowed);
686
- if (retry.ok) {
687
- recordSyncResults(allowed, retry);
688
- pushed = landedPaths.size;
836
+ const uploadBatches = buildPushUploadBatches(filesToPush);
837
+ for (const batch of uploadBatches) {
838
+ result = await syncFiles(batch);
839
+
840
+ if (!result.ok) {
841
+ if (result.status === 403) {
842
+ const detail = result.errorMessage || result.error || (result.data && result.data.detail) || '';
843
+ if (detail && /plan required|business, max, or enterprise/i.test(detail)) {
844
+ console.error(`\n Access denied: ${detail}`);
845
+ await emit('access_denied', { error_detail: detail });
846
+ process.exit(1);
847
+ }
848
+ // Permission denied — retry with only team/ and journal/ files
849
+ const allowed = filesToPush.filter(f => f.path.startsWith('/team/') || f.path.startsWith('/journal/'));
850
+ skipped = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));
851
+
852
+ if (allowed.length > 0) {
853
+ const retry = await syncFiles(allowed);
854
+ if (retry.ok) {
855
+ recordSyncResults(allowed, retry);
856
+ pushed = landedPaths.size;
857
+ break;
858
+ } else {
859
+ console.error(`\n Push failed: ${retry.errorMessage || retry.error || retry.status}`);
860
+ await emit('access_denied', { error_detail: `403 retry failed: ${retry.status}` });
861
+ process.exit(1);
862
+ }
689
863
  } else {
690
- console.error(`\n Push failed: ${retry.errorMessage || retry.error || retry.status}`);
691
- await emit('access_denied', { error_detail: `403 retry failed: ${retry.status}` });
864
+ console.error('\n Access denied: you can only push to your team/ folder.');
865
+ await emit('access_denied');
692
866
  process.exit(1);
693
867
  }
694
- } else {
695
- console.error('\n Access denied: you can only push to your team/ folder.');
696
- await emit('access_denied');
868
+ } else if (result.status === 409) {
869
+ console.error('\n Computer is sleeping. Wake it first.');
870
+ await emit('cold_wake', { error_detail: 'computer sleeping (409)' });
697
871
  process.exit(1);
698
- }
699
- } else if (result.status === 409) {
700
- console.error('\n Computer is sleeping. Wake it first.');
701
- await emit('cold_wake', { error_detail: 'computer sleeping (409)' });
702
- process.exit(1);
703
- } else if (shouldRetrySyncIndividually(result, filesToPush)) {
704
- batchFailureDetail = `${result.status || 'unknown'}: ${result.errorMessage || result.error || 'batch sync failed'}`;
705
- console.log('');
706
- console.log(` Batch push failed (${result.errorMessage || result.error || result.status}). Retrying one file at a time...`);
707
- for (const f of filesToPush) {
708
- const single = await syncFiles([f]);
709
- if (single.ok) {
710
- recordSyncResults([f], single);
711
- } else {
712
- failedToLand.push({
713
- path: f.path,
714
- status: single.status || 'error',
715
- error: single.errorMessage || single.error || '',
716
- });
872
+ } else if (shouldRetrySyncIndividually(result, batch)) {
873
+ batchFailureDetail = `${result.status || 'unknown'}: ${result.errorMessage || result.error || 'batch sync failed'}`;
874
+ console.log('');
875
+ console.log(` Batch push failed (${result.errorMessage || result.error || result.status}). Retrying one file at a time...`);
876
+ for (const f of batch) {
877
+ const single = await syncFiles([f]);
878
+ if (single.ok) {
879
+ recordSyncResults([f], single);
880
+ } else {
881
+ failedToLand.push({
882
+ path: f.path,
883
+ status: single.status || 'error',
884
+ error: single.errorMessage || single.error || '',
885
+ });
886
+ }
717
887
  }
888
+ pushed = landedPaths.size;
889
+ } else {
890
+ console.error(`\n Push failed: ${result.errorMessage || result.error || result.status}`);
891
+ await emit('status_unknown', { error_detail: `sync status ${result.status}` });
892
+ process.exit(1);
718
893
  }
719
- pushed = landedPaths.size;
720
894
  } else {
721
- console.error(`\n Push failed: ${result.errorMessage || result.error || result.status}`);
722
- await emit('status_unknown', { error_detail: `sync status ${result.status}` });
723
- process.exit(1);
895
+ recordSyncResults(batch, result);
896
+ pushed = landedPaths.size;
724
897
  }
725
- } else {
726
- recordSyncResults(filesToPush, result);
727
- pushed = landedPaths.size;
728
898
  }
729
899
  }
730
900
 
@@ -882,6 +1052,7 @@ async function pushAtris() {
882
1052
  module.exports = {
883
1053
  pushAtris,
884
1054
  buildPushChangePlan,
1055
+ buildPushUploadBatches,
885
1056
  shouldRetrySyncIndividually,
886
1057
  resolvePushSourceDir,
887
1058
  canonicalWorkspaceRoot,
@@ -890,8 +1061,15 @@ module.exports = {
890
1061
  isBusinessWorkspaceRoot,
891
1062
  isMassDeletePlan,
892
1063
  parsePushTimeoutSec,
1064
+ parsePushArgv,
1065
+ normalizePushScopePath,
1066
+ resolveChangedScopePaths,
1067
+ mergePushScopes,
893
1068
  analyzePushSafety,
894
1069
  isNestedWorkspacePollutionPath,
895
1070
  isSyncReviewArtifactPath,
896
1071
  renderPushSafetyBlock,
1072
+ renderDriftBlock,
1073
+ needsForceBroadWorkspaceConfirm,
1074
+ promptConfirm,
897
1075
  };