@enderfga/claw-orchestrator 7.11.0 → 7.12.1
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/README.md +5 -5
- package/dist/src/kernel/nodes/autoloop.d.ts +3 -1
- package/dist/src/kernel/nodes/autoloop.js +3 -1
- package/dist/src/kernel/nodes/autoloop.js.map +1 -1
- package/dist/src/models.js +10 -2
- package/dist/src/models.js.map +1 -1
- package/dist/src/persistent-grok-session.d.ts +4 -0
- package/dist/src/persistent-grok-session.js +11 -2
- package/dist/src/persistent-grok-session.js.map +1 -1
- package/dist/src/session-manager.js +304 -40
- package/dist/src/session-manager.js.map +1 -1
- package/package.json +2 -2
- package/skills/references/autoloop.md +34 -9
- package/skills/references/claude-cli-tracking.md +3 -1
- package/skills/references/multi-engine.md +8 -6
- package/skills/references/workflow.md +1 -1
|
@@ -115,7 +115,7 @@ import { detectRepoLang } from './kernel/repo.js';
|
|
|
115
115
|
import { RunKernel, runDir as kernelRunDir } from './kernel/engine.js';
|
|
116
116
|
import { registerDefaultExecutors } from './kernel/nodes/index.js';
|
|
117
117
|
import { autoloopStateFromRecord, makeAutoloopExecutor } from './kernel/nodes/autoloop.js';
|
|
118
|
-
import { loadRun, readNodeOutput } from './kernel/store.js';
|
|
118
|
+
import { isValidRunId, loadRun, readNodeOutput } from './kernel/store.js';
|
|
119
119
|
import { LEGACY_NODE, joinFindings, toCouncilSession, toFanoutSession, toUltraplanResult, toUltrareviewResult, } from './kernel/projections.js';
|
|
120
120
|
import { legacyCouncilWorkflow, legacyFanoutWorkflow, legacyUltraplanWorkflow, splitAgentSecrets, } from './kernel/templates/index.js';
|
|
121
121
|
import { normalizeContract } from './verify/contract.js';
|
|
@@ -143,7 +143,7 @@ import { isAgyConversationId } from './agy-conversation.js';
|
|
|
143
143
|
import { Council } from './council.js';
|
|
144
144
|
import { AutoloopRunner } from './autoloop/runner.js';
|
|
145
145
|
import { ClaudeAgentDispatcher } from './autoloop/dispatcher.js';
|
|
146
|
-
import { DEFAULT_PUSH_POLICY, DEFAULT_SEND_TIMEOUT_MS, validateAutoloopTimeoutConfig } from './autoloop/types.js';
|
|
146
|
+
import { DEFAULT_PUSH_POLICY, DEFAULT_SEND_TIMEOUT_MS, MAX_METRIC_HISTORY, validateAutoloopTimeoutConfig, } from './autoloop/types.js';
|
|
147
147
|
import { Msg as AutoloopMsg } from './autoloop/messages.js';
|
|
148
148
|
import { appendPushLog, notifyUserFallbackChain } from './autoloop/notify.js';
|
|
149
149
|
import { UltraappManager } from './ultraapp/manager.js';
|
|
@@ -454,6 +454,165 @@ function porcelainHasDisallowedAutoloopDirt(workspace, porcelain, prefix) {
|
|
|
454
454
|
}
|
|
455
455
|
return false;
|
|
456
456
|
}
|
|
457
|
+
/** Fixed generic refusal — no runId, path, file bytes, or git stderr. */
|
|
458
|
+
const AUTOLOOP_LEDGER_ISOLATION_ERROR = 'Autoloop could not isolate the run ledger from Git';
|
|
459
|
+
function refuseAutoloopLedgerIsolation() {
|
|
460
|
+
throw new Error(AUTOLOOP_LEDGER_ISOLATION_ERROR);
|
|
461
|
+
}
|
|
462
|
+
/** Ensure absPath is a real directory (not a symlink); mkdir non-recursively if missing. */
|
|
463
|
+
function ensureRealAutoloopDir(absPath) {
|
|
464
|
+
if (!fs.existsSync(absPath))
|
|
465
|
+
fs.mkdirSync(absPath);
|
|
466
|
+
let st;
|
|
467
|
+
try {
|
|
468
|
+
st = fs.lstatSync(absPath);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
refuseAutoloopLedgerIsolation();
|
|
472
|
+
}
|
|
473
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
474
|
+
refuseAutoloopLedgerIsolation();
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* After dirty refusal: create `tasks/<runId>/.gitignore` (`*`) and prove via
|
|
478
|
+
* check-ignore. Never deletes ledger state. Invalid runIds skip (kernel rejects).
|
|
479
|
+
*/
|
|
480
|
+
function ensureAutoloopRunLedgerIgnored(workspace, runId) {
|
|
481
|
+
if (!isValidRunId(runId))
|
|
482
|
+
return;
|
|
483
|
+
const relLedger = path.posix.join('tasks', runId);
|
|
484
|
+
const gitEnv = { ...process.env, LC_ALL: 'C' };
|
|
485
|
+
let tracked;
|
|
486
|
+
try {
|
|
487
|
+
tracked = execFileSync('git', ['-C', workspace, '--literal-pathspecs', 'ls-files', '-z', '--', relLedger], {
|
|
488
|
+
encoding: 'utf8',
|
|
489
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
490
|
+
env: gitEnv,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
refuseAutoloopLedgerIsolation();
|
|
495
|
+
}
|
|
496
|
+
if (tracked.length > 0)
|
|
497
|
+
refuseAutoloopLedgerIsolation();
|
|
498
|
+
const tasksAbs = path.join(workspace, 'tasks');
|
|
499
|
+
const ledgerAbs = path.join(tasksAbs, runId);
|
|
500
|
+
try {
|
|
501
|
+
ensureRealAutoloopDir(tasksAbs);
|
|
502
|
+
ensureRealAutoloopDir(ledgerAbs);
|
|
503
|
+
if (fs.realpathSync(ledgerAbs) !== path.join(fs.realpathSync(workspace), 'tasks', runId)) {
|
|
504
|
+
refuseAutoloopLedgerIsolation();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
if (err instanceof Error && err.message === AUTOLOOP_LEDGER_ISOLATION_ERROR)
|
|
509
|
+
throw err;
|
|
510
|
+
refuseAutoloopLedgerIsolation();
|
|
511
|
+
}
|
|
512
|
+
const ignoreAbs = path.join(ledgerAbs, '.gitignore');
|
|
513
|
+
try {
|
|
514
|
+
const fd = fs.openSync(ignoreAbs, 'wx');
|
|
515
|
+
try {
|
|
516
|
+
fs.writeFileSync(fd, '*\n');
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
fs.closeSync(fd);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
if (err.code !== 'EEXIST')
|
|
524
|
+
refuseAutoloopLedgerIsolation();
|
|
525
|
+
try {
|
|
526
|
+
const st = fs.lstatSync(ignoreAbs);
|
|
527
|
+
if (!st.isFile() || st.isSymbolicLink())
|
|
528
|
+
refuseAutoloopLedgerIsolation();
|
|
529
|
+
const bytes = fs.readFileSync(ignoreAbs, 'utf8');
|
|
530
|
+
if (bytes !== '*' && bytes !== '*\n' && bytes !== '*\r\n')
|
|
531
|
+
refuseAutoloopLedgerIsolation();
|
|
532
|
+
}
|
|
533
|
+
catch (inner) {
|
|
534
|
+
if (inner instanceof Error && inner.message === AUTOLOOP_LEDGER_ISOLATION_ERROR)
|
|
535
|
+
throw inner;
|
|
536
|
+
refuseAutoloopLedgerIsolation();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// check-ignore rejects --literal-pathspecs; valid runIds have no pathspec magic.
|
|
540
|
+
for (const probe of [
|
|
541
|
+
`${relLedger}/.gitignore`,
|
|
542
|
+
`${relLedger}/chat.jsonl`,
|
|
543
|
+
`${relLedger}/decisions.jsonl`,
|
|
544
|
+
`${relLedger}/iter/0/verdict.json`,
|
|
545
|
+
`${relLedger}/reviewer_sandbox/prior_verdict.json`,
|
|
546
|
+
]) {
|
|
547
|
+
try {
|
|
548
|
+
execFileSync('git', ['-C', workspace, 'check-ignore', '-q', '--', probe], {
|
|
549
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
550
|
+
env: gitEnv,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
refuseAutoloopLedgerIsolation();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Fresh-start workspace boundary shared by `autoloopStart` and a fresh workflow
|
|
560
|
+
* `autoloop` boot: refuse a dirty Git worktree, then isolate the run ledger.
|
|
561
|
+
* `invalidRunId: 'defer'` skips isolation for invalid ids (direct start — the
|
|
562
|
+
* kernel rejects them). `'refuse'` fails closed with the fixed isolation error
|
|
563
|
+
* (fresh workflow path — config.runId is not the kernel run id).
|
|
564
|
+
*/
|
|
565
|
+
function applyAutoloopFreshStartBoundary(workspace, runId, opts) {
|
|
566
|
+
// Refuse a dirty git worktree before the run directory or Planner exists.
|
|
567
|
+
// --porcelain=v1 -z keeps paths literal (spaces/quotes). --untracked-files=all
|
|
568
|
+
// overrides status.showUntrackedFiles=no, while --ignore-submodules=none
|
|
569
|
+
// prevents repository config from hiding submodule dirt; do not pass
|
|
570
|
+
// --ignored. Only the deterministic not-a-git-repository error allows the
|
|
571
|
+
// start; every other status failure rejects. Do not raise maxBuffer:
|
|
572
|
+
// ENOBUFS or already-nonempty stdout means dirt. Untracked (??) or
|
|
573
|
+
// index-clean worktree mods
|
|
574
|
+
// (` M`) under tasks/<id>/ are ignored only when tasks/<id>/goal.json is a
|
|
575
|
+
// regular file — every other status (staged/rename/copy/delete/conflict/
|
|
576
|
+
// malformed) rejects.
|
|
577
|
+
// Nested workspaces: status paths are repo-root-relative; strip a verified
|
|
578
|
+
// `rev-parse --show-prefix` before the ledger check; paths outside the
|
|
579
|
+
// workspace still refuse.
|
|
580
|
+
const dirtyStartError = new Error('Autoloop workspace has uncommitted changes; commit or stash them before starting');
|
|
581
|
+
let porcelain = '';
|
|
582
|
+
let inGitRepository = false;
|
|
583
|
+
try {
|
|
584
|
+
porcelain = execFileSync('git', ['-C', workspace, 'status', '--porcelain=v1', '-z', '--untracked-files=all', '--ignore-submodules=none'], {
|
|
585
|
+
encoding: 'utf8',
|
|
586
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
587
|
+
env: { ...process.env, LC_ALL: 'C' },
|
|
588
|
+
});
|
|
589
|
+
inGitRepository = true;
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
const e = err;
|
|
593
|
+
const partial = typeof e.stdout === 'string' ? e.stdout : Buffer.isBuffer(e.stdout) ? e.stdout.toString('utf8') : '';
|
|
594
|
+
if (e.code === 'ENOBUFS' || partial.trim() !== '' || !isNotGitRepositoryError(err)) {
|
|
595
|
+
throw dirtyStartError;
|
|
596
|
+
}
|
|
597
|
+
porcelain = '';
|
|
598
|
+
}
|
|
599
|
+
if (porcelain.length > 0) {
|
|
600
|
+
const prefix = readVerifiedGitShowPrefix(workspace);
|
|
601
|
+
if (prefix === null || porcelainHasDisallowedAutoloopDirt(workspace, porcelain, prefix)) {
|
|
602
|
+
throw dirtyStartError;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Run-local ledger isolation: tasks/<runId>/.gitignore (*) + check-ignore proof.
|
|
606
|
+
// Git workspaces only; skips invalid ids so kernel.start rejects them unchanged.
|
|
607
|
+
if (inGitRepository) {
|
|
608
|
+
if (!isValidRunId(runId)) {
|
|
609
|
+
if (opts.invalidRunId === 'refuse')
|
|
610
|
+
refuseAutoloopLedgerIsolation();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
ensureAutoloopRunLedgerIgnored(workspace, runId);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
457
616
|
/**
|
|
458
617
|
* The tool calls a turn's `result` event says the engine refused, normalized.
|
|
459
618
|
*
|
|
@@ -487,6 +646,128 @@ function readPermissionDenials(evt) {
|
|
|
487
646
|
}
|
|
488
647
|
return out;
|
|
489
648
|
}
|
|
649
|
+
/** Fixed refusal when a non-fresh boot cannot trust the closed-ledger prefix. */
|
|
650
|
+
const AUTOLOOP_CLOSED_LEDGER_REFUSAL = 'Autoloop resume refused: ledger closed-prefix is incomplete or malformed';
|
|
651
|
+
const REVIEWER_DECISIONS = new Set(['advance', 'hold', 'rollback']);
|
|
652
|
+
/**
|
|
653
|
+
* One-reader closed-ledger restore for non-fresh `_bootAutoloop`.
|
|
654
|
+
*
|
|
655
|
+
* Derives typed `iter` / `metric_history` from the contiguous valid closed
|
|
656
|
+
* prefix starting at 0. Checkpoint bytes are never progress authority.
|
|
657
|
+
* Returns null when there is no valid prefix (legacy lone verdict, empty).
|
|
658
|
+
* Throws {@link AUTOLOOP_CLOSED_LEDGER_REFUSAL} on gap / malformed tail.
|
|
659
|
+
*/
|
|
660
|
+
/** Canonical numeric iter directory names — no leading zeros. */
|
|
661
|
+
const CLOSED_LEDGER_ITER_DIR = /^(0|[1-9][0-9]*)$/;
|
|
662
|
+
function readClosedLedgerRestore(ledgerDir) {
|
|
663
|
+
const iterRoot = path.join(ledgerDir, 'iter');
|
|
664
|
+
if (!fs.existsSync(iterRoot))
|
|
665
|
+
return null;
|
|
666
|
+
let numericDirs;
|
|
667
|
+
try {
|
|
668
|
+
numericDirs = fs
|
|
669
|
+
.readdirSync(iterRoot)
|
|
670
|
+
.filter((name) => CLOSED_LEDGER_ITER_DIR.test(name))
|
|
671
|
+
.map((name) => Number(name))
|
|
672
|
+
.sort((a, b) => a - b);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
if (numericDirs.length === 0)
|
|
678
|
+
return null;
|
|
679
|
+
const present = new Set(numericDirs);
|
|
680
|
+
const metrics = [];
|
|
681
|
+
let i = 0;
|
|
682
|
+
for (;;) {
|
|
683
|
+
const iterDir = path.join(iterRoot, String(i));
|
|
684
|
+
if (!fs.existsSync(iterDir)) {
|
|
685
|
+
// K=-1 / no valid closed prefix: never refuse on higher dirs (base Planner-only).
|
|
686
|
+
if (i === 0)
|
|
687
|
+
return null;
|
|
688
|
+
for (const j of present) {
|
|
689
|
+
if (j > i)
|
|
690
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
691
|
+
}
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
const directivePath = path.join(iterDir, 'directive.json');
|
|
695
|
+
const verdictPath = path.join(iterDir, 'verdict.json');
|
|
696
|
+
const hasDirective = fs.existsSync(directivePath);
|
|
697
|
+
const hasVerdict = fs.existsSync(verdictPath);
|
|
698
|
+
if (!hasDirective) {
|
|
699
|
+
if (hasVerdict) {
|
|
700
|
+
// Legacy lone verdict (or directive-less "closed" appearance).
|
|
701
|
+
if (i === 0)
|
|
702
|
+
return null;
|
|
703
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
704
|
+
}
|
|
705
|
+
// Empty/non-closed at 0: prefixless compat before any gap check.
|
|
706
|
+
if (i === 0)
|
|
707
|
+
return null;
|
|
708
|
+
for (const j of present) {
|
|
709
|
+
if (j > i)
|
|
710
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
if (!hasVerdict) {
|
|
715
|
+
// Open / directive-only / committed-without-review tail at i.
|
|
716
|
+
// No closed prefix yet: return null before gap checks (open0+lateriter1 compat).
|
|
717
|
+
if (i === 0)
|
|
718
|
+
return null;
|
|
719
|
+
for (const j of present) {
|
|
720
|
+
if (j > i)
|
|
721
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
722
|
+
}
|
|
723
|
+
const history = metrics.length > MAX_METRIC_HISTORY ? metrics.slice(metrics.length - MAX_METRIC_HISTORY) : metrics.slice();
|
|
724
|
+
return { iter: i, metric_history: history };
|
|
725
|
+
}
|
|
726
|
+
let raw;
|
|
727
|
+
try {
|
|
728
|
+
raw = fs.readFileSync(verdictPath, 'utf8');
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// Prefixless unreadable verdict at 0: no-restore / no-refusal (base compat).
|
|
732
|
+
if (i === 0)
|
|
733
|
+
return null;
|
|
734
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
735
|
+
}
|
|
736
|
+
let parsed;
|
|
737
|
+
try {
|
|
738
|
+
parsed = JSON.parse(raw);
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
if (i === 0)
|
|
742
|
+
return null;
|
|
743
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
744
|
+
}
|
|
745
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
746
|
+
if (i === 0)
|
|
747
|
+
return null;
|
|
748
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
749
|
+
}
|
|
750
|
+
const verdict = parsed;
|
|
751
|
+
if (verdict.iter !== i) {
|
|
752
|
+
if (i === 0)
|
|
753
|
+
return null;
|
|
754
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
755
|
+
}
|
|
756
|
+
if (typeof verdict.decision !== 'string' || !REVIEWER_DECISIONS.has(verdict.decision)) {
|
|
757
|
+
if (i === 0)
|
|
758
|
+
return null;
|
|
759
|
+
throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
|
|
760
|
+
}
|
|
761
|
+
if (typeof verdict.metric === 'number' && Number.isFinite(verdict.metric)) {
|
|
762
|
+
metrics.push(verdict.metric);
|
|
763
|
+
}
|
|
764
|
+
i += 1;
|
|
765
|
+
}
|
|
766
|
+
if (i === 0)
|
|
767
|
+
return null;
|
|
768
|
+
const history = metrics.length > MAX_METRIC_HISTORY ? metrics.slice(metrics.length - MAX_METRIC_HISTORY) : metrics.slice();
|
|
769
|
+
return { iter: i, metric_history: history };
|
|
770
|
+
}
|
|
490
771
|
export class SessionManager {
|
|
491
772
|
sessions = new Map();
|
|
492
773
|
_pendingSessions = new Map();
|
|
@@ -1140,12 +1421,17 @@ export class SessionManager {
|
|
|
1140
1421
|
// its executor is registered here with a builder closed over `this`
|
|
1141
1422
|
// rather than living in the kernel.
|
|
1142
1423
|
kernel.setExecutor('autoloop', makeAutoloopExecutor({
|
|
1143
|
-
boot: (config, secrets) => this._bootAutoloop({
|
|
1424
|
+
boot: (config, secrets, { freshStart }) => this._bootAutoloop({
|
|
1144
1425
|
...config,
|
|
1145
1426
|
// Custom-engine configs never reach the spec, so they come from
|
|
1146
1427
|
// the run's in-memory secret bag — supplied at start, and
|
|
1147
1428
|
// re-supplied by the caller on a resume.
|
|
1148
1429
|
...secrets,
|
|
1430
|
+
// Internal never-persisted marker LAST so spec.config / secrets
|
|
1431
|
+
// cannot override the kernel visits===1 fresh-start signal.
|
|
1432
|
+
// Always set (including false on resume) — omitting false left a
|
|
1433
|
+
// `_freshStart: true` from config/secrets intact.
|
|
1434
|
+
_freshStart: freshStart,
|
|
1149
1435
|
}),
|
|
1150
1436
|
ready: (key, value) => {
|
|
1151
1437
|
const deferred = this._autoloopReady.get(key);
|
|
@@ -2916,9 +3202,19 @@ export class SessionManager {
|
|
|
2916
3202
|
}
|
|
2917
3203
|
}
|
|
2918
3204
|
const ledgerDir = path.join(opts.workspace, 'tasks', opts.runId);
|
|
3205
|
+
if (opts._freshStart === true) {
|
|
3206
|
+
applyAutoloopFreshStartBoundary(opts.workspace, opts.runId, { invalidRunId: 'refuse' });
|
|
3207
|
+
}
|
|
2919
3208
|
if (!fs.existsSync(ledgerDir)) {
|
|
2920
3209
|
fs.mkdirSync(ledgerDir, { recursive: true });
|
|
2921
3210
|
}
|
|
3211
|
+
// Non-fresh boots: restore typed progress from the contiguous closed ledger
|
|
3212
|
+
// prefix before any dispatcher/runner/session work. Refusal throws here so
|
|
3213
|
+
// no Planner start or checkpoint publish occurs.
|
|
3214
|
+
let closedLedgerRestore = null;
|
|
3215
|
+
if (opts._freshStart !== true) {
|
|
3216
|
+
closedLedgerRestore = readClosedLedgerRestore(ledgerDir);
|
|
3217
|
+
}
|
|
2922
3218
|
// Per-run policy object — mutable so Planner's update_push_policy is visible
|
|
2923
3219
|
// to the runner without re-wiring.
|
|
2924
3220
|
const pushPolicy = JSON.parse(JSON.stringify(DEFAULT_PUSH_POLICY));
|
|
@@ -2990,6 +3286,10 @@ export class SessionManager {
|
|
|
2990
3286
|
autoloopHardTimeoutMs: opts.autoloopHardTimeoutMs,
|
|
2991
3287
|
});
|
|
2992
3288
|
runnerRef = runner;
|
|
3289
|
+
if (closedLedgerRestore) {
|
|
3290
|
+
runner.state.iter = closedLedgerRestore.iter;
|
|
3291
|
+
runner.state.metric_history = closedLedgerRestore.metric_history.slice();
|
|
3292
|
+
}
|
|
2993
3293
|
try {
|
|
2994
3294
|
await runner.start();
|
|
2995
3295
|
// A reconstructed migration becomes externally visible only after its
|
|
@@ -3095,43 +3395,7 @@ export class SessionManager {
|
|
|
3095
3395
|
throw new Error(`Autoloop session name '${sessionName}' is already in use`);
|
|
3096
3396
|
}
|
|
3097
3397
|
}
|
|
3098
|
-
|
|
3099
|
-
// --porcelain=v1 -z keeps paths literal (spaces/quotes). --untracked-files=all
|
|
3100
|
-
// overrides status.showUntrackedFiles=no, while --ignore-submodules=none
|
|
3101
|
-
// prevents repository config from hiding submodule dirt; do not pass
|
|
3102
|
-
// --ignored. Only the deterministic not-a-git-repository error allows the
|
|
3103
|
-
// start; every other status failure rejects. Do not raise maxBuffer:
|
|
3104
|
-
// ENOBUFS or already-nonempty stdout means dirt. Untracked (??) or
|
|
3105
|
-
// index-clean worktree mods
|
|
3106
|
-
// (` M`) under tasks/<id>/ are ignored only when tasks/<id>/goal.json is a
|
|
3107
|
-
// regular file — every other status (staged/rename/copy/delete/conflict/
|
|
3108
|
-
// malformed) rejects.
|
|
3109
|
-
// Nested workspaces: status paths are repo-root-relative; strip a verified
|
|
3110
|
-
// `rev-parse --show-prefix` before the ledger check; paths outside the
|
|
3111
|
-
// workspace still refuse.
|
|
3112
|
-
const dirtyStartError = new Error('Autoloop workspace has uncommitted changes; commit or stash them before starting');
|
|
3113
|
-
let porcelain = '';
|
|
3114
|
-
try {
|
|
3115
|
-
porcelain = execFileSync('git', ['-C', opts.workspace, 'status', '--porcelain=v1', '-z', '--untracked-files=all', '--ignore-submodules=none'], {
|
|
3116
|
-
encoding: 'utf8',
|
|
3117
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3118
|
-
env: { ...process.env, LC_ALL: 'C' },
|
|
3119
|
-
});
|
|
3120
|
-
}
|
|
3121
|
-
catch (err) {
|
|
3122
|
-
const e = err;
|
|
3123
|
-
const partial = typeof e.stdout === 'string' ? e.stdout : Buffer.isBuffer(e.stdout) ? e.stdout.toString('utf8') : '';
|
|
3124
|
-
if (e.code === 'ENOBUFS' || partial.trim() !== '' || !isNotGitRepositoryError(err)) {
|
|
3125
|
-
throw dirtyStartError;
|
|
3126
|
-
}
|
|
3127
|
-
porcelain = '';
|
|
3128
|
-
}
|
|
3129
|
-
if (porcelain.length > 0) {
|
|
3130
|
-
const prefix = readVerifiedGitShowPrefix(opts.workspace);
|
|
3131
|
-
if (prefix === null || porcelainHasDisallowedAutoloopDirt(opts.workspace, porcelain, prefix)) {
|
|
3132
|
-
throw dirtyStartError;
|
|
3133
|
-
}
|
|
3134
|
-
}
|
|
3398
|
+
applyAutoloopFreshStartBoundary(opts.workspace, opts.runId, { invalidRunId: 'defer' });
|
|
3135
3399
|
const tag = `${opts.runId}:${randomUUID()}`;
|
|
3136
3400
|
const ready = new Promise((resolve, reject) => {
|
|
3137
3401
|
this._autoloopReady.set(tag, { resolve, reject });
|