@stravica-ai/digital-operator-pa 0.1.12 → 0.1.13

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.
@@ -69,8 +69,31 @@ export async function fetchOrigin(runner, { cwd }) {
69
69
  return runner.spawn('git', ['fetch', 'origin'], { cwd });
70
70
  }
71
71
 
72
+ // `--autostash` is load-bearing, not convenience. estate-sync commits
73
+ // FIRST (see run.mjs step 6), but only the registry-scoped IN-SCOPE dirty
74
+ // paths are ever staged — dirty paths under an excluded mount subtree
75
+ // (down-path deliveries of canonical-owned content, materialised into the
76
+ // workspace by library-sync on hosts that run no wrap-pr leg) legitimately
77
+ // remain unstaged at rebase time. A plain `git pull --rebase` refuses to
78
+ // start over those unstaged changes ("cannot pull with rebase: You have
79
+ // unstaged changes"), which wedged ops-01 twice on 2026-07-29
80
+ // (w-2026-07-29-019): exit 2 every tick while the sync's own
81
+ // push-race-lost commits piled up ahead and the workspace diverged
82
+ // (ahead-2/behind-5), taking every librarian sweep down with
83
+ // fail-clone-behind/dirty until a human ran stash -> rebase -> push ->
84
+ // pop by hand. `--autostash` is exactly that manual recovery, done by
85
+ // git itself: stash the unstaged (excluded-scope) dirt, rebase, pop.
86
+ // carry-home and housekeeping already pull with --autostash for the same
87
+ // reason.
88
+ //
89
+ // If popping the autostash conflicts (upstream changed the same excluded
90
+ // file differently), git completes the rebase, KEEPS the changes in the
91
+ // stash, and prints "applying autostash resulted in conflicts" — the
92
+ // working tree is left clean, the push still proceeds, and run.mjs
93
+ // detects that message and surfaces it in the audit record (see
94
+ // AUTOSTASH_CONFLICT_RE) rather than letting parked dirt go silent.
72
95
  export async function pullRebase(runner, { cwd, branch }) {
73
- return runner.spawn('git', ['pull', '--rebase', '--quiet', 'origin', branch], { cwd });
96
+ return runner.spawn('git', ['pull', '--rebase', '--autostash', '--quiet', 'origin', branch], { cwd });
74
97
  }
75
98
 
76
99
  export async function rebaseAbort(runner, { cwd }) {
@@ -25,8 +25,18 @@
25
25
  // `git commit -m "estate-sync <ISO ts>" -m "<body>"`.
26
26
  // (this happens BEFORE the pull — git pull --rebase refuses to run
27
27
  // with uncommitted changes in the index or working tree)
28
- // 7. git pull --rebase origin <branch> (fetch already done in step 2)
28
+ // 7. git pull --rebase --autostash origin <branch> (fetch already done
29
+ // in step 2). --autostash matters: step 6 stages only IN-SCOPE dirty
30
+ // paths, so registry-excluded dirt (down-path deliveries of
31
+ // mount-owned content on hosts with no wrap-pr leg) legitimately
32
+ // remains unstaged here — without autostash the rebase refuses to
33
+ // start over it and the sync wedges with exit 2 on every tick
34
+ // (observed live on ops-01, 2026-07-29, w-2026-07-29-019).
29
35
  // on rebase conflict: git rebase --abort + fail-rebase audit + exit 2
36
+ // (abort also restores the autostash). If the rebase succeeds but
37
+ // POPPING the autostash conflicts, git keeps the dirt in the stash
38
+ // and completes — we detect the message and record
39
+ // autostashConflict:true in the audit rather than going silent.
30
40
  // 8. git push. on rejection: fetch + pull --rebase + push retry. one
31
41
  // retry only.
32
42
  // 9. append success audit to logs/runs.jsonl
@@ -89,6 +99,25 @@ function log(line, { stderr = process.stderr } = {}) {
89
99
  stderr.write(`[estate-sync] ${line}\n`);
90
100
  }
91
101
 
102
+ // Git's exact wording when a rebase completes but re-applying the
103
+ // autostash conflicts: "Applying autostash resulted in conflicts. Your
104
+ // changes are safe in the stash. ..." (sequencer.c, stable across
105
+ // versions). The rebase itself SUCCEEDS in this case (exit 0) — the
106
+ // excluded-scope dirt is parked in `git stash` and the working tree is
107
+ // left clean. That is a safe state (the next down-path re-materialises
108
+ // the same content), but it must never be silent: the audit record
109
+ // carries autostashConflict:true so a human can `git stash pop` / `drop`
110
+ // deliberately.
111
+ const AUTOSTASH_CONFLICT_RE = /applying autostash resulted in conflicts/i;
112
+
113
+ function detectAutostashConflict(res, stderr) {
114
+ const matched = AUTOSTASH_CONFLICT_RE.test(`${res.stdout || ''}\n${res.stderr || ''}`);
115
+ if (matched) {
116
+ log('rebase succeeded but applying the autostash conflicted — excluded-scope dirt is parked in `git stash` (run `git stash pop` or `git stash drop` deliberately)', { stderr });
117
+ }
118
+ return matched;
119
+ }
120
+
92
121
  async function writeAudit(runsLog, record, stderr) {
93
122
  try {
94
123
  await appendFile(runsLog, JSON.stringify(record) + '\n');
@@ -317,8 +346,10 @@ export async function runSync({
317
346
  // If fetch failed earlier, we'd be rebasing onto stale refs which
318
347
  // either no-ops (if local refs already match) or fails spuriously.
319
348
  let autoResolvedPaths = [];
349
+ let autostashConflict = false;
320
350
  if (upfrontFetch.code === 0) {
321
351
  const prRes = await pullRebase(runner, { cwd: repoRoot, branch });
352
+ autostashConflict = detectAutostashConflict(prRes, stderr) || autostashConflict;
322
353
  if (prRes.code !== 0) {
323
354
  log(`git pull --rebase failed: ${prRes.stderr.trim()}`, { stderr });
324
355
  // Before aborting, see if the conflict is limited to files that
@@ -365,9 +396,12 @@ export async function runSync({
365
396
  // pointing at remote refs (nothing to push). If we committed, we have
366
397
  // at least one local-only commit. If we were just behind, pull
367
398
  // brought us up to date — nothing to push.
368
- const autoResolveFields = autoResolvedPaths.length > 0
369
- ? { autoResolved: autoResolvedPaths.length, autoResolvedPaths }
370
- : {};
399
+ const autoResolveFields = {
400
+ ...(autoResolvedPaths.length > 0
401
+ ? { autoResolved: autoResolvedPaths.length, autoResolvedPaths }
402
+ : {}),
403
+ ...(autostashConflict ? { autostashConflict: true } : {}),
404
+ };
371
405
 
372
406
  const { ahead: aheadAfter } = await aheadBehind(runner, { cwd: repoRoot });
373
407
  const needsPush = Number.isFinite(aheadAfter) && aheadAfter > 0;
@@ -416,6 +450,7 @@ export async function runSync({
416
450
  return 2;
417
451
  }
418
452
  const pr2 = await pullRebase(runner, { cwd: repoRoot, branch });
453
+ autostashConflict = detectAutostashConflict(pr2, stderr) || autostashConflict;
419
454
  if (pr2.code !== 0) {
420
455
  log(`git pull --rebase (for push-retry) failed: ${pr2.stderr.trim()}`, { stderr });
421
456
  // Symmetric with step 7: if the push-retry rebase conflicts only
@@ -460,9 +495,12 @@ export async function runSync({
460
495
  }
461
496
  log('pushed after rebase', { stderr });
462
497
  // Recompute so any auto-resolve during the push-retry is captured.
463
- const autoResolveFieldsFinal = autoResolvedPaths.length > 0
464
- ? { autoResolved: autoResolvedPaths.length, autoResolvedPaths }
465
- : {};
498
+ const autoResolveFieldsFinal = {
499
+ ...(autoResolvedPaths.length > 0
500
+ ? { autoResolved: autoResolvedPaths.length, autoResolvedPaths }
501
+ : {}),
502
+ ...(autostashConflict ? { autostashConflict: true } : {}),
503
+ };
466
504
  await writeAudit(runsLog, {
467
505
  ts: tsIso,
468
506
  status: 'ok',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Digital Operator PA — single-bundled public package surface (bins + library) for the platform.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",